mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-06-27 17:39:39 -06:00
Compare commits
49 Commits
1.3.2
...
154f056422
Author | SHA1 | Date | |
---|---|---|---|
154f056422 | |||
58c2dc4ac4 | |||
d6232008d5 | |||
076dd9a56a | |||
952be47f3c | |||
b3c2fbcfa7 | |||
030d9f768f | |||
d4f7dfabf4 | |||
3ec079855d | |||
ef0dac5533 | |||
10e6fbcb72 | |||
de25673820 | |||
ba250df73d | |||
bfd715b607 | |||
d7929a7f0e | |||
1e86aa9764 | |||
175d5f9bb3 | |||
8765dc9901 | |||
74a9b94227 | |||
d3208a4c44 | |||
5d136980a3 | |||
572ad1eac5 | |||
6bb2af0091 | |||
534a194ed9 | |||
331805791e | |||
6773406bb6 | |||
6226eadf55 | |||
b1cde5fd97 | |||
39944b2063 | |||
973c6ba5df | |||
6803c91da8 | |||
557c2a50b2 | |||
77a797f154 | |||
faf9e3cdd7 | |||
7bc80ed4fe | |||
a1d44ec496 | |||
bab3beb0ac | |||
aa9e74339b | |||
908273d848 | |||
b51ad11574 | |||
ea027d65a7 | |||
d03ae9c164 | |||
90e9492f6c | |||
512120db04 | |||
90582e9e93 | |||
b97fae08b5 | |||
eed6ef632d | |||
0409c15903 | |||
c58272ac20 |
4
.github/workflows/canary.yml
vendored
4
.github/workflows/canary.yml
vendored
@ -243,3 +243,7 @@ jobs:
|
||||
- name: Send notification webhook
|
||||
run: |
|
||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|FF4500|${{ secrets.CANARY_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false"
|
||||
|
||||
- name: Notify update server of new builds
|
||||
run: |
|
||||
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=canary' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}'
|
||||
|
77
.github/workflows/release.yml
vendored
77
.github/workflows/release.yml
vendored
@ -14,38 +14,6 @@ env:
|
||||
RELEASE: 1
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
name: Create tag
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
||||
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ steps.version_info.outputs.build_version }}
|
||||
tag: ${{ steps.version_info.outputs.build_version }}
|
||||
body: |
|
||||
# Stable builds:
|
||||
| Platform | Artifact |
|
||||
|--|--|
|
||||
| Windows 64-bit | [Stable Windows Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_x64.zip) |
|
||||
| Windows ARM 64-bit | [Stable Windows ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_arm64.zip) |
|
||||
| Linux 64-bit | [Stable Linux Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz) |
|
||||
| Linux ARM 64-bit | [Stable Linux ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_arm64.tar.gz) |
|
||||
| macOS | [Stable macOS Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz) |
|
||||
|
||||
**[Full Changelog](https://git.ryujinx.app/ryubing/ryujinx/-/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }})**
|
||||
omitBodyDuringUpdate: true
|
||||
owner: ${{ secrets.RC_OWNER }}
|
||||
repo: ${{ secrets.RC_STABLE_NAME }}
|
||||
token: ${{ secrets.ALT_RELEASE_TOKEN }}
|
||||
|
||||
release:
|
||||
name: Release for ${{ matrix.platform.name }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
@ -169,30 +137,6 @@ jobs:
|
||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync"
|
||||
shell: bash
|
||||
|
||||
- name: Pushing new release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ steps.version_info.outputs.build_version }}
|
||||
artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
|
||||
tag: ${{ steps.version_info.outputs.build_version }}
|
||||
body: |
|
||||
# Stable builds:
|
||||
| Platform | Artifact |
|
||||
|--|--|
|
||||
| Windows 64-bit | [Stable Windows Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_x64.zip) |
|
||||
| Windows ARM 64-bit | [Stable Windows ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_arm64.zip) |
|
||||
| Linux 64-bit | [Stable Linux Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz) |
|
||||
| Linux ARM 64-bit | [Stable Linux ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_arm64.tar.gz) |
|
||||
| macOS | [Stable macOS Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz) |
|
||||
|
||||
**[Full Changelog](https://git.ryujinx.app/ryubing/ryujinx/-/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }})**
|
||||
omitBodyDuringUpdate: true
|
||||
allowUpdates: true
|
||||
replacesArtifacts: true
|
||||
owner: ${{ secrets.RC_OWNER }}
|
||||
repo: ${{ secrets.RC_STABLE_NAME }}
|
||||
token: ${{ secrets.ALT_RELEASE_TOKEN }}
|
||||
|
||||
macos_release:
|
||||
name: Release MacOS universal
|
||||
runs-on: ubuntu-24.04
|
||||
@ -249,26 +193,11 @@ jobs:
|
||||
run: |
|
||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
|
||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
|
||||
|
||||
- name: Pushing new release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ steps.version_info.outputs.build_version }}
|
||||
artifacts: "publish/*.tar.gz"
|
||||
tag: ${{ steps.version_info.outputs.build_version }}
|
||||
body: ""
|
||||
omitBodyDuringUpdate: true
|
||||
allowUpdates: true
|
||||
replacesArtifacts: true
|
||||
owner: ${{ secrets.RC_OWNER }}
|
||||
repo: ${{ secrets.RC_STABLE_NAME }}
|
||||
token: ${{ secrets.ALT_RELEASE_TOKEN }}
|
||||
|
||||
|
||||
create_gitlab_release:
|
||||
name: Create GitLab Release
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- tag
|
||||
- macos_release
|
||||
- release
|
||||
steps:
|
||||
@ -299,3 +228,7 @@ jobs:
|
||||
- name: Send notification webhook
|
||||
run: |
|
||||
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|32cd32|${{ secrets.STABLE_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false"
|
||||
|
||||
- name: Notify update server of new builds
|
||||
run: |
|
||||
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=stable' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}'
|
||||
|
@ -2,20 +2,17 @@
|
||||
|
||||
All updates to this Ryujinx branch will be documented in this file.
|
||||
|
||||
## [1.3.2](<https://git.ryujinx.app/ryubing/ryujinx/-/releases/1.3.2>) - 2025-06-09
|
||||
|
||||
## [1.3.1](<https://git.ryujinx.app/ryubing/ryujinx/-/releases/1.3.1>) - 2025-04-23
|
||||
A list of notable changes can be found on the release linked in the version number above.
|
||||
|
||||
## [1.2.86](<https://github.com/Ryubing/Stable-Releases/releases/tag/1.2.86>) - 2025-03-13
|
||||
A list of notable changes can be found on the release linked in the version number above.
|
||||
|
||||
## [1.2.82](<https://web.archive.org/web/20250312010534/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.82>) - 2025-02-16
|
||||
A list of notable changes can be found on the release linked in the version number above.
|
||||
|
||||
## [1.2.80-81](<https://web.archive.org/web/20250302064257/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.81>) - 2025-01-22
|
||||
A list of notable changes can be found on the release linked in the version number above.
|
||||
|
||||
## [1.2.78](<https://web.archive.org/web/20250301174537/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.78>) - 2024-12-19
|
||||
A list of notable changes can be found on the release linked in the version number above.
|
||||
|
||||
## [1.2.73-1.2.76](<https://web.archive.org/web/20250209202612/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.76>) - 2024-11-19
|
||||
A list of notable changes can be found on the release linked in the version number above.
|
||||
@ -254,4 +251,4 @@ Added Low-power PPTC mode strings to the translation files.
|
||||
- Autoload DLC/Updates from dir ([#12](https://github.com/GreemDev/Ryujinx/pull/12)).
|
||||
- Changed executable icon to rainbow logo.
|
||||
- Extract Data > Logo now also extracts the square thumbnail you see for the game in the UI.
|
||||
- The "use random UUID hack" checkbox in the Amiibo screen now remembers its last state when you reopen the window in a given session.
|
||||
- The "use random UUID hack" checkbox in the Amiibo screen now remembers its last state when you reopen the window in a given session.
|
||||
|
@ -42,6 +42,8 @@
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.LibHac" Version="0.20.0" />
|
||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
|
||||
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.29" />
|
||||
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.29" />
|
||||
<PackageVersion Include="Gommon" Version="2.7.1.1" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="Sep" Version="0.6.0" />
|
||||
|
@ -77,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Gene
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
@ -84,10 +86,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
.github\workflows\canary.yml = .github\workflows\canary.yml
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
.github\workflows\release.yml = .github\workflows\release.yml
|
||||
nuget.config = nuget.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -187,7 +187,7 @@
|
||||
"ko_KR": "소프트웨어",
|
||||
"no_NO": "Programvare",
|
||||
"pl_PL": "Oprogramowanie",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Programa",
|
||||
"ru_RU": "Программное обеспечение",
|
||||
"sv_SE": "Programvara",
|
||||
"th_TH": "ซอฟต์แวร์",
|
||||
@ -2089,12 +2089,12 @@
|
||||
"pl_PL": "Całkowity czas gry: {0}",
|
||||
"pt_BR": "Tempo total de jogo: {0}",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"sv_SE": "Total speltid: {0}",
|
||||
"th_TH": "",
|
||||
"tr_TR": "Toplam Oyun Süresi: {0}",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "总游戏时间: {0}",
|
||||
"zh_TW": ""
|
||||
"zh_TW": "總遊戲時間: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -2537,7 +2537,7 @@
|
||||
"ko_KR": "PPTC 디렉터리 열기",
|
||||
"no_NO": "Åpne PPTC mappe",
|
||||
"pl_PL": "Otwórz katalog PPTC",
|
||||
"pt_BR": "Abrir Diretório de PPTC Cache",
|
||||
"pt_BR": "Abrir Diretório de Cache PPTC",
|
||||
"ru_RU": "Открыть папку PPTC",
|
||||
"sv_SE": "Öppna PPTC-katalog",
|
||||
"th_TH": "เปิดไดเรกทอรี่ PPTC",
|
||||
@ -2912,7 +2912,7 @@
|
||||
"ko_KR": "사용자 정의 구성 만들기",
|
||||
"no_NO": "Opprett egendefinert konfigurasjon",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Criar Configuração Custumizada",
|
||||
"ru_RU": "Задать индивидуальные параметры",
|
||||
"sv_SE": "Skapa anpassad konfiguration",
|
||||
"th_TH": "",
|
||||
@ -2937,7 +2937,7 @@
|
||||
"ko_KR": "사용자 정의 구성 편집",
|
||||
"no_NO": "Rediger egendefinert konfigurasjon",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Editar Configuração Customizada",
|
||||
"ru_RU": "Изменить индивидуальные параметры",
|
||||
"sv_SE": "Redigera anpassad konfiguration",
|
||||
"th_TH": "",
|
||||
@ -3012,7 +3012,7 @@
|
||||
"ko_KR": "선택한 게임에 대한 기존 독립 구성 편집",
|
||||
"no_NO": "Rediger din eksisterende uavhengige konfigurasjon for det valgte spillet",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Editar sua configuração independente existente para o jogo selecionado",
|
||||
"ru_RU": "Отредактировать существующие независимые параметры для выбранной игры.",
|
||||
"sv_SE": "Redigera din befintliga oberoende konfiguration för det valda spelet",
|
||||
"th_TH": "",
|
||||
@ -3162,7 +3162,7 @@
|
||||
"ko_KR": "앱의 모드가 포함된 디렉터리 열기",
|
||||
"no_NO": "Åpner mappen som inneholder programmets modifikasjoner",
|
||||
"pl_PL": "Otwiera katalog zawierający mody dla danej aplikacji",
|
||||
"pt_BR": "Abre a pasta que contém os mods da aplicação ",
|
||||
"pt_BR": "Abre a pasta que contém os mods da aplicação",
|
||||
"ru_RU": "Открывает папку, содержащую моды для приложений и игр",
|
||||
"sv_SE": "Öppnar katalogen som innehåller applikationens Mods",
|
||||
"th_TH": "เปิดไดเร็กทอรี่ Mods ของแอปพลิเคชัน",
|
||||
@ -3987,7 +3987,7 @@
|
||||
"ko_KR": "원래 UI 스타일 표시(다시 시작 필요)",
|
||||
"no_NO": "Vis original UI-stil (krever omstart)",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Mostrar Estilo Original da Interface (Requer Reinicialização)",
|
||||
"ru_RU": "Включить оригинальный интерфейса (требуется перезагрузка)",
|
||||
"sv_SE": "Visa ursprunglig gränssnittsstil (kräver omstart)",
|
||||
"th_TH": "",
|
||||
@ -4009,10 +4009,10 @@
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "Ryujinx 1.1.1403을 연상시키는 이전 Avalonia Ryujinx UI를 표시합니다. 이 기능은 Windows가 아닌 플랫폼에서는 기본적으로 활성화됩니다.\n 클래식 스타일의 타이틀 바가 돌아왔고 주요 창 레이아웃 재작업이 역전되었습니다. 이 툴팁 위의 설정 탐색 배치와 같은 작업입니다.",
|
||||
"ko_KR": "Ryujinx 1.1.1403을 연상시키는 이전 Avalonia Ryujinx UI를 표시합니다. 이 기능은 윈도가 아닌 플랫폼에서는 기본적으로 활성화됩니다.\n 클래식 스타일의 타이틀 바가 돌아왔고 주요 창 레이아웃 변경 사항이 원래대로 적용됩니다. 이 툴팁 위의 설정 탐색 배치와 같은 경우입니다.",
|
||||
"no_NO": "Vis det eldre Avalonia Ryujinx-grensesnittet som minner om Ryujinx 1.1.1403. Dette er aktivert som standard på plattformer som ikke er Windows.\nTittellinjen i klassisk stil er tilbake, og store omarbeidinger av vindusoppsettet er reversert, for eksempel plasseringen av innstillingsnavigasjonen over dette verktøytipset.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Mostrar a Interface Avalonia antiga do Ryujinx 1.1.1403. Esta versão é ativada por padrão nas plataformas que não sejam Windows. \nO estilo clássico da Barra de Título retorna e grande parte das mudanças do Layout de janela são revertidas; assim como as configurações de posicionamento da navegação acima dessa descrição.",
|
||||
"ru_RU": "Показать старый пользовательский интерфейс Avalonia Ryujinx, напоминающий Ryujinx 1.1.1403. Включено по умолчанию на платформах, отличных от Windows.\nСтрока заголовка в классическом стиле вернётся на место, а основные изменения в оформлении окна будут отменены; например, расположение навигации по настройкам над этой всплывающей подсказкой.",
|
||||
"sv_SE": "Visa det gamla Ryuijinx-gränssnittet baserat på Avalonia som påminner om version 1.1.1403. Detta är aktiverat som standard på plattformat som inte är Windows.\nDen klassiska titelfältet är tillbaka och de stora omarbetningarna av fönsterlayouten är omvända, till exempel placeringen av inställningsnavigeringen ovanför detta verktygstips.",
|
||||
"th_TH": "",
|
||||
@ -5087,7 +5087,7 @@
|
||||
"ko_KR": "터보 모드 배수 :",
|
||||
"no_NO": "Multiplikator i turbomodus:",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Multiplicador do Modo Turbo",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Multiplikator för turboläge:",
|
||||
"th_TH": "",
|
||||
@ -5112,7 +5112,7 @@
|
||||
"ko_KR": "터보 모드 배수 목표 값입니다.\n\n모르면 200으로 두세요.",
|
||||
"no_NO": "Målverdien for multiplikatoren i turbomodus. \n\nLa syå på 200 hvis du er usikker.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "O valor do Multiplicador do Modo Turbo. Deixe em 200 se não tiver certeza.",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Målvärdet för multiplikatorn i turboläget. \n\nLämna den på 200 om du är osäker.",
|
||||
"th_TH": "",
|
||||
@ -5137,7 +5137,7 @@
|
||||
"ko_KR": "터보 모드는 게임이 프레임 속도에 민감하지 않을 때 효과적으로 속도를 높이거나 낮추는 에뮬레이터 기능입니다.\n이 기능은 Ryujinx 키보드 단축키 설정에서 구성할 수 있는 단축키를 사용하여 게임 내에서 전환할 수 있습니다.\n\n모르면 200으로 두세요.",
|
||||
"no_NO": "Turbo-modus er en emulatorfunksjon som effektivt øker eller senker hastigheten når et spill ikke er følsom for bildefrekvens.\nDu kan slå på denne funksjonen i spillet med en hurtigtast, som kan konfigureres i Ryujinx Keyboard Hotkeys-innstillingene.\n\nLa den stå på 200 hvis du er usikker.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "O Modo Turbo é um recurso do emulador que efetivamente aumenta ou dimimui a velocidade de um jogo quando o mesmo não é sensivel à taxa de quadros. \nVocê pode ativar esse recurso dentro do jogo com uma tecla de atalho, configurável nas Configurações de Teclas de Atalho do Ryujinx. \n\nDeixe em 200 se não tiver certeza.",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Turboläget är en emulatorfunktion som effektivt ökar eller sänker hastigheten när ett spel inte är känsligt för bildfrekvens.\nDu kan växla denna funktion i spelet med en snabbtangent, konfigurerbar i Ryujinx inställningar för snabbtangenter.\n\nLämna den på 200 om du är osäker.",
|
||||
"th_TH": "",
|
||||
@ -5312,7 +5312,7 @@
|
||||
"ko_KR": "핵",
|
||||
"no_NO": "",
|
||||
"pl_PL": "Hacki",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Trapaças",
|
||||
"ru_RU": "Хаки",
|
||||
"sv_SE": "Hack",
|
||||
"th_TH": "แฮ็ก",
|
||||
@ -6112,7 +6112,7 @@
|
||||
"ko_KR": "개발자 옵션",
|
||||
"no_NO": "Utvikleralternativer",
|
||||
"pl_PL": "Opcje programisty",
|
||||
"pt_BR": "Opções do desenvolvedor",
|
||||
"pt_BR": "Opções do Desenvolvedor",
|
||||
"ru_RU": "Параметры разработчика",
|
||||
"sv_SE": "Utvecklarinställningar",
|
||||
"th_TH": "ตัวเลือกนักพัฒนา",
|
||||
@ -6237,7 +6237,7 @@
|
||||
"ko_KR": "조각 기록 활성화",
|
||||
"no_NO": "Aktiver Stub-logger",
|
||||
"pl_PL": "Wlącz Skróty Logów",
|
||||
"pt_BR": "Habilitar logs de Stub",
|
||||
"pt_BR": "Habilitar Logs de Stub",
|
||||
"ru_RU": "Включить журнал-заглушку",
|
||||
"sv_SE": "Aktivera stubbloggar",
|
||||
"th_TH": "เปิดใช้งานการบันทึกประวัติ",
|
||||
@ -6262,7 +6262,7 @@
|
||||
"ko_KR": "정보 기록 활성화",
|
||||
"no_NO": "Aktiver informasjonslogger",
|
||||
"pl_PL": "Włącz Logi Informacyjne",
|
||||
"pt_BR": "Habilitar logs de Informação",
|
||||
"pt_BR": "Habilitar Logs de Informação",
|
||||
"ru_RU": "Включить информационный журнал",
|
||||
"sv_SE": "Aktivera informationsloggar",
|
||||
"th_TH": "เปิดใช้งานการบันทึกประวัติการใช้งาน",
|
||||
@ -6462,7 +6462,7 @@
|
||||
"ko_KR": "개발자 옵션",
|
||||
"no_NO": "Utvikleralternativer",
|
||||
"pl_PL": "Opcje programisty (UWAGA: wpływa na wydajność)",
|
||||
"pt_BR": "Opções do desenvolvedor",
|
||||
"pt_BR": "Opções do Desenvolvedor",
|
||||
"ru_RU": "Параметры разработчика",
|
||||
"sv_SE": "Utvecklarinställningar",
|
||||
"th_TH": "ตัวเลือกนักพัฒนา",
|
||||
@ -7212,14 +7212,14 @@
|
||||
"ko_KR": "발견된 구성 :\n\n이름 : \t{0}\n가이드 : \t{1}\n\n 컨트롤러 연결 대기 중...",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Configuração encontrada:\n\nNome:\t{0}\nGUID:\t{1}\n\n Aguardando conexão do controle...",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"sv_SE": "Konfiguration hittad:\n\nNamn:\t{0}\nGUID:\t{1}\n\n Väntar på anslutning till kontroller...",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "发现配置:\n\n名称:\t{0}\nGUID:\t{1}\n\n 正在等待控制器连接...",
|
||||
"zh_TW": ""
|
||||
"zh_TW": "找到控制器的配置:\n\n名稱:\t{0}\nGUID:\t{1}\n\n 正在等待控制器連線..."
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -10912,7 +10912,7 @@
|
||||
"ko_KR": "좌측 스틱 버튼",
|
||||
"no_NO": "Venstre Styrespak Trykk",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Botão Analógico Esquerdo",
|
||||
"ru_RU": "Кнопка лев. стика",
|
||||
"sv_SE": "L-spakknapp",
|
||||
"th_TH": "",
|
||||
@ -10937,7 +10937,7 @@
|
||||
"ko_KR": "우측 스틱 버튼",
|
||||
"no_NO": "Høyre Styrespak Trykk",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Botão Analógico Direito",
|
||||
"ru_RU": "Кнопка пр. стика",
|
||||
"sv_SE": "R-spakknapp",
|
||||
"th_TH": "",
|
||||
@ -10962,7 +10962,7 @@
|
||||
"ko_KR": "좌측 숄더",
|
||||
"no_NO": "Venstre Skulder",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Ombro Esquerdo",
|
||||
"ru_RU": "Левый бампер",
|
||||
"sv_SE": "Vänster kantknapp",
|
||||
"th_TH": "",
|
||||
@ -10987,7 +10987,7 @@
|
||||
"ko_KR": "우측 숄더",
|
||||
"no_NO": "Høyre Skulder",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Ombro Direito",
|
||||
"ru_RU": "Правый бампер",
|
||||
"sv_SE": "Höger kantknapp",
|
||||
"th_TH": "",
|
||||
@ -11012,7 +11012,7 @@
|
||||
"ko_KR": "좌측 트리거",
|
||||
"no_NO": "Venstre utløser",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Gatilho Esquerdo",
|
||||
"ru_RU": "Левый триггер",
|
||||
"sv_SE": "Vänster avtryckare",
|
||||
"th_TH": "",
|
||||
@ -11037,7 +11037,7 @@
|
||||
"ko_KR": "우측 트리거",
|
||||
"no_NO": "Høyre utløser",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Gatilho Direito",
|
||||
"ru_RU": "Правый триггер",
|
||||
"sv_SE": "Höger avtryckare",
|
||||
"th_TH": "",
|
||||
@ -11062,7 +11062,7 @@
|
||||
"ko_KR": "↑",
|
||||
"no_NO": "Opp",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Cima",
|
||||
"ru_RU": "Вверх",
|
||||
"sv_SE": "Upp",
|
||||
"th_TH": "",
|
||||
@ -11087,7 +11087,7 @@
|
||||
"ko_KR": "↓",
|
||||
"no_NO": "Ned",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Baixo",
|
||||
"ru_RU": "Вниз",
|
||||
"sv_SE": "Ner",
|
||||
"th_TH": "",
|
||||
@ -11112,7 +11112,7 @@
|
||||
"ko_KR": "←",
|
||||
"no_NO": "Venstre",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Esquerda",
|
||||
"ru_RU": "Влево",
|
||||
"sv_SE": "Vänster",
|
||||
"th_TH": "",
|
||||
@ -11137,7 +11137,7 @@
|
||||
"ko_KR": "→",
|
||||
"no_NO": "Høyre",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Direita",
|
||||
"ru_RU": "Вправо",
|
||||
"sv_SE": "Höger",
|
||||
"th_TH": "",
|
||||
@ -11212,7 +11212,7 @@
|
||||
"ko_KR": "가이드",
|
||||
"no_NO": "Veiledning",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Guia",
|
||||
"ru_RU": "Кнопка меню",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
@ -11387,7 +11387,7 @@
|
||||
"ko_KR": "좌측 트리거 0",
|
||||
"no_NO": "Venstre utløser 0",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Gatilho Esquerdo 0",
|
||||
"ru_RU": "Левый триггер 0",
|
||||
"sv_SE": "Vänster avtryckare 0",
|
||||
"th_TH": "",
|
||||
@ -11412,7 +11412,7 @@
|
||||
"ko_KR": "우측 트리거 0",
|
||||
"no_NO": "Høyre utløser 0",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Gatilho Direito 0",
|
||||
"ru_RU": "Правый триггер 0",
|
||||
"sv_SE": "Höger avtryckare 0",
|
||||
"th_TH": "",
|
||||
@ -11437,7 +11437,7 @@
|
||||
"ko_KR": "좌측 트리거 1",
|
||||
"no_NO": "Venstre utløser 1",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": " Gatilho Esquerdo 1",
|
||||
"ru_RU": "Левый триггер 1",
|
||||
"sv_SE": "Vänster avtryckare 1",
|
||||
"th_TH": "",
|
||||
@ -11462,7 +11462,7 @@
|
||||
"ko_KR": "우측 트리거 1",
|
||||
"no_NO": "Høyre utløser 1",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Gatilho Direito 1",
|
||||
"ru_RU": "Правый триггер 1",
|
||||
"sv_SE": "Höger avtryckare 1",
|
||||
"th_TH": "",
|
||||
@ -11487,7 +11487,7 @@
|
||||
"ko_KR": "좌측 스틱",
|
||||
"no_NO": "Venstre styrespak",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Analógico Esquerdo",
|
||||
"ru_RU": "Левый стик",
|
||||
"sv_SE": "Vänster spak",
|
||||
"th_TH": "",
|
||||
@ -11512,7 +11512,7 @@
|
||||
"ko_KR": "우측 스틱",
|
||||
"no_NO": "Høyre styrespak",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Analógico Direito",
|
||||
"ru_RU": "Правый стик",
|
||||
"sv_SE": "Höger spak",
|
||||
"th_TH": "",
|
||||
@ -11787,7 +11787,7 @@
|
||||
"ko_KR": "사용자 지정 프로필 이미지를 가져오거나 시스템 펌웨어에서 아바타 선택 가능",
|
||||
"no_NO": "Du kan importere et tilpasset profilbilde, eller velge en avatar fra system fastvare",
|
||||
"pl_PL": "Możesz zaimportować niestandardowy obraz profilu lub wybrać awatar z firmware'u systemowego",
|
||||
"pt_BR": "Você pode importar uma imagem customizada, ou selecionar um avatar do firmware",
|
||||
"pt_BR": "Você pode importar uma imagem customizada, ou selecionar um avatar do Firmware",
|
||||
"ru_RU": "Вы можете импортировать собственное изображение или выбрать аватар из системной прошивки.",
|
||||
"sv_SE": "Du kan importera en anpassad profilbild eller välja en avatar från systemets firmware",
|
||||
"th_TH": "คุณสามารถนำเข้ารูปโปรไฟล์ที่กำหนดเองได้ หรือ เลือกรูปที่มีจากระบบ",
|
||||
@ -13087,7 +13087,7 @@
|
||||
"ko_KR": "업데이트가 취소되었습니다!",
|
||||
"no_NO": "Avbryter oppdatering!",
|
||||
"pl_PL": "Anulowanie aktualizacji!",
|
||||
"pt_BR": "Cancelando atualização!",
|
||||
"pt_BR": "Atualização Cancelada!",
|
||||
"ru_RU": "Отмена обновления...",
|
||||
"sv_SE": "Avbryter uppdatering!",
|
||||
"th_TH": "ยกเลิกการอัพเดต!",
|
||||
@ -13123,53 +13123,28 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUpdaterFailedToGetVersionMessage",
|
||||
"ID": "DialogUpdaterConvertFailedServerMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "حدث خطأ أثناء محاولة الحصول على معلومات الإصدار من إصدار غيت هاب. يمكن أن يحدث هذا إذا تم تجميع إصدار جديد بواسطة إجراءات غيت هاب. جرب مجددا بعد دقائق.",
|
||||
"de_DE": "Beim Versuch, Veröffentlichungs-Info von GitHub Release zu erhalten, ist ein Fehler aufgetreten. Dies kann aufgrund einer neuen Veröffentlichung, die gerade von GitHub Actions kompiliert wird, verursacht werden.",
|
||||
"el_GR": "Προέκυψε ένα σφάλμα στη λήψη πληροφοριών έκδοσης από τα GitHub Releases. Αυτό δύναται να συμβεί αν μία έκδοση χτίζεται αυτή τη στιγμή στα GitHub Actions. Παρακαλούμε προσπαθήστε αργότερα.",
|
||||
"en_US": "An error occurred while trying to retrieve release information from GitHub. This may happen if a new release is currently being compiled by GitHub Actions. Please try again in a few minutes.",
|
||||
"es_ES": "Se ha producido un error al intentar obtener información de liberación de GitHub Release. Esto puede ser causado si una nueva versión está siendo compilada por GitHub Actions. Inténtalo de nuevo en unos minutos.",
|
||||
"fr_FR": "Une erreur s'est produite lors de la tentative d'obtention des informations de publication de la version GitHub. Cela peut survenir lorsqu'une nouvelle version est en cours de compilation par GitHub Actions. Réessayez dans quelques minutes.",
|
||||
"he_IL": "אירעה שגיאה בעת ניסיון לקבל עדכונים מ-גיטהב. זה יכול להיגרם אם הגרסה המעודכנת האחרונה נוצרה על ידי פעולות של גיטהב. נסה שוב בעוד מספר דקות.",
|
||||
"it_IT": "Si è verificato un errore durante il tentativo di recuperare le informazioni sulla versione da GitHub Release. Ciò può verificarsi se una nuova versione è in fase di compilazione da GitHub Actions. Riprova tra qualche minuto.",
|
||||
"ja_JP": "Github からのリリース情報取得時にエラーが発生しました. Github Actions でリリースファイルを作成中かもしれません. 後ほどもう一度試してみてください.",
|
||||
"ko_KR": "GitHub에서 릴리스 정보를 검색하는 동안 오류가 발생했습니다. 현재 GitHub Actions에서 새 릴리스를 컴파일하는 중일 때 발생할 수 있습니다. 몇 분 후에 다시 시도해 주세요.",
|
||||
"no_NO": "En feil oppstod ved forsøk på å få utgivelsesinformasjon fra GitHub Utgivelse. Dette kan forårsakes hvis en ny utgave blir samlet av GitHub Handlinger. Prøv igjen om noen minutter.",
|
||||
"pl_PL": "Wystąpił błąd podczas próby uzyskania informacji o obecnej wersji z GitHub Release. Może to być spowodowane nową wersją kompilowaną przez GitHub Actions. Spróbuj ponownie za kilka minut.",
|
||||
"pt_BR": "Ocorreu um erro ao tentar obter as informações de atualização do GitHub Release. Isso pode ser causado se uma nova versão estiver sendo compilado pelas Ações do GitHub. Tente novamente em alguns minutos.",
|
||||
"ru_RU": "Произошла ошибка при попытке получить информацию о выпуске от GitHub Release. Это может быть вызвано тем, что в данный момент в GitHub Actions компилируется новый релиз. Повторите попытку позже.",
|
||||
"sv_SE": "Ett fel inträffade vid försök att hämta information om utgåvan från GitHub. Detta kan hända om en ny utgåva har kompilerats av GitHub Actions. Försök igen om några minuter.",
|
||||
"th_TH": "เกิดข้อผิดพลาดขณะพยายามรับข้อมูลเวอร์ชั่นจาก GitHub Release ปัญหานี้อาจเกิดขึ้นได้หากมีการรวบรวมเวอร์ชั่นใหม่โดย GitHub โปรดลองอีกครั้งในอีกไม่กี่นาทีข้างหน้า",
|
||||
"tr_TR": "GitHub tarafından sürüm bilgileri alınırken bir hata oluştu. Eğer yeni sürüm için hazırlıklar yapılıyorsa bu hatayı almanız olasıdır. Lütfen birkaç dakika sonra tekrar deneyiniz.",
|
||||
"uk_UA": "Під час спроби отримати інформацію про випуск із GitHub Release сталася помилка. Це може бути спричинено, якщо новий випуск компілюється GitHub Actions. Повторіть спробу через кілька хвилин.",
|
||||
"zh_CN": "尝试从 Github 获取版本信息时无效,可能由于 GitHub Actions 正在编译新版本。\n请过一会再试。",
|
||||
"zh_TW": "嘗試從 GitHub Release 取得發布資訊時發生錯誤。如果 GitHub Actions 正在編譯新版本,則可能會出現這種情況。請幾分鐘後再試一次。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUpdaterConvertFailedGithubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "فشل تحويل إصدار ريوجينكس المستلم من إصدار غيت هاب.",
|
||||
"de_DE": "Fehler beim Konvertieren der erhaltenen Ryujinx-Version von GitHub Release.",
|
||||
"el_GR": "Αποτυχία μετατροπής της ληφθείσας έκδοσης Ryujinx από την έκδοση GitHub.",
|
||||
"en_US": "Failed to convert the Ryujinx version received from GitHub.",
|
||||
"es_ES": "No se pudo convertir la versión de Ryujinx recibida de GitHub Release.",
|
||||
"fr_FR": "Impossible de convertir la version reçue de Ryujinx depuis GitHub Release.",
|
||||
"he_IL": "המרת גרסת ריוג'ינקס שהתקבלה מ-עדכון הגרסאות של גיטהב נכשלה.",
|
||||
"it_IT": "La conversione della versione di Ryujinx ricevuta da GitHub Release è fallita.",
|
||||
"ja_JP": "Github から取得した Ryujinx バージョンの変換に失敗しました.",
|
||||
"ko_KR": "GitHub에서 받은 Ryujinx 버전을 변환하지 못했습니다.",
|
||||
"no_NO": "Kan ikke konvertere mottatt Ryujinx-versjon fra GitHub Utgivelse.",
|
||||
"pl_PL": "Nie udało się przekonwertować otrzymanej wersji Ryujinx z Github Release.",
|
||||
"pt_BR": "Falha ao converter a versão Ryujinx recebida do GitHub.",
|
||||
"ru_RU": "Не удалось преобразовать полученную версию Ryujinx из GitHub Release.",
|
||||
"sv_SE": "Misslyckades med att konvertera mottagen Ryujinx-version från GitHub.",
|
||||
"th_TH": "ไม่สามารถแปลงเวอร์ชั่น Ryujinx ที่ได้รับจาก GitHub Release",
|
||||
"tr_TR": "Github Release'den alınan Ryujinx sürümü dönüştürülemedi.",
|
||||
"uk_UA": "Не вдалося конвертувати отриману версію Ryujinx із випуску GitHub.",
|
||||
"zh_CN": "无法切换至从 GitHub 接收到的新版 Ryujinx 模拟器。",
|
||||
"zh_TW": "無法轉換從 GitHub Release 接收到的 Ryujinx 版本。"
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Failed to convert the Ryujinx version received from the update server.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "La conversion de la version de Ryujinx reçue du serveur a échoué.",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "업데이트 서버에서 받은 Ryujinx 버전을 변환하는 데 실패했습니다.",
|
||||
"no_NO": "Kunne ikke konvertere Ryujinx-versjonen som ble mottatt fra oppdateringsserveren.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Falha em atualizar a versão do Ryujinx recebida do servidor de atualização.",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Det gick inte att konvertera Ryujinx-versionen som mottogs från uppdateringsservern.",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "无法转换从更新服务器接收的 Ryujinx 版本。",
|
||||
"zh_TW": "無法轉換從更新伺服器接收的 Ryujinx 版本。"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -16584,17 +16559,17 @@
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"ko_KR": "사용자 지정 설정에서 이 옵션을 활성화하면 전역 입력 구성이 사용됩니다.\n\n전역 설정에서 필요에 따라 활성화하거나 비활성화할 수 있습니다. 이 설정은 새로 생성된 모든 사용자 지정 구성에 상속됩니다.",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Se esta opção está ativada nas configurações customizadas, as configurações globais de entrada serão usadas.\n\nNas configurações globais: você pode ativar ou desativá-las se necessário; está configuração será herdada por qualquer nova configuração customizada criada.",
|
||||
"ru_RU": "Если эта опция включена в пользовательских настройках, будет использована глобальная конфигурация ввода.\n\nВ глобальных настройках: переключите эту опцию по своему усмотрению, это будет унаследовано для вновь созданых пользовательских конфигураций",
|
||||
"sv_SE": "",
|
||||
"sv_SE": "Om det här alternativet är aktiverat i anpassade inställningar kommer den globala inmatningskonfigurationen att användas.\n\nI de globala inställningarna: du kan aktivera eller inaktivera det efter behov; den här inställningen kommer att ärvas av alla nya anpassade konfigurationer som skapas.",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
"zh_CN": "如果在自定义设置中启用了此选项,则将使用全局输入配置。\n\n在全局设置中: 您可以根据需要启用或禁用它;之后创建的任何自定义配置都将继承此设置。",
|
||||
"zh_TW": "如果在自訂設定啟用了此選項,則將使用全域輸入配置。\n\n在全域設定中:你可以根據需要啟用或停用它;之後建立的任何自訂配置都將繼承此設定。"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -18569,7 +18544,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "{0} FPS ({1}毫秒)",
|
||||
"zh_TW": ""
|
||||
"zh_TW": "{0} FPS ({1}毫秒)"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23087,7 +23062,7 @@
|
||||
"ko_KR": "근린",
|
||||
"no_NO": "Nærmeste",
|
||||
"pl_PL": "Najbliższe",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Mais Próximo",
|
||||
"ru_RU": "Ступенчатая",
|
||||
"sv_SE": "Närmaste",
|
||||
"th_TH": "ใกล้สุด",
|
||||
@ -23462,14 +23437,14 @@
|
||||
"ko_KR": "변경 로그 보기",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Ver Registro",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"sv_SE": "Visa ändringslogg",
|
||||
"th_TH": "ด",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "查看更新日志",
|
||||
"zh_TW": ""
|
||||
"zh_TW": "檢視更新日誌"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23694,7 +23669,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Вимкнути хостинг P2P мережі, піри будуть підключатися через майстер-сервер замість прямого з'єднання з вами.",
|
||||
"zh_CN": "禁用 P2P 网络连接,对方将通过主服务器进行连接,而不是直接连接到您。",
|
||||
"zh_TW": "停用對等網路代管 (P2P Network Hosting), 用戶群會經過代理何服器而非直接連線至你的主機。"
|
||||
"zh_TW": "停用對等網路代管 (P2P Network Hosting), 用戶群會經過代理伺服器而非直接連線至你的主機。"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -24337,7 +24312,7 @@
|
||||
"ko_KR": "터보 모드 :",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Modo Turbo:",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Turboläge:",
|
||||
"th_TH": "",
|
||||
@ -24362,7 +24337,7 @@
|
||||
"ko_KR": "터보 모드 단축키입니다.\nRyujinx CPU 설정에서 터보 모드의 동작을 구성합니다.\n\n모르면 바인딩 해제 상태로 두세요.",
|
||||
"no_NO": "Hurtigtasten for turbo-modus.\nKonfigurer oppførselen til turbo-modus i Ryujinx CPU-innstillinger.\n\nLa være ubundet hvis du er usikker.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Tecla de atalho do Modo Turbo.\nConfigure o comportamento do Modo Turbo nas configurações de CPU do Ryujinx.\n\nDeixe Não Atribuído se não tiver certeza.",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Snabbtangenten för turboläge.\nKonfigurera beteendet för turboläge i Ryujinx CPU-inställningar.\n\nLämna Obunden om du är osäker.",
|
||||
"th_TH": "",
|
||||
@ -24387,7 +24362,7 @@
|
||||
"ko_KR": "누르고 있는 동안만",
|
||||
"no_NO": "Bare mens du trykker på",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Somente enquanto pressionado.",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "Endast när du trycker ner",
|
||||
"th_TH": "",
|
||||
@ -24397,6 +24372,156 @@
|
||||
"zh_TW": "只在按下時"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabHotkeysCycleInputDevicePlayerX",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Cycle Input Device {0}:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ControllerOverlayTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Controller Bindings",
|
||||
"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": "ControllerOverlayNoController",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "No controller assigned",
|
||||
"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": "ControllerOverlayKeyboard",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Keyboard",
|
||||
"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": "ControllerOverlayController",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Controller",
|
||||
"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": "ControllerOverlayUnknown",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Unknown",
|
||||
"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": "CompatibilityListLastUpdated",
|
||||
"Translations": {
|
||||
@ -24437,7 +24562,7 @@
|
||||
"ko_KR": "호환성 목록 - {0}개 항목",
|
||||
"no_NO": "Kompatibilitetsliste - {0} oppføringer",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Lista de Compatibilidade - {0} registros",
|
||||
"ru_RU": "Список совместимости — записей: {0}",
|
||||
"sv_SE": "Kompatibilitetslista - {0} poster",
|
||||
"th_TH": "",
|
||||
@ -24512,7 +24637,7 @@
|
||||
"ko_KR": "어카이브 {0} 호환성 항목...",
|
||||
"no_NO": "Søk i {0} kompatibilitetsoppføringer...",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Procurando {0} registros de compatibilidade...",
|
||||
"ru_RU": "Поиск среди {0} записей о совместимости...",
|
||||
"sv_SE": "Sök i {0} kompatibilitetsposter...",
|
||||
"th_TH": "",
|
||||
@ -24564,7 +24689,7 @@
|
||||
"pl_PL": "Gry i Aplikacje",
|
||||
"pt_BR": "Jogos e Aplicativos",
|
||||
"ru_RU": "Игры и Приложения",
|
||||
"sv_SE": "Spel och Applikationer",
|
||||
"sv_SE": "Spel och applikationer",
|
||||
"th_TH": "",
|
||||
"tr_TR": "Oyunlar ve Uygulamalar",
|
||||
"uk_UA": "Ігри та Додатки",
|
||||
@ -24614,7 +24739,7 @@
|
||||
"pl_PL": "Problemy i Cechy",
|
||||
"pt_BR": "Problemas e Características",
|
||||
"ru_RU": "Проблемы и Особенности",
|
||||
"sv_SE": "Problem och Egenskaper",
|
||||
"sv_SE": "Problem och egenskaper",
|
||||
"th_TH": "",
|
||||
"tr_TR": "Sorunlar ve Özellikler",
|
||||
"uk_UA": "Проблеми та Особливості",
|
||||
@ -24937,7 +25062,7 @@
|
||||
"ko_KR": "사용자 정의 설정",
|
||||
"no_NO": "Tilpasset konfigurasjon",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"pt_BR": "Configurações Customizadas",
|
||||
"ru_RU": "Индивидуальные параметры",
|
||||
"sv_SE": "Anpassad konfiguration",
|
||||
"th_TH": "",
|
||||
@ -25046,6 +25171,131 @@
|
||||
"zh_CN": "动态 Rich Presence",
|
||||
"zh_TW": "動態 Rich Presence"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabUIControllerOverlay",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Controller Overlay",
|
||||
"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": "SettingsTabUIControllerOverlayGameStartDuration",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show on game start (seconds):",
|
||||
"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": "SettingsTabUIControllerOverlayGameStartDurationTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Duration to show controller overlay when game starts (0 = disabled)",
|
||||
"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": "SettingsTabUIControllerOverlayInputCycleDuration",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show on input cycle (seconds):",
|
||||
"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": "SettingsTabUIControllerOverlayInputCycleDurationTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Duration to show controller overlay when cycling inputs (0 = disabled)",
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2746,6 +2746,7 @@
|
||||
01005D701264A000,"SpyHack",,playable,2021-04-15 10:53:51
|
||||
010077B00E046000,"Spyro™ Reignited Trilogy",nvdec;UE4,playable,2022-09-11 18:38:33
|
||||
0100085012A0E000,"Squeakers",,playable,2020-12-13 12:13:05
|
||||
0100E1D01EB2E000,"Squeakross: Home Squeak Home",,playable,2025-06-16 02:02:00
|
||||
010009300D31C000,"Squidgies Takeover",,playable,2020-07-20 22:28:08
|
||||
0100FCD0102EC000,"Squidlit",,playable,2020-08-06 12:38:32
|
||||
0100EBF00E702000,"STAR OCEAN First Departure R",nvdec,playable,2021-07-05 19:29:16
|
||||
@ -3016,6 +3017,7 @@
|
||||
01009B101044C000,"The Legend of Heroes: Trails of Cold Steel III Demo",demo;nvdec,playable,2021-04-23 01:07:32
|
||||
0100D3C010DE8000,"The Legend of Heroes: Trails of Cold Steel IV",nvdec,playable,2021-04-23 14:01:05
|
||||
01005E5013862000,"THE LEGEND OF HEROES: ZERO NO KISEKI KAI [英雄傳說 零之軌跡:改]",crash,nothing,2021-09-30 14:41:07
|
||||
01009C901ACEE000,"The Legend of Nayuta: Boundless Trails",,ingame,2025-06-12 15:47
|
||||
01008CF01BAAC000,"The Legend of Zelda Echoes of Wisdom",nvdec;ASTC;intel-vendor-bug,playable,2024-10-01 14:11:01
|
||||
0100509005AF2000,"The Legend of Zelda: Breath of the Wild Demo",demo,ingame,2022-12-24 05:02:58
|
||||
01007EF00011E000,"The Legend of Zelda™: Breath of the Wild",gpu;amd-vendor-bug;mac-bug,ingame,2024-09-23 19:35:46
|
||||
|
|
15
nuget.config
15
nuget.config
@ -6,5 +6,20 @@
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<!-- Only needed when using pre-release versions of Ryujinx.LibHac. -->
|
||||
<!--<add key="LibHacAlpha" value="https://git.ryujinx.app/api/v4/projects/17/packages/nuget/index.json" />-->
|
||||
<add key="Ryujinx.UpdateClient" value="https://git.ryujinx.app/api/v4/projects/71/packages/nuget/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||
<!-- These are defined and .NET still yells about multiple package sources with no mappings. Not sure what to do, this is in the docs lol -->
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="Ryujinx.UpdateClient">
|
||||
<package pattern="Ryujinx.UpdateClient" />
|
||||
<package pattern="Ryujinx.Systems.Update.Common" />
|
||||
</packageSource>
|
||||
<!--<packageSource key="LibHacAlpha">
|
||||
<package pattern="Ryujinx.LibHac" />
|
||||
</packageSource>-->
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
|
@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides default input configurations for keyboard and controller devices
|
||||
/// </summary>
|
||||
public static class DefaultInputConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a default keyboard input configuration
|
||||
/// </summary>
|
||||
/// <param name="id">Device ID</param>
|
||||
/// <param name="name">Device name</param>
|
||||
/// <param name="playerIndex">Player index</param>
|
||||
/// <param name="controllerType">Controller type (defaults to ProController)</param>
|
||||
/// <returns>Default keyboard input configuration</returns>
|
||||
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfig(string id, string name, PlayerIndex playerIndex, ControllerType controllerType = ControllerType.ProController)
|
||||
{
|
||||
return new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = id,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
PlayerIndex = playerIndex,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default controller input configuration
|
||||
/// </summary>
|
||||
/// <param name="id">Device ID</param>
|
||||
/// <param name="name">Device name</param>
|
||||
/// <param name="playerIndex">Player index</param>
|
||||
/// <param name="isNintendoStyle">Whether to use Nintendo-style button mapping</param>
|
||||
/// <returns>Default controller input configuration</returns>
|
||||
public static StandardControllerInputConfig CreateDefaultControllerConfig(string id, string name, PlayerIndex playerIndex, bool isNintendoStyle = false)
|
||||
{
|
||||
// Split the ID for controller configs
|
||||
string cleanId = id.Split(" ")[0];
|
||||
|
||||
return new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL2,
|
||||
Id = cleanId,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
PlayerIndex = playerIndex,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
EnableMotion = true,
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
GyroDeadzone = 1,
|
||||
Sensitivity = 100,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
EnableRumble = false,
|
||||
WeakRumble = 1f,
|
||||
StrongRumble = 1f,
|
||||
},
|
||||
Led = new LedConfigController
|
||||
{
|
||||
EnableLed = false,
|
||||
TurnOffLed = false,
|
||||
UseRainbow = false,
|
||||
LedColor = 0xFFFFFFFF,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the short name of a gamepad by removing SDL prefix and truncating if too long
|
||||
/// </summary>
|
||||
/// <param name="name">Full gamepad name</param>
|
||||
/// <param name="maxLength">Maximum length before truncation (default: 50)</param>
|
||||
/// <returns>Short gamepad name</returns>
|
||||
public static string GetShortGamepadName(string name, int maxLength = 50)
|
||||
{
|
||||
const string SdlGamepadNamePrefix = "SDL2 Gamepad ";
|
||||
const string Ellipsis = "...";
|
||||
|
||||
// First remove SDL prefix if present
|
||||
string shortName = name;
|
||||
if (name.StartsWith(SdlGamepadNamePrefix))
|
||||
{
|
||||
shortName = name[SdlGamepadNamePrefix.Length..];
|
||||
}
|
||||
|
||||
// Then truncate if too long
|
||||
if (shortName.Length > maxLength)
|
||||
{
|
||||
return $"{shortName.AsSpan(0, maxLength - Ellipsis.Length)}{Ellipsis}";
|
||||
}
|
||||
|
||||
return shortName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a controller uses Nintendo-style button mapping
|
||||
/// </summary>
|
||||
/// <param name="name">Controller name</param>
|
||||
/// <returns>True if Nintendo-style mapping should be used</returns>
|
||||
public static bool IsNintendoStyleController(string name)
|
||||
{
|
||||
return name.Contains("Nintendo");
|
||||
}
|
||||
}
|
||||
}
|
@ -15,5 +15,14 @@ namespace Ryujinx.Common.Configuration.Hid
|
||||
public Key CustomVSyncIntervalDecrement { get; set; }
|
||||
public Key TurboMode { get; set; }
|
||||
public bool TurboModeWhileHeld { get; set; }
|
||||
public Key CycleInputDevicePlayer1 { get; set; }
|
||||
public Key CycleInputDevicePlayer2 { get; set; }
|
||||
public Key CycleInputDevicePlayer3 { get; set; }
|
||||
public Key CycleInputDevicePlayer4 { get; set; }
|
||||
public Key CycleInputDevicePlayer5 { get; set; }
|
||||
public Key CycleInputDevicePlayer6 { get; set; }
|
||||
public Key CycleInputDevicePlayer7 { get; set; }
|
||||
public Key CycleInputDevicePlayer8 { get; set; }
|
||||
public Key CycleInputDeviceHandheld { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -195,6 +195,7 @@ namespace Ryujinx.Common
|
||||
"01008d100d43e000", // Saints Row IV
|
||||
"0100de600beee000", // Saints Row: The Third - The Full Package
|
||||
"01001180021fa000", // Shovel Knight: Specter of Torment
|
||||
"0100e1D01eb2e000", // Squeakross: Home Squeak Home
|
||||
"0100e65002bb8000", // Stardew Valley
|
||||
"0100d7a01b7a2000", // Star Wars: Bounty Hunter
|
||||
"0100800015926000", // Suika Game
|
||||
|
@ -41,5 +41,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
public void SetScalingFilterLevel(float level) { }
|
||||
|
||||
public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying implementation window for direct access
|
||||
/// </summary>
|
||||
public IWindow BaseWindow => _impl.Window;
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Gpu
|
||||
/// GPU synchronization manager.
|
||||
/// </summary>
|
||||
public SynchronizationManager Synchronization { get; }
|
||||
public IOverlayManager OverlayManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Presentation window.
|
||||
@ -121,14 +122,18 @@ namespace Ryujinx.Graphics.Gpu
|
||||
/// Creates a new instance of the GPU emulation context.
|
||||
/// </summary>
|
||||
/// <param name="renderer">Host renderer</param>
|
||||
public GpuContext(IRenderer renderer, DirtyHacks hacks)
|
||||
/// <param name="hacks">Enabled dirty hacks</param>
|
||||
/// <param name="overlayManager">Overlay manager for rendering overlays</param>
|
||||
public GpuContext(IRenderer renderer, DirtyHacks hacks, IOverlayManager overlayManager)
|
||||
{
|
||||
Renderer = renderer;
|
||||
|
||||
GPFifo = new GPFifoDevice(this);
|
||||
|
||||
Synchronization = new SynchronizationManager();
|
||||
|
||||
|
||||
OverlayManager = overlayManager;
|
||||
|
||||
Window = new Window(this);
|
||||
|
||||
HostInitalized = new ManualResetEvent(false);
|
||||
@ -462,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu
|
||||
RunDeferredActions();
|
||||
|
||||
Renderer.Dispose();
|
||||
|
||||
OverlayManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
src/Ryujinx.Graphics.Gpu/IOverlay.cs
Normal file
54
src/Ryujinx.Graphics.Gpu/IOverlay.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for overlay functionality
|
||||
/// </summary>
|
||||
public interface IOverlay : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the overlay
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the overlay is visible
|
||||
/// </summary>
|
||||
bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Opacity of the overlay (0.0 to 1.0)
|
||||
/// </summary>
|
||||
float Opacity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// X position of the overlay
|
||||
/// </summary>
|
||||
float X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Y position of the overlay
|
||||
/// </summary>
|
||||
float Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Z-index for overlay ordering
|
||||
/// </summary>
|
||||
int ZIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay (for animations)
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time elapsed since last update</param>
|
||||
/// <param name="screenSize">Current screen size</param>
|
||||
void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render this overlay
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to</param>
|
||||
void Render(SKCanvas canvas);
|
||||
}
|
||||
}
|
30
src/Ryujinx.Graphics.Gpu/IOverlayManager.cs
Normal file
30
src/Ryujinx.Graphics.Gpu/IOverlayManager.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for overlay management functionality
|
||||
/// </summary>
|
||||
public interface IOverlayManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an overlay to the manager
|
||||
/// </summary>
|
||||
/// <param name="overlay">The overlay to add</param>
|
||||
void AddOverlay(IOverlay overlay);
|
||||
|
||||
/// <summary>
|
||||
/// Update all overlays (for animations)
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time elapsed since last update</param>
|
||||
/// <param name="screenSize">Current screen size</param>
|
||||
void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render all visible overlays
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to</param>
|
||||
void Render(SKCanvas canvas);
|
||||
}
|
||||
}
|
@ -14,4 +14,8 @@
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,10 +1,14 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
using Ryujinx.Graphics.Gpu.Memory;
|
||||
using Ryujinx.Graphics.Texture;
|
||||
using Ryujinx.Memory.Range;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
@ -15,6 +19,7 @@ namespace Ryujinx.Graphics.Gpu
|
||||
public class Window
|
||||
{
|
||||
private readonly GpuContext _context;
|
||||
private DateTime? _lastUpdateTime = null;
|
||||
|
||||
/// <summary>
|
||||
/// Texture presented on the window.
|
||||
@ -207,6 +212,9 @@ namespace Ryujinx.Graphics.Gpu
|
||||
|
||||
texture.SynchronizeMemory();
|
||||
|
||||
// Add overlays by modifying texture data directly
|
||||
AddOverlaysToTexture(texture, pt.Crop);
|
||||
|
||||
ImageCrop crop = new(
|
||||
(int)(pt.Crop.Left * texture.ScaleFactor),
|
||||
(int)MathF.Ceiling(pt.Crop.Right * texture.ScaleFactor),
|
||||
@ -244,6 +252,99 @@ namespace Ryujinx.Graphics.Gpu
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add overlay to the overlay manager
|
||||
/// </summary>
|
||||
public void AddOverlay(IOverlay overlay)
|
||||
{
|
||||
_context.OverlayManager.AddOverlay(overlay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add overlays to the texture using SkiaSharp
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture to modify</param>
|
||||
/// <param name="crop">The crop information containing flip flags</param>
|
||||
private void AddOverlaysToTexture(Image.Texture texture, ImageCrop crop)
|
||||
{
|
||||
try
|
||||
{
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
if (_lastUpdateTime != null)
|
||||
{
|
||||
// Calculate delta time for lifespan updates
|
||||
float deltaTime = (float)(currentTime - _lastUpdateTime.Value).TotalSeconds;
|
||||
_context.OverlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height));
|
||||
}
|
||||
|
||||
// Update overlay animations
|
||||
_lastUpdateTime = currentTime;
|
||||
|
||||
// Get texture data from host texture
|
||||
using var pinnedData = texture.HostTexture.GetData();
|
||||
var data = pinnedData.Get().ToArray();
|
||||
if (data == null || data.Length == 0)
|
||||
return;
|
||||
|
||||
int width = texture.Info.Width;
|
||||
int height = texture.Info.Height;
|
||||
int bytesPerPixel = texture.Info.FormatInfo.BytesPerPixel;
|
||||
|
||||
// Determine the SKColorType based on bytes per pixel
|
||||
SKColorType colorType = bytesPerPixel switch
|
||||
{
|
||||
4 => SKColorType.Rgba8888,
|
||||
3 => SKColorType.Rgb888x,
|
||||
2 => SKColorType.Rgb565,
|
||||
_ => SKColorType.Rgba8888
|
||||
};
|
||||
|
||||
// Create SKBitmap from texture data
|
||||
var imageInfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
|
||||
using var bitmap = new SKBitmap(imageInfo);
|
||||
|
||||
// Copy texture data to bitmap
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
bitmap.SetPixels((IntPtr)dataPtr);
|
||||
}
|
||||
}
|
||||
|
||||
// Create canvas for drawing overlays
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
|
||||
// Flip Y-axis if the game/texture requires it
|
||||
// Some games have textures that are already flipped, while others need flipping
|
||||
if (crop.FlipY)
|
||||
{
|
||||
canvas.Scale(1, -1);
|
||||
canvas.Translate(0, -height);
|
||||
}
|
||||
|
||||
// Render all overlays
|
||||
_context.OverlayManager.Render(canvas);
|
||||
|
||||
// Copy modified bitmap data back to texture data array
|
||||
var pixels = bitmap.Bytes;
|
||||
if (pixels.Length <= data.Length)
|
||||
{
|
||||
Array.Copy(pixels, data, pixels.Length);
|
||||
}
|
||||
|
||||
// Upload modified data back to texture
|
||||
var memoryOwner = MemoryOwner<byte>.Rent(data.Length);
|
||||
data.CopyTo(memoryOwner.Span);
|
||||
texture.HostTexture.SetData(memoryOwner); // SetData will dispose the MemoryOwner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail if overlay rendering doesn't work
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Overlay rendering failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that a frame on the queue is ready to be acquired.
|
||||
/// </summary>
|
||||
|
@ -377,7 +377,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
|
||||
|
||||
bool cursorVisible = false;
|
||||
|
||||
if (state.CursorBegin != state.CursorEnd)
|
||||
if (state.CursorBegin != state.CursorEnd && state.CursorEnd <= state.InputText.Length)
|
||||
{
|
||||
Debug.Assert(state.InputText.Length > 0);
|
||||
|
||||
|
@ -21,6 +21,21 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(3)] // 20.0.0+
|
||||
// CreateLibraryAppletEx(u32, u32, u64) -> object<nn::am::service::ILibraryAppletAccessor>
|
||||
public ResultCode CreateLibraryAppletEx(ServiceCtx context)
|
||||
{
|
||||
AppletId appletId = (AppletId)context.RequestData.ReadInt32();
|
||||
|
||||
_ = context.RequestData.ReadInt32(); // libraryAppletMode
|
||||
|
||||
_ = context.RequestData.ReadUInt64(); // threadId
|
||||
|
||||
MakeObject(context, new ILibraryAppletAccessor(appletId, context.Device.System));
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(10)]
|
||||
// CreateStorage(u64) -> object<nn::am::service::IStorage>
|
||||
public ResultCode CreateStorage(ServiceCtx context)
|
||||
|
@ -885,7 +885,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
|
||||
// F_SETFL
|
||||
else if (cmd == 0x4)
|
||||
{
|
||||
socket.Blocking = (arg & 0x800) != 0;
|
||||
socket.Blocking = (arg & 0x800) == 0;
|
||||
result = 0;
|
||||
}
|
||||
else
|
||||
|
@ -3,6 +3,7 @@ using Ryujinx.Audio.Integration;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
@ -65,6 +66,12 @@ namespace Ryujinx.HLE
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal IHostUIHandler HostUIHandler { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The overlay manager to use for all overlay operations.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal IOverlayManager OverlayManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Control the memory configuration used by the emulation context.
|
||||
/// </summary>
|
||||
@ -262,7 +269,8 @@ namespace Ryujinx.HLE
|
||||
UserChannelPersistence userChannelPersistence,
|
||||
IRenderer gpuRenderer,
|
||||
IHardwareDeviceDriver audioDeviceDriver,
|
||||
IHostUIHandler hostUIHandler
|
||||
IHostUIHandler hostUIHandler,
|
||||
IOverlayManager overlayManager
|
||||
)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
@ -273,6 +281,7 @@ namespace Ryujinx.HLE
|
||||
GpuRenderer = gpuRenderer;
|
||||
AudioDeviceDriver = audioDeviceDriver;
|
||||
HostUIHandler = hostUIHandler;
|
||||
OverlayManager = overlayManager;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ namespace Ryujinx.HLE
|
||||
DirtyHacks = new DirtyHacks(Configuration.Hacks);
|
||||
AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver);
|
||||
Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags);
|
||||
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks);
|
||||
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks, Configuration.OverlayManager);
|
||||
System = new HOS.Horizon(this);
|
||||
Statistics = new PerformanceStatistics(this);
|
||||
Hid = new Hid(this, System.HidStorage);
|
||||
|
@ -15,5 +15,14 @@ namespace Ryujinx.Ava.Common
|
||||
CustomVSyncIntervalIncrement,
|
||||
CustomVSyncIntervalDecrement,
|
||||
TurboMode,
|
||||
CycleInputDevicePlayer1,
|
||||
CycleInputDevicePlayer2,
|
||||
CycleInputDevicePlayer3,
|
||||
CycleInputDevicePlayer4,
|
||||
CycleInputDevicePlayer5,
|
||||
CycleInputDevicePlayer6,
|
||||
CycleInputDevicePlayer7,
|
||||
CycleInputDevicePlayer8,
|
||||
CycleInputDeviceHandheld,
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions;
|
||||
using Projektanker.Icons.Avalonia;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@ -18,11 +19,19 @@ namespace Ryujinx.Ava.Common.Markup
|
||||
|
||||
internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension<string>
|
||||
{
|
||||
public IValueConverter Converter { get; set; }
|
||||
|
||||
public override string Name => "Translation";
|
||||
protected override string Value => LocaleManager.Instance[key];
|
||||
|
||||
protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension)
|
||||
=> bindingExtension.Source = LocaleManager.Instance;
|
||||
{
|
||||
bindingExtension.Source = LocaleManager.Instance;
|
||||
if (Converter != null)
|
||||
{
|
||||
bindingExtension.Converter = Converter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class WindowTitleExtension(LocaleKeys key, bool includeVersion) : BasicMarkupExtension<string>
|
||||
|
@ -1,9 +0,0 @@
|
||||
namespace Ryujinx.Ava.Common.Models.Github
|
||||
{
|
||||
public class GithubReleaseAssetJsonResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string State { get; set; }
|
||||
public string BrowserDownloadUrl { get; set; }
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.Common.Models.Github
|
||||
{
|
||||
public class GithubReleasesJsonResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string TagName { get; set; }
|
||||
public List<GithubReleaseAssetJsonResponse> Assets { get; set; }
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Common.Models.Github
|
||||
{
|
||||
[JsonSerializable(typeof(GithubReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||
public partial class GithubReleasesJsonSerializerContext : JsonSerializerContext;
|
||||
}
|
@ -17,6 +17,7 @@ using Ryujinx.Graphics.OpenGL;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.UI.Overlay;
|
||||
using Silk.NET.Vulkan;
|
||||
using System;
|
||||
using System.IO;
|
||||
@ -348,7 +349,8 @@ namespace Ryujinx.Headless
|
||||
_userChannelPersistence,
|
||||
renderer.TryMakeThreaded(options.BackendThreading),
|
||||
new SDL2HardwareDeviceDriver(),
|
||||
window
|
||||
window,
|
||||
new OverlayManager()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -65,6 +65,8 @@
|
||||
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" />
|
||||
<PackageReference Include="Ryujinx.UpdateClient" />
|
||||
<PackageReference Include="Ryujinx.Systems.Update.Common" />
|
||||
<PackageReference Include="securifybv.ShellLink" />
|
||||
<PackageReference Include="Sep" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
|
@ -33,6 +33,7 @@ using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.GAL.Multithreading;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
using Ryujinx.UI.Overlay;
|
||||
using Ryujinx.Graphics.OpenGL;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
@ -40,6 +41,11 @@ using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System.Linq;
|
||||
using SkiaSharp;
|
||||
using SPB.Graphics.Vulkan;
|
||||
using System;
|
||||
@ -75,6 +81,7 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
private readonly long _ticksPerFrame;
|
||||
private readonly Stopwatch _chrono;
|
||||
private readonly Stopwatch _playTimer;
|
||||
private long _ticks;
|
||||
|
||||
private readonly AccountManager _accountManager;
|
||||
@ -123,6 +130,7 @@ namespace Ryujinx.Ava.Systems
|
||||
private readonly bool _isFirmwareTitle;
|
||||
|
||||
private readonly Lock _lockObject = new();
|
||||
private ControllerOverlay _controllerOverlay;
|
||||
|
||||
public event EventHandler AppExit;
|
||||
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
||||
@ -175,6 +183,7 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
_chrono = new Stopwatch();
|
||||
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
|
||||
_playTimer = new Stopwatch();
|
||||
|
||||
if (ApplicationPath.StartsWith("@SystemContent"))
|
||||
{
|
||||
@ -565,6 +574,7 @@ namespace Ryujinx.Ava.Systems
|
||||
public void Stop()
|
||||
{
|
||||
_isActive = false;
|
||||
_playTimer.Stop();
|
||||
}
|
||||
|
||||
private void Exit()
|
||||
@ -616,7 +626,7 @@ namespace Ryujinx.Ava.Systems
|
||||
private void Dispose()
|
||||
{
|
||||
if (Device.Processes != null)
|
||||
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
|
||||
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed);
|
||||
|
||||
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
|
||||
@ -635,6 +645,7 @@ namespace Ryujinx.Ava.Systems
|
||||
_gpuCancellationTokenSource.Dispose();
|
||||
|
||||
_chrono.Stop();
|
||||
_playTimer.Stop();
|
||||
}
|
||||
|
||||
public void DisposeGpu()
|
||||
@ -868,6 +879,7 @@ namespace Ryujinx.Ava.Systems
|
||||
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
|
||||
appMetadata => appMetadata.UpdatePreGame()
|
||||
);
|
||||
_playTimer.Start();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -877,6 +889,7 @@ namespace Ryujinx.Ava.Systems
|
||||
Device?.System.TogglePauseEmulation(false);
|
||||
|
||||
_viewModel.IsPaused = false;
|
||||
_playTimer.Start();
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
|
||||
}
|
||||
@ -886,6 +899,7 @@ namespace Ryujinx.Ava.Systems
|
||||
Device?.System.TogglePauseEmulation(true);
|
||||
|
||||
_viewModel.IsPaused = true;
|
||||
_playTimer.Stop();
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
|
||||
}
|
||||
@ -917,9 +931,13 @@ namespace Ryujinx.Ava.Systems
|
||||
_userChannelPersistence,
|
||||
renderer.TryMakeThreaded(ConfigurationState.Instance.Graphics.BackendThreading),
|
||||
InitializeAudio(),
|
||||
_viewModel.UiHandler
|
||||
_viewModel.UiHandler,
|
||||
new OverlayManager()
|
||||
)
|
||||
);
|
||||
|
||||
_controllerOverlay = new ControllerOverlay();
|
||||
Device.Gpu.Window.AddOverlay(_controllerOverlay);
|
||||
}
|
||||
|
||||
private static IHardwareDeviceDriver InitializeAudio()
|
||||
@ -1152,6 +1170,24 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
_dialogShown = true;
|
||||
|
||||
// The hard-coded hotkey mapped to exit is Escape, but it's also the same key
|
||||
// that causes the dialog we launch to close (without doing anything). In release
|
||||
// mode, a race is observed that between ShowExitPrompt() appearing on KeyDown
|
||||
// and the ContentDialog we create seeing the key state before KeyUp. Merely waiting
|
||||
// for the key to no longer be pressed appears to be insufficient.
|
||||
// NB: Using _keyboardInterface.IsPressed(Key.Escape) does not currently work.
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
while (GetAsyncKeyState(0x1B) != 0)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(250);
|
||||
}
|
||||
|
||||
shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
|
||||
|
||||
_dialogShown = false;
|
||||
@ -1308,6 +1344,33 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
_viewModel.Volume = Device.GetVolume();
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer1:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player1);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer2:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player2);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer3:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player3);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer4:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player4);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer5:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player5);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer6:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player6);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer7:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player7);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer8:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player8);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDeviceHandheld:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Handheld);
|
||||
break;
|
||||
case KeyboardHotkeyState.None:
|
||||
(_keyboardInterface as AvaloniaKeyboard).Clear();
|
||||
break;
|
||||
@ -1341,6 +1404,118 @@ namespace Ryujinx.Ava.Systems
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex playerIndex)
|
||||
{
|
||||
// Get current input configuration
|
||||
List<InputConfig> currentConfig = new(ConfigurationState.Instance.Hid.InputConfig.Value);
|
||||
|
||||
// Find current configuration for this player
|
||||
InputConfig playerConfig = currentConfig.FirstOrDefault(x => x.PlayerIndex == (PlayerIndex)playerIndex);
|
||||
|
||||
// Get available devices from InputManager
|
||||
List<(DeviceType Type, string Id, string Name)> availableDevices = [];
|
||||
|
||||
// Add disabled option
|
||||
availableDevices.Add((DeviceType.None, "disabled", "Disabled"));
|
||||
|
||||
// Add keyboard devices
|
||||
foreach (string id in _inputManager.KeyboardDriver.GamepadsIds)
|
||||
{
|
||||
using var gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
|
||||
if (gamepad != null)
|
||||
{
|
||||
availableDevices.Add((DeviceType.Keyboard, id, gamepad.Name));
|
||||
}
|
||||
}
|
||||
|
||||
// Add controller devices
|
||||
int controllerNumber = 0;
|
||||
foreach (string id in _inputManager.GamepadDriver.GamepadsIds)
|
||||
{
|
||||
using var gamepad = _inputManager.GamepadDriver.GetGamepad(id);
|
||||
if (gamepad != null)
|
||||
{
|
||||
string name = $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber++})";
|
||||
availableDevices.Add((DeviceType.Controller, id, name));
|
||||
}
|
||||
}
|
||||
|
||||
// Find current device index
|
||||
int currentIndex = 0;
|
||||
if (playerConfig != null)
|
||||
{
|
||||
DeviceType currentType = DeviceType.None;
|
||||
if (playerConfig is StandardKeyboardInputConfig)
|
||||
currentType = DeviceType.Keyboard;
|
||||
else if (playerConfig is StandardControllerInputConfig)
|
||||
currentType = DeviceType.Controller;
|
||||
|
||||
currentIndex = availableDevices.FindIndex(x => x.Type == currentType && x.Id == playerConfig.Id);
|
||||
if (currentIndex == -1) currentIndex = 0;
|
||||
}
|
||||
|
||||
// Cycle to next device
|
||||
int nextIndex = (currentIndex + 1) % availableDevices.Count;
|
||||
var nextDevice = availableDevices[nextIndex];
|
||||
|
||||
// Remove existing configuration for this player
|
||||
currentConfig.RemoveAll(x => x.PlayerIndex == (PlayerIndex)playerIndex);
|
||||
|
||||
// Add new configuration if not disabled
|
||||
if (nextDevice.Type != DeviceType.None)
|
||||
{
|
||||
InputConfig newConfig = CreateDefaultInputConfig(nextDevice, (PlayerIndex)playerIndex);
|
||||
if (newConfig != null)
|
||||
{
|
||||
currentConfig.Add(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the new configuration
|
||||
ConfigurationState.Instance.Hid.InputConfig.Value = currentConfig;
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
|
||||
// Reload the input system
|
||||
NpadManager.ReloadConfiguration(currentConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
||||
|
||||
// Show controller overlay
|
||||
ShowControllerOverlay(currentConfig, ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value);
|
||||
}
|
||||
|
||||
private InputConfig CreateDefaultInputConfig((DeviceType Type, string Id, string Name) device, PlayerIndex playerIndex)
|
||||
{
|
||||
if (device.Type == DeviceType.Keyboard)
|
||||
{
|
||||
return DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(device.Id, device.Name, playerIndex);
|
||||
}
|
||||
else if (device.Type == DeviceType.Controller)
|
||||
{
|
||||
bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(device.Name);
|
||||
return DefaultInputConfigurationProvider.CreateDefaultControllerConfig(device.Id, device.Name, playerIndex, isNintendoStyle);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void ShowControllerOverlay(List<InputConfig> inputConfigs, int duration)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_controllerOverlay != null)
|
||||
{
|
||||
_controllerOverlay.ShowControllerBindings(inputConfigs, duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - ControllerOverlay is null");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private KeyboardHotkeyState GetHotkeyState()
|
||||
{
|
||||
KeyboardHotkeyState state = KeyboardHotkeyState.None;
|
||||
@ -1393,6 +1568,42 @@ namespace Ryujinx.Ava.Systems
|
||||
{
|
||||
state = KeyboardHotkeyState.TurboMode;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer1))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer1;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer2))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer2;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer3))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer3;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer4))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer4;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer5))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer5;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer6))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer6;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer7))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer7;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer8))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer8;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDeviceHandheld))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDeviceHandheld;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
@ -33,19 +33,11 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
|
||||
/// </summary>
|
||||
public void UpdatePostGame()
|
||||
/// <param name="playTime">The active gameplay time this past session.</param>
|
||||
public void UpdatePostGame(TimeSpan playTime)
|
||||
{
|
||||
DateTime? prevLastPlayed = LastPlayed;
|
||||
UpdatePreGame();
|
||||
|
||||
if (!prevLastPlayed.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
|
||||
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
|
||||
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
|
||||
TimePlayed += playTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 70;
|
||||
public const int CurrentVersion = 72;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
@ -217,6 +217,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public HideCursorMode HideCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when game starts (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public int ControllerOverlayGameStartDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when input is cycled (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public int ControllerOverlayInputCycleDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Vertical Sync
|
||||
/// </summary>
|
||||
|
@ -53,6 +53,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
ShowOldUI.Value = shouldLoadFromFile ? cff.ShowTitleBar : ShowOldUI.Value; // Get from global config only
|
||||
EnableHardwareAcceleration.Value = shouldLoadFromFile ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only
|
||||
HideCursor.Value = cff.HideCursor;
|
||||
ControllerOverlayGameStartDuration.Value = cff.ControllerOverlayGameStartDuration;
|
||||
ControllerOverlayInputCycleDuration.Value = cff.ControllerOverlayInputCycleDuration;
|
||||
|
||||
Logger.EnableFileLog.Value = cff.EnableFileLog;
|
||||
Logger.EnableDebug.Value = cff.LoggingEnableDebug;
|
||||
@ -479,7 +481,40 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
};
|
||||
}
|
||||
),
|
||||
(69, static cff => cff.SkipUserProfiles = false)
|
||||
(69, static cff => cff.SkipUserProfiles = false),
|
||||
(71, static cff =>
|
||||
{
|
||||
cff.ControllerOverlayGameStartDuration = 3;
|
||||
cff.ControllerOverlayInputCycleDuration = 2;
|
||||
}),
|
||||
(72, static cff =>
|
||||
{
|
||||
cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = cff.Hotkeys.Pause,
|
||||
ToggleMute = cff.Hotkeys.ToggleMute,
|
||||
ResScaleUp = cff.Hotkeys.ResScaleUp,
|
||||
ResScaleDown = cff.Hotkeys.ResScaleDown,
|
||||
VolumeUp = cff.Hotkeys.VolumeUp,
|
||||
VolumeDown = cff.Hotkeys.VolumeDown,
|
||||
CustomVSyncIntervalIncrement = cff.Hotkeys.CustomVSyncIntervalIncrement,
|
||||
CustomVSyncIntervalDecrement = cff.Hotkeys.CustomVSyncIntervalDecrement,
|
||||
TurboMode = cff.Hotkeys.TurboMode,
|
||||
TurboModeWhileHeld = cff.Hotkeys.TurboModeWhileHeld,
|
||||
CycleInputDevicePlayer1 = Key.Unbound,
|
||||
CycleInputDevicePlayer2 = Key.Unbound,
|
||||
CycleInputDevicePlayer3 = Key.Unbound,
|
||||
CycleInputDevicePlayer4 = Key.Unbound,
|
||||
CycleInputDevicePlayer5 = Key.Unbound,
|
||||
CycleInputDevicePlayer6 = Key.Unbound,
|
||||
CycleInputDevicePlayer7 = Key.Unbound,
|
||||
CycleInputDevicePlayer8 = Key.Unbound,
|
||||
CycleInputDeviceHandheld = Key.Unbound
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -846,6 +846,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when game starts (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ControllerOverlayGameStartDuration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when input is cycled (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ControllerOverlayInputCycleDuration { get; private set; }
|
||||
|
||||
private ConfigurationState()
|
||||
{
|
||||
UI = new UISection();
|
||||
@ -863,6 +873,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
RememberWindowState = new ReactiveObject<bool>();
|
||||
ShowOldUI = new ReactiveObject<bool>();
|
||||
EnableHardwareAcceleration = new ReactiveObject<bool>();
|
||||
ControllerOverlayGameStartDuration = new ReactiveObject<int>();
|
||||
ControllerOverlayInputCycleDuration = new ReactiveObject<int>();
|
||||
}
|
||||
|
||||
public HleConfiguration CreateHleConfiguration() =>
|
||||
|
@ -65,6 +65,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
ShowTitleBar = ShowOldUI,
|
||||
EnableHardwareAcceleration = EnableHardwareAcceleration,
|
||||
HideCursor = HideCursor,
|
||||
ControllerOverlayGameStartDuration = ControllerOverlayGameStartDuration,
|
||||
ControllerOverlayInputCycleDuration = ControllerOverlayInputCycleDuration,
|
||||
VSyncMode = Graphics.VSyncMode,
|
||||
EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval,
|
||||
CustomVSyncInterval = Graphics.CustomVSyncInterval,
|
||||
@ -190,6 +192,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
ShowOldUI.Value = !OperatingSystem.IsWindows();
|
||||
EnableHardwareAcceleration.Value = true;
|
||||
HideCursor.Value = HideCursorMode.OnIdle;
|
||||
ControllerOverlayGameStartDuration.Value = 3;
|
||||
ControllerOverlayInputCycleDuration.Value = 2;
|
||||
Graphics.VSyncMode.Value = VSyncMode.Switch;
|
||||
Graphics.CustomVSyncInterval.Value = 120;
|
||||
Graphics.EnableCustomVSyncInterval.Value = false;
|
||||
@ -269,7 +273,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
CustomVSyncIntervalIncrement = Key.Unbound,
|
||||
CustomVSyncIntervalDecrement = Key.Unbound,
|
||||
TurboMode = Key.Unbound,
|
||||
TurboModeWhileHeld = false
|
||||
TurboModeWhileHeld = false,
|
||||
CycleInputDevicePlayer1 = Key.Unbound,
|
||||
CycleInputDevicePlayer2 = Key.Unbound,
|
||||
CycleInputDevicePlayer3 = Key.Unbound,
|
||||
CycleInputDevicePlayer4 = Key.Unbound,
|
||||
CycleInputDevicePlayer5 = Key.Unbound,
|
||||
CycleInputDevicePlayer6 = Key.Unbound,
|
||||
CycleInputDevicePlayer7 = Key.Unbound,
|
||||
CycleInputDevicePlayer8 = Key.Unbound,
|
||||
CycleInputDeviceHandheld = Key.Unbound
|
||||
};
|
||||
Hid.RainbowSpeed.Value = 1f;
|
||||
Hid.InputConfig.Value =
|
||||
|
@ -1,190 +0,0 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Common.Models.Github;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.Systems
|
||||
{
|
||||
internal static partial class Updater
|
||||
{
|
||||
private static GitHubReleaseChannels.Channel? _currentGitHubReleaseChannel;
|
||||
|
||||
private static async Task<Optional<(Version Current, Version Incoming)>> CheckGitHubVersionAsync(bool showVersionUpToDate = false)
|
||||
{
|
||||
if (!Version.TryParse(Program.Version, out Version currentVersion))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
|
||||
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||
|
||||
_running = false;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, "Checking for updates from GitHub.");
|
||||
|
||||
// Get latest version number from GitHub API
|
||||
try
|
||||
{
|
||||
using HttpClient jsonClient = ConstructHttpClient();
|
||||
|
||||
if (_currentGitHubReleaseChannel == null)
|
||||
{
|
||||
GitHubReleaseChannels releaseChannels = await GitHubReleaseChannels.GetAsync(jsonClient);
|
||||
|
||||
_currentGitHubReleaseChannel = ReleaseInformation.IsCanaryBuild
|
||||
? releaseChannels.Canary
|
||||
: releaseChannels.Stable;
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"Loaded GitHub release channel for '{(ReleaseInformation.IsCanaryBuild ? "canary" : "stable")}'");
|
||||
|
||||
_changelogUrlFormat = _currentGitHubReleaseChannel.Value.UrlFormat;
|
||||
}
|
||||
|
||||
string fetchedJson = await jsonClient.GetStringAsync(_currentGitHubReleaseChannel.Value.GetLatestReleaseApiUrl());
|
||||
GithubReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _ghSerializerContext.GithubReleasesJsonResponse);
|
||||
_buildVer = fetched.TagName;
|
||||
|
||||
foreach (GithubReleaseAssetJsonResponse asset in fetched.Assets)
|
||||
{
|
||||
if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt))
|
||||
{
|
||||
_buildUrl = asset.BrowserDownloadUrl;
|
||||
|
||||
if (asset.State != "uploaded")
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
|
||||
string.Empty);
|
||||
|
||||
if (userResult is UserResult.Ok)
|
||||
{
|
||||
OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion));
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, "Up to date.");
|
||||
|
||||
_running = false;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If build not done, assume no new update is available.
|
||||
if (_buildUrl is null)
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
|
||||
string.Empty);
|
||||
|
||||
if (userResult is UserResult.Ok)
|
||||
{
|
||||
OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion));
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, "Up to date.");
|
||||
|
||||
_running = false;
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, exception.Message);
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
|
||||
|
||||
_running = false;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!Version.TryParse(_buildVer, out Version newVersion))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {RyujinxApp.FullAppName} version from GitHub!");
|
||||
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||
|
||||
_running = false;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
return (currentVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct GitHubReleaseChannels
|
||||
{
|
||||
public static async Task<GitHubReleaseChannels> GetAsync(HttpClient httpClient)
|
||||
{
|
||||
ReleaseChannelPair releaseChannelPair = await httpClient.GetFromJsonAsync("https://ryujinx.app/api/release-channels", ReleaseChannelPairContext.Default.ReleaseChannelPair);
|
||||
return new GitHubReleaseChannels(releaseChannelPair);
|
||||
}
|
||||
|
||||
internal GitHubReleaseChannels(ReleaseChannelPair channelPair)
|
||||
{
|
||||
Stable = new Channel(channelPair.Stable);
|
||||
Canary = new Channel(channelPair.Canary);
|
||||
}
|
||||
|
||||
public readonly Channel Stable;
|
||||
public readonly Channel Canary;
|
||||
|
||||
public readonly struct Channel
|
||||
{
|
||||
public Channel(string raw)
|
||||
{
|
||||
string[] parts = raw.Split('/');
|
||||
Owner = parts[0];
|
||||
Repo = parts[1];
|
||||
}
|
||||
|
||||
public readonly string Owner;
|
||||
public readonly string Repo;
|
||||
|
||||
public string UrlFormat => $"https://github.com/{ToString()}/releases/{{0}}";
|
||||
|
||||
public override string ToString() => $"{Owner}/{Repo}";
|
||||
|
||||
public string GetLatestReleaseApiUrl() =>
|
||||
$"https://api.github.com/repos/{ToString()}/releases/latest";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(ReleaseChannelPair))]
|
||||
partial class ReleaseChannelPairContext : JsonSerializerContext;
|
||||
|
||||
class ReleaseChannelPair
|
||||
{
|
||||
[JsonPropertyName("stable")]
|
||||
public string Stable { get; set; }
|
||||
[JsonPropertyName("canary")]
|
||||
public string Canary { get; set; }
|
||||
}
|
||||
}
|
@ -4,44 +4,28 @@ using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Systems.Update.Client;
|
||||
using Ryujinx.Systems.Update.Common;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.Systems
|
||||
{
|
||||
internal static partial class Updater
|
||||
{
|
||||
private static string CreateUpdateQueryUrl()
|
||||
{
|
||||
#pragma warning disable CS8524
|
||||
var os = RunningPlatform.CurrentOS switch
|
||||
#pragma warning restore CS8524
|
||||
{
|
||||
OperatingSystemType.MacOS => "mac",
|
||||
OperatingSystemType.Linux => "linux",
|
||||
OperatingSystemType.Windows => "win"
|
||||
};
|
||||
private static VersionResponse _versionResponse;
|
||||
|
||||
var arch = RunningPlatform.Architecture switch
|
||||
{
|
||||
Architecture.Arm64 => "arm",
|
||||
Architecture.X64 => "amd64",
|
||||
_ => null
|
||||
};
|
||||
private static UpdateClient CreateUpdateClient()
|
||||
=> UpdateClient.Builder()
|
||||
.WithServerEndpoint("https://update.ryujinx.app") // This is the default, and doesn't need to be provided; it's here for transparency.
|
||||
.WithLogger((format, args, caller) =>
|
||||
Logger.Info?.Print(
|
||||
LogClass.Application,
|
||||
args.Length is 0 ? format : format.Format(args),
|
||||
caller: caller)
|
||||
);
|
||||
|
||||
if (arch is null)
|
||||
return null;
|
||||
|
||||
var rc = ReleaseInformation.IsCanaryBuild ? "canary" : "stable";
|
||||
|
||||
return $"https://update.ryujinx.app/latest/query?os={os}&arch={arch}&rc={rc}";
|
||||
}
|
||||
|
||||
private static async Task<Optional<(Version Current, Version Incoming)>> CheckGitLabVersionAsync(bool showVersionUpToDate = false)
|
||||
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
|
||||
{
|
||||
if (!Version.TryParse(Program.Version, out Version currentVersion))
|
||||
{
|
||||
@ -57,41 +41,31 @@ namespace Ryujinx.Ava.Systems
|
||||
return default;
|
||||
}
|
||||
|
||||
if (CreateUpdateQueryUrl() is not {} updateUrl)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "Could not determine URL for updates.");
|
||||
|
||||
_running = false;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"Checking for updates from {updateUrl}.");
|
||||
|
||||
// Get latest version number from GitLab API
|
||||
using HttpClient jsonClient = ConstructHttpClient();
|
||||
|
||||
// GitLab instance is located in Ukraine. Connection times will vary across the world.
|
||||
jsonClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
using UpdateClient updateClient = CreateUpdateClient();
|
||||
|
||||
try
|
||||
{
|
||||
UpdaterResponse response =
|
||||
await jsonClient.GetFromJsonAsync(updateUrl, UpdaterResponseJsonContext.Default.UpdaterResponse);
|
||||
|
||||
_buildVer = response.Tag;
|
||||
_buildUrl = response.DownloadUrl;
|
||||
_changelogUrlFormat = response.ReleaseUrlFormat;
|
||||
_versionResponse = await updateClient.QueryLatestAsync(ReleaseInformation.IsCanaryBuild
|
||||
? ReleaseChannel.Canary
|
||||
: ReleaseChannel.Stable);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"An error occurred when parsing JSON response from API ({e.GetType().AsFullNamePrettyString()}): {e.Message}",
|
||||
e);
|
||||
Logger.Error?.Print(LogClass.Application, $"An error occurred when requesting for updates ({e.GetType().AsFullNamePrettyString()}): {e.Message}");
|
||||
|
||||
_running = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (_versionResponse == null)
|
||||
{
|
||||
// logging is done via the UpdateClient library
|
||||
_running = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
// If build URL not found, assume no new update is available.
|
||||
if (_buildUrl is null or "")
|
||||
if (_versionResponse.ArtifactUrl is null or "")
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
@ -101,7 +75,7 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
if (userResult is UserResult.Ok)
|
||||
{
|
||||
OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion));
|
||||
OpenHelper.OpenUrl(_versionResponse.ReleaseUrlFormat.Format(currentVersion));
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,13 +87,13 @@ namespace Ryujinx.Ava.Systems
|
||||
}
|
||||
|
||||
|
||||
if (!Version.TryParse(_buildVer, out Version newVersion))
|
||||
if (!Version.TryParse(_versionResponse.Version, out Version newVersion))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
$"Failed to convert the received {RyujinxApp.FullAppName} version from GitLab!");
|
||||
$"Failed to convert the received {RyujinxApp.FullAppName} version from the update server!");
|
||||
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedServerMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||
|
||||
_running = false;
|
||||
@ -129,17 +103,5 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
return (currentVersion, newVersion);
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(UpdaterResponse))]
|
||||
partial class UpdaterResponseJsonContext : JsonSerializerContext;
|
||||
|
||||
public class UpdaterResponse
|
||||
{
|
||||
[JsonPropertyName("tag")] public string Tag { get; set; }
|
||||
[JsonPropertyName("download_url")] public string DownloadUrl { get; set; }
|
||||
[JsonPropertyName("web_url")] public string ReleaseUrl { get; set; }
|
||||
|
||||
[JsonIgnore] public string ReleaseUrlFormat => ReleaseUrl.Replace(Tag, "{0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,13 +5,11 @@ using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Common.Models.Github;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@ -31,43 +29,17 @@ namespace Ryujinx.Ava.Systems
|
||||
{
|
||||
internal static partial class Updater
|
||||
{
|
||||
private static readonly GithubReleasesJsonSerializerContext _ghSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
private static readonly string _platformExt = BuildPlatformExtension();
|
||||
private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||
private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
|
||||
private const int ConnectionCount = 4;
|
||||
|
||||
private static string _buildVer;
|
||||
private static string _buildUrl;
|
||||
|
||||
private static long _buildSize;
|
||||
private static bool _updateSuccessful;
|
||||
private static bool _running;
|
||||
|
||||
private static readonly string[] _windowsDependencyDirs = [];
|
||||
|
||||
private static string _changelogUrlFormat = null;
|
||||
|
||||
public static async Task<Optional<(Version, Version)>> CheckVersionAsync(bool showVersionUpToDate = false)
|
||||
{
|
||||
Optional<(Version, Version)> versionTuple;
|
||||
|
||||
try
|
||||
{
|
||||
versionTuple = await CheckGitLabVersionAsync(showVersionUpToDate);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Update checking from GitLab failed; falling back to GitHub.");
|
||||
Logger.Error?.PrintMsg(LogClass.Application, e.Message);
|
||||
versionTuple = await CheckGitHubVersionAsync(showVersionUpToDate);
|
||||
}
|
||||
|
||||
return versionTuple;
|
||||
}
|
||||
|
||||
|
||||
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
|
||||
{
|
||||
if (_running)
|
||||
@ -94,7 +66,7 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
if (userResult is UserResult.Ok)
|
||||
{
|
||||
OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion));
|
||||
OpenHelper.OpenUrl(_versionResponse.ReleaseUrlFormat.Format(currentVersion));
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +86,7 @@ namespace Ryujinx.Ava.Systems
|
||||
// GitLab instance is located in Ukraine. Connection times will vary across the world.
|
||||
buildSizeClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
|
||||
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
_buildSize = message.Content.Headers.ContentRange.Length.Value;
|
||||
}
|
||||
@ -144,7 +116,7 @@ namespace Ryujinx.Ava.Systems
|
||||
switch (shouldUpdate)
|
||||
{
|
||||
case UserResult.Yes:
|
||||
await UpdateRyujinx(_buildUrl);
|
||||
await UpdateRyujinx(_versionResponse.ArtifactUrl);
|
||||
break;
|
||||
// Secondary button maps to no, which in this case is the show changelog button.
|
||||
case UserResult.No:
|
||||
|
@ -2,29 +2,104 @@ using Avalonia.Media;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.UI;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
class AvaloniaHostUITheme(MainWindow parent) : IHostUITheme
|
||||
class AvaloniaHostUITheme : IHostUITheme
|
||||
{
|
||||
public string FontFamily { get; } = OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) ? "Segoe UI Variable" : parent.FontFamily.Name;
|
||||
private readonly MainWindow _parent;
|
||||
|
||||
public ThemeColor DefaultBackgroundColor { get; } = BrushToThemeColor(parent.Background);
|
||||
public ThemeColor DefaultForegroundColor { get; } = BrushToThemeColor(parent.Foreground);
|
||||
public ThemeColor DefaultBorderColor { get; } = BrushToThemeColor(parent.BorderBrush);
|
||||
public ThemeColor SelectionBackgroundColor { get; } = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
|
||||
public ThemeColor SelectionForegroundColor { get; } = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
|
||||
public string FontFamily { get; }
|
||||
public ThemeColor DefaultBackgroundColor { get; }
|
||||
public ThemeColor DefaultForegroundColor { get; }
|
||||
public ThemeColor DefaultBorderColor { get; }
|
||||
public ThemeColor SelectionBackgroundColor { get; }
|
||||
public ThemeColor SelectionForegroundColor { get; }
|
||||
|
||||
public AvaloniaHostUITheme(MainWindow parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
// Initialize font property
|
||||
FontFamily = GetSystemFontFamily();
|
||||
|
||||
// Initialize all properties that depend on parent
|
||||
DefaultBackgroundColor = BrushToThemeColor(parent.Background);
|
||||
DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
|
||||
DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
|
||||
SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
|
||||
SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
|
||||
}
|
||||
|
||||
private string GetSystemFontFamily()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return GetWindowsFontByLanguage();
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return GetMacOSFontByLanguage();
|
||||
}
|
||||
else // Linux and other platforms
|
||||
{
|
||||
return GetLinuxFontByLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetWindowsFontByLanguage()
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
string langCode = culture.Name;
|
||||
|
||||
return culture.TwoLetterISOLanguageName switch
|
||||
{
|
||||
"zh" => langCode == "zh-CN" || langCode == "zh-Hans" || langCode == "zh-SG"
|
||||
? "Microsoft YaHei UI" // Simplified Chinese
|
||||
: "Microsoft JhengHei UI", // Traditional Chinese
|
||||
|
||||
"ja" => "Yu Gothic UI", // Japanese
|
||||
"ko" => "Malgun Gothic", // Korean
|
||||
_ => OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)
|
||||
? "Segoe UI Variable" // Other languages - Windows 11+
|
||||
: _parent.FontFamily.Name // Fallback to parent window font
|
||||
};
|
||||
}
|
||||
|
||||
private string GetMacOSFontByLanguage()
|
||||
{
|
||||
return CultureInfo.CurrentUICulture.TwoLetterISOLanguageName switch
|
||||
{
|
||||
"zh" => "PingFang SC", // Chinese (both simplified and traditional)
|
||||
"ja" => "Hiragino Sans", // Japanese
|
||||
"ko" => "Apple SD Gothic Neo", // Korean
|
||||
_ => _parent.FontFamily.Name // Fallback to parent window font
|
||||
};
|
||||
}
|
||||
|
||||
private string GetLinuxFontByLanguage()
|
||||
{
|
||||
return CultureInfo.CurrentUICulture.TwoLetterISOLanguageName switch
|
||||
{
|
||||
"zh" => "Noto Sans CJK SC", // Chinese
|
||||
"ja" => "Noto Sans CJK JP", // Japanese
|
||||
"ko" => "Noto Sans CJK KR", // Korean
|
||||
_ => _parent.FontFamily.Name // Fallback to parent window font
|
||||
};
|
||||
}
|
||||
|
||||
private static ThemeColor BrushToThemeColor(IBrush brush)
|
||||
{
|
||||
if (brush is SolidColorBrush solidColor)
|
||||
{
|
||||
return new ThemeColor((float)solidColor.Color.A / 255,
|
||||
return new ThemeColor(
|
||||
(float)solidColor.Color.A / 255,
|
||||
(float)solidColor.Color.R / 255,
|
||||
(float)solidColor.Color.G / 255,
|
||||
(float)solidColor.Color.B / 255);
|
||||
(float)solidColor.Color.B / 255
|
||||
);
|
||||
}
|
||||
|
||||
return new ThemeColor();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class PlayerHotkeyLabelConverter : IValueConverter
|
||||
{
|
||||
public static readonly PlayerHotkeyLabelConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string playerName && !string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
string baseText = LocaleManager.Instance[LocaleKeys.SettingsTabHotkeysCycleInputDevicePlayerX];
|
||||
return string.Format(baseText, playerName);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -110,5 +110,8 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
public static partial ushort GetAsyncKeyState(int nVirtKey);
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,24 @@ namespace Ryujinx.Ava.UI.Models.Input
|
||||
|
||||
[ObservableProperty] private bool _turboModeWhileHeld;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer1;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer2;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer3;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer4;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer5;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer6;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer7;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer8;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDeviceHandheld;
|
||||
|
||||
public HotkeyConfig(KeyboardHotkeys config)
|
||||
{
|
||||
if (config == null)
|
||||
@ -50,6 +68,15 @@ namespace Ryujinx.Ava.UI.Models.Input
|
||||
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
|
||||
TurboMode = config.TurboMode;
|
||||
TurboModeWhileHeld = config.TurboModeWhileHeld;
|
||||
CycleInputDevicePlayer1 = config.CycleInputDevicePlayer1;
|
||||
CycleInputDevicePlayer2 = config.CycleInputDevicePlayer2;
|
||||
CycleInputDevicePlayer3 = config.CycleInputDevicePlayer3;
|
||||
CycleInputDevicePlayer4 = config.CycleInputDevicePlayer4;
|
||||
CycleInputDevicePlayer5 = config.CycleInputDevicePlayer5;
|
||||
CycleInputDevicePlayer6 = config.CycleInputDevicePlayer6;
|
||||
CycleInputDevicePlayer7 = config.CycleInputDevicePlayer7;
|
||||
CycleInputDevicePlayer8 = config.CycleInputDevicePlayer8;
|
||||
CycleInputDeviceHandheld = config.CycleInputDeviceHandheld;
|
||||
}
|
||||
|
||||
public KeyboardHotkeys GetConfig() =>
|
||||
@ -67,7 +94,16 @@ namespace Ryujinx.Ava.UI.Models.Input
|
||||
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
|
||||
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
|
||||
TurboMode = TurboMode,
|
||||
TurboModeWhileHeld = TurboModeWhileHeld
|
||||
TurboModeWhileHeld = TurboModeWhileHeld,
|
||||
CycleInputDevicePlayer1 = CycleInputDevicePlayer1,
|
||||
CycleInputDevicePlayer2 = CycleInputDevicePlayer2,
|
||||
CycleInputDevicePlayer3 = CycleInputDevicePlayer3,
|
||||
CycleInputDevicePlayer4 = CycleInputDevicePlayer4,
|
||||
CycleInputDevicePlayer5 = CycleInputDevicePlayer5,
|
||||
CycleInputDevicePlayer6 = CycleInputDevicePlayer6,
|
||||
CycleInputDevicePlayer7 = CycleInputDevicePlayer7,
|
||||
CycleInputDevicePlayer8 = CycleInputDevicePlayer8,
|
||||
CycleInputDeviceHandheld = CycleInputDeviceHandheld
|
||||
};
|
||||
}
|
||||
}
|
||||
|
292
src/Ryujinx/UI/Overlay/ControllerOverlay.cs
Normal file
292
src/Ryujinx/UI/Overlay/ControllerOverlay.cs
Normal file
@ -0,0 +1,292 @@
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OriginalInputConfig = Ryujinx.Common.Configuration.Hid.InputConfig;
|
||||
using OriginalPlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex;
|
||||
using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendType;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller overlay that shows controller bindings matching the original AXAML design
|
||||
/// </summary>
|
||||
public class ControllerOverlay : Overlay
|
||||
{
|
||||
private const float OverlayWidth = 400;
|
||||
private const float Padding = 24;
|
||||
private const float PlayerSpacing = 12;
|
||||
private const float PlayerRowHeight = 32;
|
||||
|
||||
private const float TitleTextSize = 25;
|
||||
private const float PlayerTextSize = 22;
|
||||
|
||||
private float _lifespan = 0f;
|
||||
|
||||
public ControllerOverlay() : base("ControllerOverlay")
|
||||
{
|
||||
CreateBaseElements();
|
||||
}
|
||||
|
||||
private void CreateBaseElements()
|
||||
{
|
||||
// Main background container
|
||||
var background = new RectangleElement(0, 0, OverlayWidth, 200, // Dynamic height will be set later
|
||||
new SKColor(0, 0, 0, 224)) // #E0000000
|
||||
{
|
||||
Name = "Background",
|
||||
CornerRadius = 12,
|
||||
BorderColor = new SKColor(255, 255, 255, 64), // #40FFFFFF
|
||||
BorderWidth = 1
|
||||
};
|
||||
AddElement(background);
|
||||
|
||||
// Title text (will be updated with localized text)
|
||||
var titleText = new TextElement(Padding + 30, Padding, LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle], TitleTextSize, SKColors.White)
|
||||
{
|
||||
Name = "TitleText",
|
||||
FontStyle = SKFontStyle.Bold
|
||||
};
|
||||
AddElement(titleText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show controller bindings with localized strings
|
||||
/// </summary>
|
||||
public void ShowControllerBindings(List<OriginalInputConfig> inputConfigs, int durationSeconds)
|
||||
{
|
||||
// Update title text
|
||||
var titleElement = FindElement<TextElement>("TitleText");
|
||||
if (titleElement != null)
|
||||
{
|
||||
titleElement.Text = LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle];
|
||||
}
|
||||
|
||||
// Reset lifespan and opacity
|
||||
_lifespan = durationSeconds;
|
||||
|
||||
// Clear existing player bindings
|
||||
ClearPlayerBindings();
|
||||
|
||||
// Group controllers by player index (support all players + handheld)
|
||||
var playerBindings = new Dictionary<OriginalPlayerIndex, List<OriginalInputConfig>>();
|
||||
|
||||
foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Handheld))
|
||||
{
|
||||
if (!playerBindings.ContainsKey(config.PlayerIndex))
|
||||
{
|
||||
playerBindings[config.PlayerIndex] = new List<OriginalInputConfig>();
|
||||
}
|
||||
playerBindings[config.PlayerIndex].Add(config);
|
||||
}
|
||||
|
||||
float currentY = Padding + 40; // After title
|
||||
|
||||
// Add player bindings to UI (support 8 players + handheld)
|
||||
var playerIndices = new[]
|
||||
{
|
||||
OriginalPlayerIndex.Player1, OriginalPlayerIndex.Player2, OriginalPlayerIndex.Player3, OriginalPlayerIndex.Player4,
|
||||
OriginalPlayerIndex.Player5, OriginalPlayerIndex.Player6, OriginalPlayerIndex.Player7, OriginalPlayerIndex.Player8,
|
||||
OriginalPlayerIndex.Handheld
|
||||
};
|
||||
|
||||
for (int i = 0; i < playerIndices.Length; i++)
|
||||
{
|
||||
var playerIndex = playerIndices[i];
|
||||
float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing));
|
||||
|
||||
// Player number with colored background (circular badge)
|
||||
var playerColor = GetPlayerColor(i);
|
||||
var playerBadge = new RectangleElement(Padding, rowY, 24, 20, playerColor)
|
||||
{
|
||||
Name = $"PlayerBadge_{i}",
|
||||
CornerRadius = 12
|
||||
};
|
||||
AddElement(playerBadge);
|
||||
|
||||
// Player number text
|
||||
string playerLabel = playerIndex == OriginalPlayerIndex.Handheld ? "H" : $"P{(int)playerIndex + 1}";
|
||||
var playerLabelElement = new TextElement(Padding + 12, rowY + 2, playerLabel, PlayerTextSize, SKColors.White)
|
||||
{
|
||||
Name = $"PlayerLabel_{i}",
|
||||
FontStyle = SKFontStyle.Bold,
|
||||
TextAlign = SKTextAlign.Center
|
||||
};
|
||||
AddElement(playerLabelElement);
|
||||
|
||||
// Controller info
|
||||
if (playerBindings.ContainsKey(playerIndex))
|
||||
{
|
||||
var controllers = playerBindings[playerIndex];
|
||||
var controllerNames = GetUniqueControllerDisplayNames(controllers);
|
||||
|
||||
var controllerTextElement = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen
|
||||
{
|
||||
Name = $"ControllerText_{i}",
|
||||
FontStyle = SKFontStyle.Bold
|
||||
};
|
||||
AddElement(controllerTextElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
var noControllerTextElement = new TextElement(Padding + 56, rowY + 2, LocaleManager.Instance[LocaleKeys.ControllerOverlayNoController], PlayerTextSize, new SKColor(128, 128, 128)) // Gray
|
||||
{
|
||||
Name = $"NoControllerText_{i}",
|
||||
FontStyle = SKFontStyle.Italic
|
||||
};
|
||||
AddElement(noControllerTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total height and update background
|
||||
float totalHeight = Padding + 40 + (playerIndices.Length * (PlayerRowHeight + PlayerSpacing)) + Padding + 20;
|
||||
var background = FindElement<RectangleElement>("Background");
|
||||
if (background != null)
|
||||
{
|
||||
background.Height = totalHeight;
|
||||
}
|
||||
|
||||
// Show the overlay (position will be set by Window class with actual dimensions)
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
private static SKColor GetPlayerColor(int playerIndex)
|
||||
{
|
||||
return playerIndex switch
|
||||
{
|
||||
0 => new SKColor(255, 92, 92), // Red for Player 1
|
||||
1 => new SKColor(54, 162, 235), // Blue for Player 2
|
||||
2 => new SKColor(255, 206, 84), // Yellow for Player 3
|
||||
3 => new SKColor(75, 192, 192), // Green for Player 4
|
||||
4 => new SKColor(153, 102, 255), // Purple for Player 5
|
||||
5 => new SKColor(255, 159, 64), // Orange for Player 6
|
||||
6 => new SKColor(199, 199, 199), // Light Gray for Player 7
|
||||
7 => new SKColor(83, 102, 255), // Indigo for Player 8
|
||||
8 => new SKColor(255, 99, 132), // Pink for Handheld
|
||||
_ => new SKColor(128, 128, 128) // Gray fallback
|
||||
};
|
||||
}
|
||||
|
||||
private List<string> GetUniqueControllerDisplayNames(List<OriginalInputConfig> controllers)
|
||||
{
|
||||
var nameGroups = new Dictionary<string, List<int>>();
|
||||
var displayNames = new List<string>();
|
||||
|
||||
// First pass: get base names and group them
|
||||
for (int i = 0; i < controllers.Count; i++)
|
||||
{
|
||||
string baseName = GetControllerDisplayName(controllers[i]);
|
||||
|
||||
if (!nameGroups.ContainsKey(baseName))
|
||||
{
|
||||
nameGroups[baseName] = new List<int>();
|
||||
}
|
||||
nameGroups[baseName].Add(i);
|
||||
displayNames.Add(baseName);
|
||||
}
|
||||
|
||||
// Second pass: add numbering for duplicates
|
||||
foreach (var group in nameGroups.Where(g => g.Value.Count > 1))
|
||||
{
|
||||
for (int i = 0; i < group.Value.Count; i++)
|
||||
{
|
||||
int index = group.Value[i];
|
||||
displayNames[index] = $"{group.Key} #{i + 1}";
|
||||
}
|
||||
}
|
||||
|
||||
return displayNames;
|
||||
}
|
||||
|
||||
private string GetControllerDisplayName(OriginalInputConfig config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config.Name))
|
||||
{
|
||||
return config.Backend switch
|
||||
{
|
||||
OriginalInputBackendType.WindowKeyboard => LocaleManager.Instance[LocaleKeys.ControllerOverlayKeyboard],
|
||||
OriginalInputBackendType.GamepadSDL2 => LocaleManager.Instance[LocaleKeys.ControllerOverlayController],
|
||||
_ => LocaleManager.Instance[LocaleKeys.ControllerOverlayUnknown]
|
||||
};
|
||||
}
|
||||
|
||||
// Truncate long controller names from the middle
|
||||
string name = config.Name;
|
||||
if (name.Length > 25)
|
||||
{
|
||||
int keepLength = 22; // Total characters to keep (excluding "...")
|
||||
int leftLength = (keepLength + 1) / 2; // Round up for left side
|
||||
int rightLength = keepLength / 2; // Round down for right side
|
||||
|
||||
name = name.Substring(0, leftLength) + "..." + name.Substring(name.Length - rightLength);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all player bindings
|
||||
/// </summary>
|
||||
private void ClearPlayerBindings()
|
||||
{
|
||||
var elementsToRemove = new List<OverlayElement>();
|
||||
|
||||
foreach (var element in GetElements())
|
||||
{
|
||||
if (element.Name.StartsWith("PlayerBadge_") ||
|
||||
element.Name.StartsWith("PlayerLabel_") ||
|
||||
element.Name.StartsWith("ControllerText_") ||
|
||||
element.Name.StartsWith("NoControllerText_"))
|
||||
{
|
||||
elementsToRemove.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var element in elementsToRemove)
|
||||
{
|
||||
RemoveElement(element);
|
||||
element.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay
|
||||
/// </summary>
|
||||
public override void Update(float deltaTime, SKSize screenSize)
|
||||
{
|
||||
_lifespan -= deltaTime;
|
||||
|
||||
if (_lifespan <= 0)
|
||||
{
|
||||
IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lifespan <= 0.5f)
|
||||
{
|
||||
// Fade out during the last 0.5 seconds
|
||||
Opacity = _lifespan / 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Opacity = 1;
|
||||
}
|
||||
|
||||
// Update position if screen size is provided
|
||||
if (screenSize.Width > 0 && screenSize.Height > 0)
|
||||
{
|
||||
SetPositionToTopRight(screenSize.Width, screenSize.Height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position overlay to top-right matching original AXAML positioning
|
||||
/// </summary>
|
||||
public void SetPositionToTopRight(float screenWidth, float screenHeight)
|
||||
{
|
||||
X = screenWidth - OverlayWidth - 20; // 20px margin from right
|
||||
Y = 50; // 50px margin from top
|
||||
}
|
||||
}
|
||||
}
|
146
src/Ryujinx/UI/Overlay/ImageElement.cs
Normal file
146
src/Ryujinx/UI/Overlay/ImageElement.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Image overlay element
|
||||
/// </summary>
|
||||
public class ImageElement : OverlayElement
|
||||
{
|
||||
private SKBitmap _bitmap;
|
||||
private byte[] _imageData;
|
||||
private string _imagePath;
|
||||
|
||||
public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium;
|
||||
public bool MaintainAspectRatio { get; set; } = true;
|
||||
|
||||
public ImageElement()
|
||||
{
|
||||
}
|
||||
|
||||
public ImageElement(float x, float y, float width, float height, byte[] imageData)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SetImageData(imageData);
|
||||
}
|
||||
|
||||
public ImageElement(float x, float y, float width, float height, string imagePath)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SetImagePath(imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from byte array
|
||||
/// </summary>
|
||||
public void SetImageData(byte[] imageData)
|
||||
{
|
||||
_imageData = imageData;
|
||||
_imagePath = null;
|
||||
LoadBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from file path
|
||||
/// </summary>
|
||||
public void SetImagePath(string imagePath)
|
||||
{
|
||||
_imagePath = imagePath;
|
||||
_imageData = null;
|
||||
LoadBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from existing SKBitmap
|
||||
/// </summary>
|
||||
public void SetBitmap(SKBitmap bitmap)
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = bitmap;
|
||||
_imageData = null;
|
||||
_imagePath = null;
|
||||
}
|
||||
|
||||
private void LoadBitmap()
|
||||
{
|
||||
try
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = null;
|
||||
|
||||
if (_imageData != null)
|
||||
{
|
||||
_bitmap = SKBitmap.Decode(_imageData);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_imagePath))
|
||||
{
|
||||
_bitmap = SKBitmap.Decode(_imagePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Failed to load image: {ex.Message}");
|
||||
_bitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || _bitmap == null || Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
FilterQuality = FilterQuality,
|
||||
Color = SKColors.White.WithAlpha((byte)(255 * effectiveOpacity))
|
||||
};
|
||||
|
||||
var sourceRect = new SKRect(0, 0, _bitmap.Width, _bitmap.Height);
|
||||
var destRect = new SKRect(X, Y, X + Width, Y + Height);
|
||||
|
||||
if (MaintainAspectRatio)
|
||||
{
|
||||
// Calculate aspect ratio preserving destination rectangle
|
||||
float sourceAspect = (float)_bitmap.Width / _bitmap.Height;
|
||||
float destAspect = Width / Height;
|
||||
|
||||
if (sourceAspect > destAspect)
|
||||
{
|
||||
// Source is wider, fit to width
|
||||
float newHeight = Width / sourceAspect;
|
||||
float yOffset = (Height - newHeight) / 2;
|
||||
destRect = new SKRect(X, Y + yOffset, X + Width, Y + yOffset + newHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Source is taller, fit to height
|
||||
float newWidth = Height * sourceAspect;
|
||||
float xOffset = (Width - newWidth) / 2;
|
||||
destRect = new SKRect(X + xOffset, Y, X + xOffset + newWidth, Y + Height);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
116
src/Ryujinx/UI/Overlay/Overlay.cs
Normal file
116
src/Ryujinx/UI/Overlay/Overlay.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Base overlay class containing multiple elements
|
||||
/// </summary>
|
||||
public abstract class Overlay : IOverlay
|
||||
{
|
||||
private readonly List<OverlayElement> _elements = new();
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public int ZIndex { get; set; } = 0;
|
||||
|
||||
public Overlay()
|
||||
{
|
||||
}
|
||||
|
||||
public Overlay(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an element to this overlay
|
||||
/// </summary>
|
||||
public void AddElement(OverlayElement element)
|
||||
{
|
||||
_elements.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an element from this overlay
|
||||
/// </summary>
|
||||
public void RemoveElement(OverlayElement element)
|
||||
{
|
||||
_elements.Remove(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all elements
|
||||
/// </summary>
|
||||
public IReadOnlyList<OverlayElement> GetElements()
|
||||
{
|
||||
return _elements.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find element by name
|
||||
/// </summary>
|
||||
public T FindElement<T>(string name) where T : OverlayElement
|
||||
{
|
||||
return _elements.OfType<T>().FirstOrDefault(e => e.Name == name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all elements
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
element.Dispose();
|
||||
}
|
||||
_elements.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay
|
||||
/// </summary>
|
||||
public abstract void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render this overlay
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
if (!IsVisible || Opacity <= 0.0f)
|
||||
return;
|
||||
|
||||
// Save canvas state
|
||||
canvas.Save();
|
||||
|
||||
// Apply overlay position offset
|
||||
if (X != 0 || Y != 0)
|
||||
{
|
||||
canvas.Translate(X, Y);
|
||||
}
|
||||
|
||||
// Render all elements
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
if (element.IsVisible)
|
||||
{
|
||||
element.Render(canvas, Opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore canvas state
|
||||
canvas.Restore();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
66
src/Ryujinx/UI/Overlay/OverlayElement.cs
Normal file
66
src/Ryujinx/UI/Overlay/OverlayElement.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all overlay elements
|
||||
/// </summary>
|
||||
public abstract class OverlayElement : IDisposable
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Render this element to the canvas
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw on</param>
|
||||
/// <param name="globalOpacity">Global opacity multiplier</param>
|
||||
public abstract void Render(SKCanvas canvas, float globalOpacity = 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a point is within this element's bounds
|
||||
/// </summary>
|
||||
public virtual bool Contains(float x, float y)
|
||||
{
|
||||
return x >= X && x <= X + Width && y >= Y && y <= Y + Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bounds of this element
|
||||
/// </summary>
|
||||
public SKRect GetBounds()
|
||||
{
|
||||
return new SKRect(X, Y, X + Width, Y + Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply opacity to a color
|
||||
/// </summary>
|
||||
protected SKColor ApplyOpacity(SKColor color, float opacity)
|
||||
{
|
||||
return color.WithAlpha((byte)(color.Alpha * opacity));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
161
src/Ryujinx/UI/Overlay/OverlayManager.cs
Normal file
161
src/Ryujinx/UI/Overlay/OverlayManager.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages multiple overlays and handles rendering
|
||||
/// </summary>
|
||||
public class OverlayManager : IOverlayManager
|
||||
{
|
||||
private readonly List<IOverlay> _overlays = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Add an overlay to the manager
|
||||
/// </summary>
|
||||
public void AddOverlay(IOverlay overlay)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overlays.Add(overlay);
|
||||
SortOverlays();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an overlay from the manager
|
||||
/// </summary>
|
||||
public void RemoveOverlay(Overlay overlay)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overlays.Remove(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove overlay by name
|
||||
/// </summary>
|
||||
public void RemoveOverlay(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var overlay = _overlays.FirstOrDefault(o => o.Name == name);
|
||||
if (overlay != null)
|
||||
{
|
||||
_overlays.Remove(overlay);
|
||||
overlay.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find overlay by name
|
||||
/// </summary>
|
||||
public IOverlay FindOverlay(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overlays.FirstOrDefault(o => o.Name == name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all overlays
|
||||
/// </summary>
|
||||
public IReadOnlyList<IOverlay> GetOverlays()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overlays.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all overlays
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays)
|
||||
{
|
||||
overlay.Dispose();
|
||||
}
|
||||
_overlays.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all overlays (for animations)
|
||||
/// </summary>
|
||||
public void Update(float deltaTime, SKSize screenSize = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays.Where(o => o.IsVisible))
|
||||
{
|
||||
overlay.Update(deltaTime, screenSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render all visible overlays
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays.Where(o => o.IsVisible && o.Opacity > 0.0f))
|
||||
{
|
||||
overlay.Render(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort overlays by Z-index
|
||||
/// </summary>
|
||||
private void SortOverlays()
|
||||
{
|
||||
_overlays.Sort((a, b) => a.ZIndex.CompareTo(b.ZIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show overlay
|
||||
/// </summary>
|
||||
public void ShowOverlay(string name)
|
||||
{
|
||||
var overlay = FindOverlay(name);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.IsVisible = true;
|
||||
overlay.Opacity = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide overlay
|
||||
/// </summary>
|
||||
public void HideOverlay(string name)
|
||||
{
|
||||
var overlay = FindOverlay(name);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.IsVisible = false;
|
||||
overlay.Opacity = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
78
src/Ryujinx/UI/Overlay/RectangleElement.cs
Normal file
78
src/Ryujinx/UI/Overlay/RectangleElement.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Rectangle overlay element
|
||||
/// </summary>
|
||||
public class RectangleElement : OverlayElement
|
||||
{
|
||||
public SKColor BackgroundColor { get; set; } = SKColors.Transparent;
|
||||
public SKColor BorderColor { get; set; } = SKColors.Transparent;
|
||||
public float BorderWidth { get; set; } = 0;
|
||||
public float CornerRadius { get; set; } = 0;
|
||||
|
||||
public RectangleElement()
|
||||
{
|
||||
}
|
||||
|
||||
public RectangleElement(float x, float y, float width, float height, SKColor backgroundColor)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
BackgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
var bounds = new SKRect(X, Y, X + Width, Y + Height);
|
||||
|
||||
// Draw background
|
||||
if (BackgroundColor.Alpha > 0)
|
||||
{
|
||||
using var backgroundPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(BackgroundColor, effectiveOpacity),
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, backgroundPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.DrawRect(bounds, backgroundPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw border
|
||||
if (BorderWidth > 0 && BorderColor.Alpha > 0)
|
||||
{
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(BorderColor, effectiveOpacity),
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = BorderWidth,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, borderPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.DrawRect(bounds, borderPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
src/Ryujinx/UI/Overlay/TextElement.cs
Normal file
121
src/Ryujinx/UI/Overlay/TextElement.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Text overlay element
|
||||
/// </summary>
|
||||
public class TextElement : OverlayElement
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public SKColor TextColor { get; set; } = SKColors.White;
|
||||
public float FontSize { get; set; } = 16;
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
public SKFontStyle FontStyle { get; set; } = SKFontStyle.Normal;
|
||||
public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left;
|
||||
public bool IsAntialias { get; set; } = true;
|
||||
|
||||
// Shadow properties
|
||||
public bool HasShadow { get; set; } = false;
|
||||
public SKColor ShadowColor { get; set; } = SKColors.Black;
|
||||
public float ShadowOffsetX { get; set; } = 1;
|
||||
public float ShadowOffsetY { get; set; } = 1;
|
||||
public float ShadowBlur { get; set; } = 0;
|
||||
|
||||
public TextElement()
|
||||
{
|
||||
}
|
||||
|
||||
public TextElement(float x, float y, string text, float fontSize = 16, SKColor? color = null)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Text = text;
|
||||
FontSize = fontSize;
|
||||
TextColor = color ?? SKColors.White;
|
||||
|
||||
// Auto-calculate width and height based on text
|
||||
UpdateDimensions();
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || string.IsNullOrEmpty(Text))
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
|
||||
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(TextColor, effectiveOpacity),
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface,
|
||||
TextAlign = TextAlign,
|
||||
IsAntialias = IsAntialias
|
||||
};
|
||||
|
||||
float textX = X;
|
||||
float textY = Y + FontSize; // Baseline adjustment
|
||||
|
||||
// Adjust X position based on alignment
|
||||
if (TextAlign == SKTextAlign.Center)
|
||||
{
|
||||
textX += Width / 2;
|
||||
}
|
||||
else if (TextAlign == SKTextAlign.Right)
|
||||
{
|
||||
textX += Width;
|
||||
}
|
||||
|
||||
// Draw shadow if enabled
|
||||
if (HasShadow)
|
||||
{
|
||||
using var shadowPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(ShadowColor, effectiveOpacity),
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface,
|
||||
TextAlign = TextAlign,
|
||||
IsAntialias = IsAntialias
|
||||
};
|
||||
|
||||
if (ShadowBlur > 0)
|
||||
{
|
||||
shadowPaint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlur);
|
||||
}
|
||||
|
||||
canvas.DrawText(Text, textX + ShadowOffsetX, textY + ShadowOffsetY, shadowPaint);
|
||||
}
|
||||
|
||||
// Draw main text
|
||||
canvas.DrawText(Text, textX, textY, paint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update width and height based on current text and font settings
|
||||
/// </summary>
|
||||
public void UpdateDimensions()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text))
|
||||
{
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface
|
||||
};
|
||||
|
||||
var bounds = new SKRect();
|
||||
paint.MeasureText(Text, ref bounds);
|
||||
|
||||
Width = bounds.Width;
|
||||
Height = FontSize; // Use font size as height for consistency
|
||||
}
|
||||
}
|
||||
}
|
@ -526,18 +526,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetShortGamepadName(string str)
|
||||
{
|
||||
const string Ellipsis = "...";
|
||||
const int MaxSize = 50;
|
||||
|
||||
if (str.Length > MaxSize)
|
||||
{
|
||||
return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string GetShortGamepadId(string str)
|
||||
{
|
||||
@ -551,7 +540,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
string GetGamepadName(IGamepad gamepad, int controllerNumber)
|
||||
{
|
||||
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
||||
return $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
||||
}
|
||||
|
||||
string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber)
|
||||
@ -579,7 +568,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
|
||||
Devices.Add((DeviceType.Keyboard, id, $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)}"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -652,138 +641,21 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
InputConfig config;
|
||||
if (activeDevice.Type == DeviceType.Keyboard)
|
||||
{
|
||||
string id = activeDevice.Id;
|
||||
string name = activeDevice.Name;
|
||||
|
||||
config = new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = id,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick =
|
||||
new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
};
|
||||
config = DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(activeDevice.Id, activeDevice.Name, _playerId);
|
||||
}
|
||||
else if (activeDevice.Type == DeviceType.Controller)
|
||||
{
|
||||
bool isNintendoStyle = Devices.ToList().FirstOrDefault(x => x.Id == activeDevice.Id).Name.Contains("Nintendo");
|
||||
|
||||
string id = activeDevice.Id.Split(" ")[0];
|
||||
string name = activeDevice.Name;
|
||||
|
||||
config = new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL2,
|
||||
Id = id,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
EnableMotion = true,
|
||||
Sensitivity = 100,
|
||||
GyroDeadzone = 1,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
EnableRumble = false,
|
||||
},
|
||||
};
|
||||
bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(activeDevice.Name);
|
||||
config = DefaultInputConfigurationProvider.CreateDefaultControllerConfig(activeDevice.Id, activeDevice.Name, _playerId, isNintendoStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new InputConfig();
|
||||
config = new InputConfig
|
||||
{
|
||||
PlayerIndex = _playerId
|
||||
};
|
||||
}
|
||||
|
||||
config.PlayerIndex = _playerId;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.UI;
|
||||
@ -310,10 +311,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void TotalTimePlayed_Recalculated(Optional<TimeSpan> ts)
|
||||
{
|
||||
ShowTotalTimePlayed = ts.HasValue;
|
||||
|
||||
if (ts.HasValue)
|
||||
LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, ValueFormatUtils.FormatTimeSpan(ts.Value));
|
||||
{
|
||||
var formattedPlayTime = ValueFormatUtils.FormatTimeSpan(ts.Value);
|
||||
LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime);
|
||||
ShowTotalTimePlayed = formattedPlayTime != string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
ShowTotalTimePlayed = ts.HasValue;
|
||||
}
|
||||
|
||||
public bool ShowTotalTimePlayed
|
||||
@ -334,7 +340,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
_listSelectedApplication = value;
|
||||
|
||||
if (_listSelectedApplication != null && ListAppContextMenu == null)
|
||||
|
||||
ListAppContextMenu = new ApplicationContextMenu();
|
||||
else if (_listSelectedApplication == null && ListAppContextMenu != null)
|
||||
ListAppContextMenu = null!;
|
||||
@ -1595,9 +1600,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
// Code where conditions will be executed after loading user configuration
|
||||
if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != backendThreadingInit)
|
||||
{
|
||||
Rebooter.RebootAppWithGame(application.Path,
|
||||
Rebooter.RebootAppWithGame(application.Path,
|
||||
[
|
||||
"--bt",
|
||||
"--bt",
|
||||
ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()
|
||||
]);
|
||||
|
||||
@ -1686,10 +1691,33 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
SetMainContent(RendererHostControl);
|
||||
|
||||
RendererHostControl.Focus();
|
||||
|
||||
// Show controller overlay
|
||||
ShowControllerOverlay();
|
||||
});
|
||||
|
||||
public static void UpdateGameMetadata(string titleId)
|
||||
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame());
|
||||
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
|
||||
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
|
||||
|
||||
private void ShowControllerOverlay()
|
||||
{
|
||||
try
|
||||
{
|
||||
var inputConfigs = ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig
|
||||
? ConfigurationState.InstanceExtra.Hid.InputConfig.Value
|
||||
: ConfigurationState.Instance.Hid.InputConfig.Value;
|
||||
|
||||
// Always show overlay - if no configs, it will show test data
|
||||
int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value;
|
||||
// Show overlay through the GPU context window directly
|
||||
AppHost.ShowControllerOverlay(inputConfigs, duration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error but don't let it crash the game launch
|
||||
Logger.Error?.Print(LogClass.UI, $"Failed to show controller overlay: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshFirmwareStatus()
|
||||
{
|
||||
|
@ -146,6 +146,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public bool EnableMouse { get; set; }
|
||||
public bool DisableInputWhenOutOfFocus { get; set; }
|
||||
public int FocusLostActionType { get; set; }
|
||||
public int ControllerOverlayGameStartDuration { get; set; }
|
||||
public int ControllerOverlayInputCycleDuration { get; set; }
|
||||
|
||||
public bool UseGlobalInputConfig
|
||||
{
|
||||
@ -587,6 +589,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
HideCursor = (int)config.HideCursor.Value;
|
||||
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
|
||||
FocusLostActionType = (int)config.FocusLostActionType.Value;
|
||||
ControllerOverlayGameStartDuration = config.ControllerOverlayGameStartDuration.Value;
|
||||
ControllerOverlayInputCycleDuration = config.ControllerOverlayInputCycleDuration.Value;
|
||||
|
||||
GameDirectories.Clear();
|
||||
GameDirectories.AddRange(config.UI.GameDirs.Value);
|
||||
@ -698,6 +702,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
config.HideCursor.Value = (HideCursorMode)HideCursor;
|
||||
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
|
||||
config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
|
||||
config.ControllerOverlayGameStartDuration.Value = ControllerOverlayGameStartDuration;
|
||||
config.ControllerOverlayInputCycleDuration.Value = ControllerOverlayInputCycleDuration;
|
||||
config.UI.GameDirs.Value = [.. GameDirectories];
|
||||
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
|
||||
|
||||
|
@ -64,6 +64,7 @@
|
||||
MinWidth="200"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0, 0, 5, 0"
|
||||
Foreground="{DynamicResource SystemAccentColorLight2}"
|
||||
IsVisible="{Binding StatusBarVisible}"
|
||||
Maximum="{Binding StatusBarProgressMaximum}"
|
||||
|
@ -120,6 +120,60 @@
|
||||
<TextBlock Text="{ext:Locale SettingsTabHotkeysOnlyWhilePressed}" Margin="10,0" />
|
||||
<CheckBox IsChecked="{Binding KeyboardHotkey.TurboModeWhileHeld}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer1, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer1">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer1, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer2, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer2">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer2, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer3, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer3">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer3, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer4, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer4">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer4, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer5, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer5">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer5, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer6, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer6">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer6, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer7, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer7">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer7, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer8, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer8">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer8, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsHandheld, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDeviceHandheld">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDeviceHandheld, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
@ -82,7 +82,16 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{ "VolumeDown", () => viewModel.KeyboardHotkey.VolumeDown = Key.Unbound },
|
||||
{ "CustomVSyncIntervalIncrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = Key.Unbound },
|
||||
{ "CustomVSyncIntervalDecrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = Key.Unbound },
|
||||
{ "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound }
|
||||
{ "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer1", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer1 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer2", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer2 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer3", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer3 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer4", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer4 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer5", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer5 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer6", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer6 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer7", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer7 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer8", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer8 = Key.Unbound },
|
||||
{ "CycleInputDeviceHandheld", () => viewModel.KeyboardHotkey.CycleInputDeviceHandheld = Key.Unbound }
|
||||
};
|
||||
|
||||
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
|
||||
@ -162,6 +171,33 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||
case "TurboMode":
|
||||
ViewModel.KeyboardHotkey.TurboMode = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer1":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer1 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer2":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer2 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer3":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer3 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer4":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer4 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer5":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer5 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer6":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer6 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer7":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer7 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer8":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer8 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDeviceHandheld":
|
||||
ViewModel.KeyboardHotkey.CycleInputDeviceHandheld = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -157,6 +157,32 @@
|
||||
</ComboBox>
|
||||
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
|
||||
</StackPanel>
|
||||
<Separator Height="1" Margin="0,15,0,15" />
|
||||
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabUIControllerOverlay}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabUIControllerOverlayGameStartDuration}"
|
||||
Width="200" />
|
||||
<NumericUpDown Value="{Binding ControllerOverlayGameStartDuration}"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
Increment="1"
|
||||
FormatString="0"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayGameStartDurationTooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabUIControllerOverlayInputCycleDuration}"
|
||||
Width="200" />
|
||||
<NumericUpDown Value="{Binding ControllerOverlayInputCycleDuration}"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
Increment="1"
|
||||
FormatString="0"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayInputCycleDurationTooltip}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1"
|
||||
|
@ -10,6 +10,7 @@
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main"
|
||||
xmlns:viewsMisc="clr-namespace:Ryujinx.Ava.UI.Views.Misc"
|
||||
xmlns:overlayControls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
Cursor="{Binding Cursor}"
|
||||
Title="{Binding Title}"
|
||||
WindowState="{Binding WindowState}"
|
||||
@ -178,5 +179,7 @@
|
||||
Name="StatusBarView"
|
||||
Grid.Row="2" />
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
</window:StyleableAppWindow>
|
||||
|
@ -213,13 +213,13 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
}
|
||||
}
|
||||
|
||||
public void Application_Opened(object sender, ApplicationOpenedEventArgs args)
|
||||
public async void Application_Opened(object sender, ApplicationOpenedEventArgs args)
|
||||
{
|
||||
if (args.Application != null)
|
||||
{
|
||||
ViewModel.SelectedIcon = args.Application.Icon;
|
||||
|
||||
ViewModel.LoadApplication(args.Application).Wait();
|
||||
await ViewModel.LoadApplication(args.Application);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
|
Reference in New Issue
Block a user