diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index baec3399f..500e0c0be 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -44,7 +44,7 @@ body: attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. - placeholder: What are the alternatives you've considered? Sometimes, the Collapse team can't always implement everything the way you envisonned it, so what are some compromises, changes you're willing to make to the current proposal? + placeholder: What are the alternatives you've considered? Sometimes, the Collapse team can't always implement everything the way you envisioned it, so what are some compromises, changes you're willing to make to the current proposal? validations: required: true @@ -56,4 +56,4 @@ body: placeholder: If there are any images, concept art, code snippets you're willing to share, please put them here. validations: required: false ---- \ No newline at end of file +--- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a7ca0d08..d347d6c92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,15 +21,16 @@ jobs: runs-on: windows-latest strategy: matrix: - configuration: [Release] # No need to distribute Debug builds + configuration: [Debug] # No need to distribute Debug builds platform: [x64] - framework: [net8.0-windows10.0.22621.0] + framework: [net9.0-windows10.0.22621.0] env: Configuration: ${{ matrix.configuration }} Platform: ${{ matrix.platform }} DOTNET_INSTALL_DIR: '.\.dotnet' - DOTNET_VERSION: '8.x' + DOTNET_VERSION: '9.x' + DOTNET_QUALITY: 'ga' NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: @@ -42,6 +43,48 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-quality: ${{ env.DOTNET_QUALITY }} + cache: true + cache-dependency-path: CollapseLauncher/packages.lock.json + + - name: Build + run: | + dotnet publish CollapseLauncher -p:PublishProfile=Publish-DebugCIRelease -p:PublishDir=".\debug-build\" + + - name: Upload Artifact + uses: actions/upload-artifact@v4.3.1 + with: + name: collapse_${{ matrix.platform }}-${{ matrix.configuration }}_${{ matrix.framework }}_${{ github.sha }} + path: ./CollapseLauncher/debug-build/ + compression-level: 9 + + build-nativeaot: + runs-on: windows-latest + strategy: + matrix: + configuration: [Debug] + platform: [x64] + framework: [net9.0-windows10.0.22621.0] + + env: + Configuration: ${{ matrix.configuration }} + Platform: ${{ matrix.platform }} + DOTNET_INSTALL_DIR: '.\.dotnet' + DOTNET_VERSION: '9.x' + DOTNET_QUALITY: 'ga' + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + + steps: + - name: Checkout + uses: actions/checkout@v4.1.5 + with: + submodules: recursive + + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-quality: ${{ env.DOTNET_QUALITY }} cache: true cache-dependency-path: CollapseLauncher/packages.lock.json @@ -51,16 +94,20 @@ jobs: - name: Build run: | - dotnet publish CollapseLauncher -p:PublishProfile=Publish-PreviewRelease -p:PublishDir=".\preview-build\" + dotnet publish CollapseLauncher -p:PublishProfile=Publish-DebugCIReleaseAOT -p:PublishDir=".\debug-aot-build\" - - name: Upload Artifact (Release) + - name: Upload Artifact uses: actions/upload-artifact@v4.3.1 - if: ${{ matrix.configuration == 'Release' }} with: - name: collapse_${{ matrix.platform }}-${{ matrix.configuration }}_${{ matrix.framework }}_${{ github.sha }} - path: ./CollapseLauncher/preview-build/ + name: aot-experimental_collapse_${{ matrix.platform }}-${{ matrix.configuration }}_${{ matrix.framework }}_${{ github.sha }} + path: ./CollapseLauncher/debug-aot-build/ compression-level: 9 + notify-discord: + runs-on: ubuntu-latest + if: always() + needs: [build, build-nativeaot] + steps: - name: Notify Discord uses: sarisia/actions-status-discord@v1.13.0 if: always() diff --git a/.gitignore b/.gitignore index 5f823895b..b2fcd1f69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -*/bin/* -*/obj/* +**/bin/* +**/obj/* *build/* .vs/* *.user @@ -7,6 +7,7 @@ packages/* CollapseLauncher/Deps/* CollapseLauncher/Invoker/* +**/Generated Files/** *.psd InstallerProp/Output/* InstallerProp/temp/** diff --git a/.gitmodules b/.gitmodules index 4a90b493f..bd0331340 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "Hi3Helper.Core/Classes/Data/Tools/SevenZipTool/SevenZipExtractor"] - path = Hi3Helper.Core/Classes/Data/Tools/SevenZipTool/SevenZipExtractor - url = https://github.com/neon-nyan/SevenZipExtractor [submodule "Hi3Helper.Http"] path = Hi3Helper.Http url = https://github.com/neon-nyan/Hi3Helper.Http @@ -19,3 +16,12 @@ [submodule "Hi3Helper.Sophon"] path = Hi3Helper.Sophon url = https://github.com/CollapseLauncher/Hi3Helper.Sophon +[submodule "ImageEx"] + path = ImageEx + url = https://github.com/CollapseLauncher/ImageEx +[submodule "SevenZipExtractor"] + path = SevenZipExtractor + url = https://github.com/CollapseLauncher/SevenZipExtractor +[submodule "H.NotifyIcon"] + path = H.NotifyIcon + url = https://github.com/CollapseLauncher/H.NotifyIcon diff --git a/.idea/.idea.CollapseLauncher/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.CollapseLauncher/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..5cb71ef0b --- /dev/null +++ b/.idea/.idea.CollapseLauncher/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.CollapseLauncher/.idea/projectSettingsUpdater.xml b/.idea/.idea.CollapseLauncher/.idea/projectSettingsUpdater.xml index 4bb9f4d2a..64af657f5 100644 --- a/.idea/.idea.CollapseLauncher/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.CollapseLauncher/.idea/projectSettingsUpdater.xml @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/.idea/.idea.CollapseLauncher/.idea/vcs.xml b/.idea/.idea.CollapseLauncher/.idea/vcs.xml index 7c2ea1047..b5af8fbf9 100644 --- a/.idea/.idea.CollapseLauncher/.idea/vcs.xml +++ b/.idea/.idea.CollapseLauncher/.idea/vcs.xml @@ -2,11 +2,16 @@ + + + + + \ No newline at end of file diff --git a/Backup/CommunityToolkit.WinUI.Controls.ImageCropper.csproj b/Backup/CommunityToolkit.WinUI.Controls.ImageCropper.csproj new file mode 100644 index 000000000..0e97ae7dd --- /dev/null +++ b/Backup/CommunityToolkit.WinUI.Controls.ImageCropper.csproj @@ -0,0 +1,30 @@ + + + + + ImageCropper + The ImageCropper control allows the user to freely crop an image. + + + CommunityToolkit.WinUI.Controls.ImageCropperRns + ReadMe.md + + + + + + + + + + + + True + \ + + + + + $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName) + + diff --git a/Backup/ReadMe.md b/Backup/ReadMe.md new file mode 100644 index 000000000..07942198d --- /dev/null +++ b/Backup/ReadMe.md @@ -0,0 +1,38 @@ + +# Windows Community Toolkit - ImageCropper + +This package is part of the [Windows Community Toolkit](https://aka.ms/toolkit/windows) from the [.NET Foundation](https://dotnetfoundation.org). + +## Package Contents + +This package contains the following controls in the `CommunityToolkit.WinUI.Controls` namespace: + +- ImageCropper + +## Which Package is for me? + +If you're developing with _UWP/WinUI 2 or Uno.UI_ you should be using the `CommunityToolkit.Uwp.Controls.ImageCropper` package. + +If you're developing with _WindowsAppSDK/WinUI 3 or Uno.WinUI_ you should be using the `CommunityToolkit.WinUI.Controls.ImageCropper` package. + +## WinUI Resources (UWP) + +For UWP projects, the WinUI 2 reference requires you include the WinUI XAML Resources in your App.xaml file: + +```xml + + + +``` + +See [Getting Started in WinUI 2](https://learn.microsoft.com/windows/apps/winui/winui2/getting-started) for more information. + +## Documentation + +Further documentation about these components can be found at: https://aka.ms/windowstoolkitdocs + +## License + +MIT + +See License.md in package for more details. diff --git a/ClearCache.bat b/ClearCache.bat index 50a691b8b..c42722760 100644 --- a/ClearCache.bat +++ b/ClearCache.bat @@ -3,6 +3,10 @@ echo Clearing Collapse cache rmdir /S /Q CollapseLauncher\bin && rmdir /S /Q CollapseLauncher\obj echo Clearing ColorThief cache rmdir /S /Q ColorThief\ColorThief\bin && rmdir /S /Q ColorThief\ColorThief\obj +echo Clearing CommunityToolkit.ImageCropper cache +rmdir /S /Q Hi3Helper.CommunityToolkit\ImageCropper\bin && rmdir /S /Q Hi3Helper.CommunityToolkit\ImageCropper\obj +echo Clearing CommunityToolkit.SettingsControls cache +rmdir /S /Q Hi3Helper.CommunityToolkit\SettingsControls\bin && rmdir /S /Q Hi3Helper.CommunityToolkit\SettingsControls\obj echo Clearing Core cache rmdir /S /Q Hi3Helper.Core\bin && rmdir /S /Q Hi3Helper.Core\obj echo Clearing EncTool cache @@ -13,12 +17,16 @@ echo Clearing Http cache rmdir /S /Q Hi3Helper.Http\bin && rmdir /S /Q Hi3Helper.Http\obj echo Clearing Http tester cache rmdir /S /Q Hi3Helper.Http\Test\bin && rmdir /S /Q Hi3Helper.Http\Test\obj +echo Clearing TaskScheduler cache +rmdir /S /Q Hi3Helper.TaskScheduler\bin && rmdir /S /Q Hi3Helper.TaskScheduler\obj echo Clearing HDiff cache rmdir /S /Q Hi3Helper.SharpHDiffPatch\Hi3Helper.SharpHDiffPatch\bin && rmdir /S /Q Hi3Helper.SharpHDiffPatch\Hi3Helper.SharpHDiffPatch\obj echo Clearing 2nd HDiff cache rmdir /S /Q Hi3Helper.SharpHDiffPatch\SharpHDiffPatch\bin && rmdir /S /Q Hi3Helper.SharpHDiffPatch\SharpHDiffPatch\obj echo Clearing InnoSetupHelper cache rmdir /S /Q InnoSetupHelper\bin && rmdir /S /Q InnoSetupHelper\obj +echo Clearing ImageEx cache +rmdir /S /Q ImageEx\ImageEx\bin && rmdir /S /Q ImageEx\ImageEx\obj echo Clearing 7z cache rmdir /S /Q Hi3Helper.Core\Classes\Data\Tools\SevenZipTool\SevenZipExtractor\SevenZipExtractor\bin && rmdir /S /Q Hi3Helper.Core\Classes\Data\Tools\SevenZipTool\SevenZipExtractor\SevenZipExtractor\obj echo Clearing SharpDiscordRPC cache diff --git a/CollapseLauncher.sln b/CollapseLauncher.sln index e25193810..c6045a5c1 100644 --- a/CollapseLauncher.sln +++ b/CollapseLauncher.sln @@ -14,7 +14,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorThief", "ColorThief\Co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hi3Helper.EncTool.Test", "Hi3Helper.EncTool.Test\Hi3Helper.EncTool.Test.csproj", "{1BAB5CE4-640E-41A7-B529-4842D378BD4C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SevenZipExtractor", "Hi3Helper.Core\Classes\Data\Tools\SevenZipTool\SevenZipExtractor\SevenZipExtractor\SevenZipExtractor.csproj", "{D145686A-C0AA-4164-9854-A8942CB1DFDE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SevenZipExtractor", "SevenZipExtractor\SevenZipExtractor\SevenZipExtractor.csproj", "{D145686A-C0AA-4164-9854-A8942CB1DFDE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InnoSetupHelper", "InnoSetupHelper\InnoSetupHelper.csproj", "{21691306-8D30-4993-98A2-A5AE0712D81A}" EndProject @@ -22,6 +22,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRPC", "Hi3Helper.Sha EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hi3Helper.Sophon", "Hi3Helper.Sophon\Hi3Helper.Sophon.csproj", "{3F87DCD0-39B7-4F8C-8F34-A0FC7C6E65A0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageEx", "ImageEx\ImageEx\ImageEx.csproj", "{A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hi3Helper.TaskScheduler", "Hi3Helper.TaskScheduler\Hi3Helper.TaskScheduler.csproj", "{C9CBAF52-49C7-4B72-A03B-130F596E24CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper", "Hi3Helper.CommunityToolkit\ImageCropper\Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj", "{558A1D17-BEB4-49DF-A200-15ABE283BDED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls", "Hi3Helper.CommunityToolkit\SettingsControls\Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj", "{5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "H.NotifyIcon.WinUI", "H.NotifyIcon\src\libs\H.NotifyIcon.WinUI\H.NotifyIcon.WinUI.csproj", "{141083CC-A924-4E19-904C-AF91361405A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "H.NotifyIcon", "H.NotifyIcon\src\libs\H.NotifyIcon\H.NotifyIcon.csproj", "{6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "H.GeneratedIcons.System.Drawing", "H.NotifyIcon\src\libs\H.GeneratedIcons.System.Drawing\H.GeneratedIcons.System.Drawing.csproj", "{911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -88,6 +102,46 @@ Global {3F87DCD0-39B7-4F8C-8F34-A0FC7C6E65A0}.Publish|x64.Build.0 = Release|x64 {3F87DCD0-39B7-4F8C-8F34-A0FC7C6E65A0}.Release|x64.ActiveCfg = Release|x64 {3F87DCD0-39B7-4F8C-8F34-A0FC7C6E65A0}.Release|x64.Build.0 = Release|x64 + {A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}.Debug|x64.ActiveCfg = Debug|x64 + {A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}.Debug|x64.Build.0 = Debug|x64 + {A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}.Publish|x64.ActiveCfg = Release|x64 + {A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}.Publish|x64.Build.0 = Release|x64 + {A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}.Release|x64.ActiveCfg = Release|x64 + {A6AF9DE9-1A18-4C2D-B106-B68A0A7CD07D}.Release|x64.Build.0 = Release|x64 + {C9CBAF52-49C7-4B72-A03B-130F596E24CB}.Debug|x64.ActiveCfg = Debug|x64 + {C9CBAF52-49C7-4B72-A03B-130F596E24CB}.Debug|x64.Build.0 = Debug|x64 + {C9CBAF52-49C7-4B72-A03B-130F596E24CB}.Publish|x64.ActiveCfg = Release|x64 + {C9CBAF52-49C7-4B72-A03B-130F596E24CB}.Release|x64.ActiveCfg = Release|x64 + {558A1D17-BEB4-49DF-A200-15ABE283BDED}.Debug|x64.ActiveCfg = Debug|x64 + {558A1D17-BEB4-49DF-A200-15ABE283BDED}.Debug|x64.Build.0 = Debug|x64 + {558A1D17-BEB4-49DF-A200-15ABE283BDED}.Publish|x64.ActiveCfg = Release|x64 + {558A1D17-BEB4-49DF-A200-15ABE283BDED}.Publish|x64.Build.0 = Release|x64 + {558A1D17-BEB4-49DF-A200-15ABE283BDED}.Release|x64.ActiveCfg = Release|x64 + {558A1D17-BEB4-49DF-A200-15ABE283BDED}.Release|x64.Build.0 = Release|x64 + {5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}.Debug|x64.ActiveCfg = Debug|x64 + {5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}.Debug|x64.Build.0 = Debug|x64 + {5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}.Publish|x64.ActiveCfg = Release|x64 + {5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}.Publish|x64.Build.0 = Release|x64 + {5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}.Release|x64.ActiveCfg = Release|x64 + {5A1243EC-EFD9-4B55-8F29-D1A91A9B027D}.Release|x64.Build.0 = Release|x64 + {141083CC-A924-4E19-904C-AF91361405A5}.Debug|x64.ActiveCfg = Debug|x64 + {141083CC-A924-4E19-904C-AF91361405A5}.Debug|x64.Build.0 = Debug|x64 + {141083CC-A924-4E19-904C-AF91361405A5}.Publish|x64.ActiveCfg = Release|x64 + {141083CC-A924-4E19-904C-AF91361405A5}.Publish|x64.Build.0 = Release|x64 + {141083CC-A924-4E19-904C-AF91361405A5}.Release|x64.ActiveCfg = Release|x64 + {141083CC-A924-4E19-904C-AF91361405A5}.Release|x64.Build.0 = Release|x64 + {6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}.Debug|x64.ActiveCfg = Debug|x64 + {6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}.Debug|x64.Build.0 = Debug|x64 + {6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}.Publish|x64.ActiveCfg = Release|x64 + {6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}.Publish|x64.Build.0 = Release|x64 + {6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}.Release|x64.ActiveCfg = Release|x64 + {6C8A25FA-BA1C-4EE4-8A9D-2FB4918077FB}.Release|x64.Build.0 = Release|x64 + {911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}.Debug|x64.ActiveCfg = Debug|x64 + {911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}.Debug|x64.Build.0 = Debug|x64 + {911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}.Publish|x64.ActiveCfg = Release|x64 + {911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}.Publish|x64.Build.0 = Release|x64 + {911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}.Release|x64.ActiveCfg = Release|x64 + {911C98FD-C64D-4BAC-8EF5-0616F8EFF7B9}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CollapseLauncher.sln.DotSettings b/CollapseLauncher.sln.DotSettings index 54eafe745..44e429afd 100644 --- a/CollapseLauncher.sln.DotSettings +++ b/CollapseLauncher.sln.DotSettings @@ -42,6 +42,7 @@ CDNURL FX FXAA + GLC HD HDR ID @@ -68,6 +69,12 @@ True True True + True + True + True + True + True True + True True \ No newline at end of file diff --git a/CollapseLauncher.slnx b/CollapseLauncher.slnx new file mode 100644 index 000000000..fd78fee4e --- /dev/null +++ b/CollapseLauncher.slnx @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CollapseLauncher/App.xaml b/CollapseLauncher/App.xaml index bcfa59e18..29062f0d2 100644 --- a/CollapseLauncher/App.xaml +++ b/CollapseLauncher/App.xaml @@ -1,7 +1,8 @@ - - + + + + + + + + + + + + + + @@ -27,345 +41,359 @@ #ffd52a #ffd52a #ffd52a - - - - - + + + + + + FallbackColor="{ThemeResource SystemAccentColor}" + Opacity="0.8" + TintColor="{ThemeResource SystemAccentColor}" + TintLuminosityOpacity="0.8" + TintOpacity="0.2" /> + Color="{ThemeResource SystemAccentColorLight2}" /> + Color="#FFFFFF" /> + Color="#000000" /> - - - - + Color="#242424" /> + + + + #ffd52a + Color="{ThemeResource DialogTitleColor}" /> - - - + TintOpacity="0.0" /> + + + + TintOpacity="0.0" /> + TintOpacity="0" /> + TintOpacity="0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="1" /> + TintOpacity="0.6" /> + TintOpacity="0.0" /> + TintOpacity="0.75" /> + TintOpacity="0.0" /> + TintLuminosityOpacity="0.2" + TintOpacity="0.2" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - - + TintOpacity="1" /> + + + TintOpacity="0.0" /> + TintLuminosityOpacity="0.5" + TintOpacity="0.0" /> - - - + TintOpacity="0.0" /> + + + + + Color="#00000000" /> + Color="#22000000" /> + Color="#11000000" /> + Color="#0A000000" /> + + Color="#22000000" /> + Color="#11000000" /> + Color="#0A000000" /> + Color="#44000000" /> + Color="#55000000" /> + Color="#33000000" /> + Color="#22000000" /> - - - + + + + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="0.0" /> + + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="0.0" /> + + TintOpacity="0" /> + Opacity="0.9" + TintColor="{ThemeResource SystemAccentColor}" /> + Opacity="0.75" + TintColor="{ThemeResource SystemAccentColor}" /> + FallbackColor="{ThemeResource SystemAccentColor}" + TintColor="{ThemeResource SystemAccentColor}" /> + Opacity="0.75" + Color="#DDFFFFFF" /> + Color="#EE000000" /> + Color="#CC000000" /> + Color="#FF111111" /> + TintOpacity="0" /> + TintOpacity="0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="0.0" /> + + + Color="#00000000" /> + Color="#00000000" /> + + + Color="#A0C00000" /> + + @@ -377,382 +405,380 @@ #693758 #693758 #693758 - - - - - + + + + + + FallbackColor="{ThemeResource SystemAccentColor}" + Opacity="0.8" + TintColor="{ThemeResource SystemAccentColor}" + TintLuminosityOpacity="0.8" + TintOpacity="0.2" /> + Color="{ThemeResource SystemAccentColor}" /> + Color="#000000" /> + Color="#FFFFFF" /> - - - - + Color="#ECECEC" /> + + + + #693758 + Color="{ThemeResource DialogTitleColor}" /> - - - + TintOpacity="0.0" /> + + + + TintOpacity="0.0" /> + TintOpacity="1" /> + TintOpacity="0" /> + TintOpacity="0" /> + TintLuminosityOpacity="0.9" + TintOpacity="0.4" /> + TintOpacity="0.25" /> + TintOpacity="0.25" /> + TintOpacity="0" /> + TintOpacity="0" /> + TintOpacity="0.0" /> + TintOpacity="0.75" /> + TintOpacity="0.7" /> + TintOpacity="0" /> + TintOpacity="0" /> + TintOpacity="0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="1" /> + + TintOpacity="0.0" /> + TintLuminosityOpacity="0.9" + TintOpacity="0.0" /> + - - - + TintOpacity="0.0" /> + + + + Color="#00FFFFFF" /> + Color="#66FFFFFF" /> + Color="#44FFFFFF" /> + Color="#22FFFFFF" /> + Color="#66FFFFFF" /> + Color="#44FFFFFF" /> + Color="#22FFFFFF" /> + Color="#88FFFFFF" /> + Color="#55FFFFFF" /> + Color="#44FFFFFF" /> - - - - + Color="#22FFFFFF" /> + + + + - + + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="0.0" /> + + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="0.0" /> + + FallbackColor="#88FFFFFF" + TintColor="#88FFFFFF" /> + Opacity="0.9" + TintColor="{ThemeResource SystemAccentColor}" /> + Opacity="0.8" + TintColor="{ThemeResource SystemAccentColor}" /> + FallbackColor="{ThemeResource SystemAccentColor}" + TintColor="{ThemeResource SystemAccentColor}" /> + Color="#88000000" /> + Color="#DDFFFFFF" /> + Color="#CCFFFFFF" /> + Color="#FFFFFF" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> + TintOpacity="0.0" /> - + TintOpacity="0.0" /> + + + Color="#00FFFFFF" /> + Color="#00FFFFFF" /> + + + Color="#90FF6666" /> + + - - ms-appx:///Assets/Fonts/FontAwesomeBrand6.otf#Font Awesome 6 Brands - ms-appx:///Assets/Fonts/FontAwesomeRegular6.otf#Font Awesome 6 Free - ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid 0,47,0,0 + - + - + - - - - - - - - - - + + + + + + + + ms-appx:///Assets/Fonts/FontAwesomeBrand6.otf#Font Awesome 6 Brands + ms-appx:///Assets/Fonts/FontAwesomeRegular6.otf#Font Awesome 6 Free + ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free diff --git a/CollapseLauncher/App.xaml.cs b/CollapseLauncher/App.xaml.cs index c7f8f2561..28a54554d 100644 --- a/CollapseLauncher/App.xaml.cs +++ b/CollapseLauncher/App.xaml.cs @@ -8,7 +8,6 @@ using PhotoSauce.MagicScaler; using PhotoSauce.NativeCodecs.Libwebp; using System; -using System.Collections.Generic; using System.Linq; using Windows.UI; using static CollapseLauncher.InnerLauncherConfig; @@ -20,7 +19,7 @@ namespace CollapseLauncher public partial class App { public static bool IsAppKilled = false; - + public App() { if (DebugSettings != null) @@ -34,9 +33,28 @@ public App() DebugSettings.IsXamlResourceReferenceTracingEnabled = true; DebugSettings.IsBindingTracingEnabled = true; #endif - DebugSettings.XamlResourceReferenceFailed += (sender, args) => { LogWriteLine($"[XAML_RES_REFERENCE] Sender: {sender}\r\n{args!.Message}", LogType.Error, true); }; - DebugSettings.BindingFailed += (sender, args) => { LogWriteLine($"[XAML_BINDING] Sender: {sender}\r\n{args!.Message}", LogType.Error, true); }; - UnhandledException += (sender, e) => { LogWriteLine($"[XAML_OTHER] Sender: {sender}\r\n{e!.Exception} {e.Exception!.InnerException}", LogType.Error, true); }; + DebugSettings.XamlResourceReferenceFailed += static (sender, args) => + { + LogWriteLine($"[XAML_RES_REFERENCE] Sender: {sender}\r\n{args!.Message}", LogType.Error, true); + #if !DEBUG + MainEntryPoint.SpawnFatalErrorConsole(new Exception(args!.Message)); + #endif + + }; + DebugSettings.BindingFailed += static (sender, args) => + { + LogWriteLine($"[XAML_BINDING] Sender: {sender}\r\n{args!.Message}", LogType.Error, true); + #if !DEBUG + MainEntryPoint.SpawnFatalErrorConsole(new Exception(args!.Message)); + #endif + }; + UnhandledException += static (sender, e) => + { + LogWriteLine($"[XAML_OTHER] Sender: {sender}\r\n{e!.Exception} {e.Exception!.InnerException}", LogType.Error, true); + #if !DEBUG + MainEntryPoint.SpawnFatalErrorConsole(e!.Exception); + #endif + }; } RequestedTheme = IsAppThemeLight ? ApplicationTheme.Light : ApplicationTheme.Dark; @@ -49,18 +67,19 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) { try { - ThemeChangerInvoker.ThemeEvent += (_, _) => { - WindowUtility.ApplyWindowTitlebarLegacyColor(); - bool isThemeLight = IsAppThemeLight; - Color color = isThemeLight ? Colors.Black : Colors.White; - Current!.Resources!["WindowCaptionForeground"] = color; + ThemeChangerInvoker.ThemeEvent += (_, _) => + { + WindowUtility.ApplyWindowTitlebarLegacyColor(); + bool isThemeLight = IsAppThemeLight; + Color color = isThemeLight ? Colors.Black : Colors.White; + Current!.Resources!["WindowCaptionForeground"] = color; - WindowUtility.CurrentAppWindow!.TitleBar!.ButtonForegroundColor = color; - WindowUtility.CurrentAppWindow!.TitleBar!.ButtonInactiveBackgroundColor = color; + WindowUtility.CurrentAppWindow!.TitleBar!.ButtonForegroundColor = color; + WindowUtility.CurrentAppWindow!.TitleBar!.ButtonInactiveBackgroundColor = color; - if (WindowUtility.CurrentWindow!.Content is not null and FrameworkElement frameworkElement) - frameworkElement.RequestedTheme = isThemeLight ? ElementTheme.Light : ElementTheme.Dark; - }; + if (WindowUtility.CurrentWindow!.Content is not null and FrameworkElement frameworkElement) + frameworkElement.RequestedTheme = isThemeLight ? ElementTheme.Light : ElementTheme.Dark; + }; Window toInitializeWindow = null; switch (m_appMode) @@ -114,7 +133,8 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) { LogWriteLine($"FATAL ERROR ON APP INITIALIZER LEVEL!!!\r\n{ex}", LogType.Error, true); LogWriteLine("\r\nIf this is not intended, please report it to: https://github.com/CollapseLauncher/Collapse/issues\r\nPress any key to exit..."); - Console.ReadLine(); + //Console.ReadLine(); + throw; } } @@ -127,14 +147,12 @@ public static void ToggleBlurBackdrop(bool useBackdrop = true) // then select the value, get the type of ResourceDictionary, then enumerate it foreach (ResourceDictionary list in resource! .ThemeDictionaries! - .OfType>() .Select(x => x.Value) .OfType()) { // Parse the dictionary as type of KeyValuePair, // and get the value which has type of AcrylicBrush only, then enumerate it foreach (AcrylicBrush theme in list - .OfType>() .Select(x => x.Value) .OfType()) { diff --git a/CollapseLauncher/Assets/Fonts/FontAwesomeBrand6.otf b/CollapseLauncher/Assets/Fonts/FontAwesomeBrand6.otf index 80ff0b665..3984ea31f 100644 Binary files a/CollapseLauncher/Assets/Fonts/FontAwesomeBrand6.otf and b/CollapseLauncher/Assets/Fonts/FontAwesomeBrand6.otf differ diff --git a/CollapseLauncher/Assets/Fonts/FontAwesomeRegular6.otf b/CollapseLauncher/Assets/Fonts/FontAwesomeRegular6.otf index 2edf2b2f3..b32b57de6 100644 Binary files a/CollapseLauncher/Assets/Fonts/FontAwesomeRegular6.otf and b/CollapseLauncher/Assets/Fonts/FontAwesomeRegular6.otf differ diff --git a/CollapseLauncher/Assets/Fonts/FontAwesomeSolid6.otf b/CollapseLauncher/Assets/Fonts/FontAwesomeSolid6.otf index 6bd19b13b..24ca1b913 100644 Binary files a/CollapseLauncher/Assets/Fonts/FontAwesomeSolid6.otf and b/CollapseLauncher/Assets/Fonts/FontAwesomeSolid6.otf differ diff --git a/CollapseLauncher/Assets/Images/GenshinHDRCalibrationScene.jxr b/CollapseLauncher/Assets/Images/GenshinHDRCalibration/Scene.jxr similarity index 100% rename from CollapseLauncher/Assets/Images/GenshinHDRCalibrationScene.jxr rename to CollapseLauncher/Assets/Images/GenshinHDRCalibration/Scene.jxr diff --git a/CollapseLauncher/Assets/Images/GenshinHDRCalibrationSign.png b/CollapseLauncher/Assets/Images/GenshinHDRCalibration/Sign.png similarity index 100% rename from CollapseLauncher/Assets/Images/GenshinHDRCalibrationSign.png rename to CollapseLauncher/Assets/Images/GenshinHDRCalibration/Sign.png diff --git a/CollapseLauncher/Assets/Images/GenshinHDRCalibration/UI.jxr b/CollapseLauncher/Assets/Images/GenshinHDRCalibration/UI.jxr new file mode 100644 index 000000000..bb5e4fbc8 Binary files /dev/null and b/CollapseLauncher/Assets/Images/GenshinHDRCalibration/UI.jxr differ diff --git a/CollapseLauncher/Assets/Images/GenshinHDRCalibrationUI.jxr b/CollapseLauncher/Assets/Images/GenshinHDRCalibrationUI.jxr deleted file mode 100644 index c048cae43..000000000 Binary files a/CollapseLauncher/Assets/Images/GenshinHDRCalibrationUI.jxr and /dev/null differ diff --git a/CollapseLauncher/Assets/Images/ImageCropperOverlay/normal.png b/CollapseLauncher/Assets/Images/ImageCropperOverlay/normal.png index 4b5a6b015..def007b66 100644 Binary files a/CollapseLauncher/Assets/Images/ImageCropperOverlay/normal.png and b/CollapseLauncher/Assets/Images/ImageCropperOverlay/normal.png differ diff --git a/CollapseLauncher/Assets/Images/ImageCropperOverlay/small.png b/CollapseLauncher/Assets/Images/ImageCropperOverlay/small.png index 23fbe96be..123af6170 100644 Binary files a/CollapseLauncher/Assets/Images/ImageCropperOverlay/small.png and b/CollapseLauncher/Assets/Images/ImageCropperOverlay/small.png differ diff --git a/CollapseLauncher/Assets/Presets/CommunityTools.json b/CollapseLauncher/Assets/Presets/CommunityTools.json deleted file mode 100644 index 9359c086a..000000000 --- a/CollapseLauncher/Assets/Presets/CommunityTools.json +++ /dev/null @@ -1,361 +0,0 @@ -{ - "OfficialToolsDictionary": { - "Honkai": [ - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Daily Check-in", - "URL": "https://act.hoyolab.com/bbs/event/signin-bh3/index.html?act_id=e202110291205111", - "Profiles": ["Hi3SEA", "Hi3Global", "Hi3TW", "Hi3KR", "Hi3JP"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "HoYoLab Website", - "URL": "https://www.hoyolab.com", - "Profiles": ["Hi3SEA", "Hi3Global", "Hi3TW", "Hi3KR", "Hi3JP"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Honkai Impact 3rd Wiki", - "URL": "https://honkaiimpact3.fandom.com", - "Profiles": ["Hi3SEA", "Hi3Global", "Hi3TW", "Hi3KR", "Hi3JP"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Battle Chronicle", - "URL": "https://act.hoyolab.com/app/community-game-records-sea/index.html?gid=1", - "Profiles": ["Hi3SEA", "Hi3Global", "Hi3TW", "Hi3KR", "Hi3JP"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "米游社", - "URL": "https://www.miyoushe.com/bh3/", - "Profiles": ["Hi3CN"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "官方 Wiki", - "URL": "https://bbs.mihoyo.com/bh3/wiki/", - "Profiles": ["Hi3CN"] - } - ], - "Genshin": [ - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Daily Check-in", - "URL": "https://act.hoyolab.com/ys/event/signin-sea-v3/index.html?act_id=e202102251931481&hyl_auth_required=true&hyl_presentation_style=fullscreen&utm_source=hoyolab&utm_medium=tools&lang=en-us&bbs_theme=light&bbs_theme_device=1", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "HoYoLab Website", - "URL": "https://www.hoyolab.com", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Genshin Impact Wiki", - "URL": "https://wiki.hoyolab.com/pc/genshin/home", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Redeem Codes", - "URL": "https://genshin.hoyoverse.com/gift", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Battle Chronicle", - "URL": "https://act.hoyolab.com/app/community-game-records-sea/index.html?gid=2", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "米游社", - "URL": "https://www.miyoushe.com/ys/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "观测枢·攻略", - "URL": "https://bbs.mihoyo.com/ys/strategy/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "观测枢·Wiki", - "URL": "https://bbs.mihoyo.com/ys/obc/", - "Profiles": ["GICN", "GICNBilibili"] - } - ], - "StarRail": [ - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Daily Check-in", - "URL": "https://act.hoyolab.com/bbs/event/signin/hkrpg/index.html?act_id=e202303301540311", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "HoYoLab Website", - "URL": "https://www.hoyolab.com", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Honkai: Star Rail Wiki", - "URL": "https://wiki.hoyolab.com/pc/hsr/home", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Redeem Codes", - "URL": "https://hsr.hoyoverse.com/gift", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Battle Chronicle", - "URL": "https://act.hoyolab.com/app/community-game-records-sea/index.html?gid=6", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "米游社", - "URL": "https://www.miyoushe.com/sr/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "官方 Wiki", - "URL": "https://bbs.mihoyo.com/sr/wiki/", - "Profiles": ["SRCN", "HSRCNBilibili"] - } - ] - }, - "CommunityToolsDictionary": { - "Honkai": [ - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Reddit Community", - "URL": "https://www.reddit.com/r/HonkaiImpact3rd", - "Profiles": ["Hi3SEA", "Hi3Global", "Hi3TW", "Hi3KR", "Hi3JP"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "ER Build Guide", - "URL": "https://risbi0.github.io/Elysian-Realm", - "Profiles": ["Hi3SEA", "Hi3Global", "Hi3TW", "Hi3KR", "Hi3JP"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeBrand6.otf#Font Awesome 6 Brands", - "IconGlyph": "", - "Text": "BWIKI", - "URL": "https://wiki.biligame.com/bh3/", - "Profiles": ["Hi3CN"] - } - ], - "Genshin": [ - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "paimon.moe", - "URL": "https://paimon.moe", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Enka Network", - "URL": "https://enka.network", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "seelie.me", - "URL": "https://seelie.me", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Genshin Optimizer", - "URL": "https://frzyc.github.io/genshin-optimizer", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Inventory Kamera", - "URL": "https://github.com/Andrewthe13th/Inventory_Kamera$OpenExternalApp:ApplicationName=Inventory Kamera,ApplicationExecName=InventoryKamera.exe,RunAsAdmin=true$OpenUrlIfCancel", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Kongying Map", - "URL": "https://yuanshen.site/docs/en/$OpenExternalApp:ApplicationName=Kongying Map,ApplicationExecName=Map.exe,RunAsAdmin=true$OpenUrlIfCancel", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "akasha.cv", - "URL": "https://akasha.cv", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Genshin Configurator", - "URL": "https://github.com/Myp3a/GenshinConfigurator/$OpenExternalApp:ApplicationName=Genshin Impact Configurator,ApplicationExecName=GenshinConfigurator*.exe,RunAsAdmin=true$OpenUrlIfCancel", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Guides by KQM", - "URL": "https://keqingmains.com", - "Profiles": ["GIGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "非小酋", - "URL": "https://feixiaoqiu.com/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "椰羊", - "URL": "https://cocogoat.work/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "莫娜占卜铺", - "URL": "https://mona-uranai.com/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "胡桃工具箱", - "URL": "https://hut.ao/zh/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeBrand6.otf#Font Awesome 6 Brands", - "IconGlyph": "", - "Text": "BWIKI", - "URL": "https://wiki.biligame.com/ys/", - "Profiles": ["GICN", "GICNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "爱丽丝工坊", - "URL": "https://genshin.kchlu.com/", - "Profiles": ["GICN", "GICNBilibili"] - } - ], - "StarRail": [ - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "pom.moe", - "URL": "https://pom.moe/", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Project Yatta (HSR)", - "URL": "https://hsr.yatta.top/", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Star Rail Station", - "URL": "https://starrailstation.com/", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "prydwen.gg", - "URL": "https://www.prydwen.gg/star-rail/", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Guides by KQM", - "URL": "https://hsr.keqingmains.com", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "\uE0BB", - "Text": "Pokke's Library", - "URL": "https://pokkelibrary.com/", - "Profiles": ["SRGlb"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "非小酋", - "URL": "https://feixiaoqiu.com/", - "Profiles": ["SRCN", "HSRCNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeBrand6.otf#Font Awesome 6 Brands", - "IconGlyph": "", - "Text": "BWIKI", - "URL": "https://wiki.biligame.com/sr/", - "Profiles": ["SRCN", "HSRCNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "爱丽丝工坊", - "URL": "https://starrail.kchlu.com/", - "Profiles": ["SRCN", "HSRCNBilibili"] - }, - { - "IconFontFamily": "ms-appx:///Assets/Fonts/FontAwesomeSolid6.otf#Font Awesome 6 Free Solid", - "IconGlyph": "", - "Text": "Enka Network", - "URL": "https://enka.network/?hsr", - "Profiles": ["SRGlb"] - } - ] - } -} diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/DownloadIcon.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/DownloadIcon.cs index d9ac202ef..59d0c86ba 100644 --- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/DownloadIcon.cs +++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/DownloadIcon.cs @@ -59,7 +59,7 @@ namespace CollapseLauncher.AnimatedVisuals.Lottie // Frame rate: 60 fps // Frame count: 300 // Duration: 5000.0 mS - sealed class DownloadIcon + sealed partial class DownloadIcon : Microsoft.UI.Xaml.Controls.IAnimatedVisualSource , Microsoft.UI.Xaml.Controls.IAnimatedVisualSource2 { @@ -132,7 +132,7 @@ public void SetScalarProperty(string propertyName, double value) { } - sealed class DownloadIcon_AnimatedVisual + sealed partial class DownloadIcon_AnimatedVisual : Microsoft.UI.Xaml.Controls.IAnimatedVisual , Microsoft.UI.Xaml.Controls.IAnimatedVisual2 { diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/LoadingSprite.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/LoadingSprite.cs index 2251c0002..3d1e74178 100644 --- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/LoadingSprite.cs +++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/LoadingSprite.cs @@ -55,7 +55,7 @@ namespace CollapseLauncher.AnimatedVisuals.Lottie // Frame rate: 60 fps // Frame count: 180 // Duration: 3000.0 mS - sealed class LoadingSprite + sealed partial class LoadingSprite : Microsoft.UI.Xaml.Controls.IAnimatedVisualSource , Microsoft.UI.Xaml.Controls.IAnimatedVisualSource2 { @@ -128,7 +128,7 @@ public void SetScalarProperty(string propertyName, double value) { } - sealed class LoadingSprite_AnimatedVisual + sealed partial class LoadingSprite_AnimatedVisual : Microsoft.UI.Xaml.Controls.IAnimatedVisual , Microsoft.UI.Xaml.Controls.IAnimatedVisual2 { diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs index 1b330cab3..8375f311b 100644 --- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs +++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs @@ -64,7 +64,7 @@ namespace CollapseLauncher.AnimatedVisuals.Lottie // Frame rate: 60 fps // Frame count: 600 // Duration: 10000.0 mS - sealed class NewLogoTitleIntro + sealed partial class NewLogoTitleIntro : Microsoft.UI.Xaml.Controls.IAnimatedVisualSource , Microsoft.UI.Xaml.Controls.IAnimatedVisualSource2 , Microsoft.UI.Xaml.Controls.IDynamicAnimatedVisualSource @@ -232,7 +232,7 @@ public void SetScalarProperty(string propertyName, double value) { } - sealed class NewLogoTitleIntro_AnimatedVisual + sealed partial class NewLogoTitleIntro_AnimatedVisual : Microsoft.UI.Xaml.Controls.IAnimatedVisual , Microsoft.UI.Xaml.Controls.IAnimatedVisual2 { diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/StartGameIcon.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/StartGameIcon.cs index f59dd3bbe..8478138ac 100644 --- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/StartGameIcon.cs +++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/StartGameIcon.cs @@ -55,7 +55,7 @@ namespace CollapseLauncher.AnimatedVisuals.Lottie // Frame rate: 60 fps // Frame count: 40 // Duration: 666.7 mS - sealed class StartGameIcon + sealed partial class StartGameIcon : Microsoft.UI.Xaml.Controls.IAnimatedVisualSource , Microsoft.UI.Xaml.Controls.IAnimatedVisualSource2 { @@ -128,7 +128,7 @@ public void SetScalarProperty(string propertyName, double value) { } - sealed class StartGameIcon_AnimatedVisual + sealed partial class StartGameIcon_AnimatedVisual : Microsoft.UI.Xaml.Controls.IAnimatedVisual , Microsoft.UI.Xaml.Controls.IAnimatedVisual2 { diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/UpdateIcon.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/UpdateIcon.cs index 95fab675b..f41006052 100644 --- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/UpdateIcon.cs +++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/UpdateIcon.cs @@ -56,7 +56,7 @@ namespace CollapseLauncher.AnimatedVisuals.Lottie // Frame rate: 60 fps // Frame count: 240 // Duration: 4000.0 mS - sealed class UpdateIcon + sealed partial class UpdateIcon : Microsoft.UI.Xaml.Controls.IAnimatedVisualSource , Microsoft.UI.Xaml.Controls.IAnimatedVisualSource2 { @@ -129,7 +129,7 @@ public void SetScalarProperty(string propertyName, double value) { } - sealed class UpdateIcon_AnimatedVisual + sealed partial class UpdateIcon_AnimatedVisual : Microsoft.UI.Xaml.Controls.IAnimatedVisual , Microsoft.UI.Xaml.Controls.IAnimatedVisual2 { diff --git a/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs b/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs index f84772a4b..0339742aa 100644 --- a/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs +++ b/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs @@ -52,10 +52,6 @@ private async Task> Check(List assetIndex, Cancella { throw ex.Flatten().InnerExceptions.First(); } - catch (Exception) - { - throw; - } // Return the asset index return returnAsset; @@ -135,16 +131,14 @@ private async ValueTask CheckAsset(CacheAsset asset, List returnAsse } // If above passes, then run the CRC check - using (FileStream fs = new FileStream(asset.ConcatPath, FileMode.Open, FileAccess.Read, FileShare.None, _bufferBigLength)) - { - // Calculate the asset CRC (SHA1) - byte[] hashArray = await CheckHashAsync(fs, new HMACSHA1(_gameSalt!), token); + await using FileStream fs = await NaivelyOpenFileStreamAsync(fileInfo, FileMode.Open, FileAccess.Read, FileShare.Read); + // Calculate the asset CRC (SHA1) + byte[] hashArray = await CheckHashAsync(fs, new HMACSHA1(_gameSalt!), token); - // If the asset CRC doesn't match, then add the file to asset index. - if (!IsArrayMatch(asset.CRCArray, hashArray)) - { - AddGenericCheckAsset(asset, CacheAssetStatus.Obsolete, returnAsset, hashArray, asset.CRCArray); - } + // If the asset CRC doesn't match, then add the file to asset index. + if (!IsArrayMatch(asset.CRCArray, hashArray)) + { + AddGenericCheckAsset(asset, CacheAssetStatus.Obsolete, returnAsset, hashArray, asset.CRCArray); } } diff --git a/CollapseLauncher/Classes/CachesManagement/Honkai/Fetch.cs b/CollapseLauncher/Classes/CachesManagement/Honkai/Fetch.cs index cdc6830a5..2fae2d352 100644 --- a/CollapseLauncher/Classes/CachesManagement/Honkai/Fetch.cs +++ b/CollapseLauncher/Classes/CachesManagement/Honkai/Fetch.cs @@ -25,66 +25,56 @@ internal partial class HonkaiCache private async Task> Fetch(CancellationToken token) { // Initialize asset index for the return - List returnAsset = new(); + List returnAsset = []; // Initialize new proxy-aware HttpClient using HttpClient httpClientNew = new HttpClientBuilder() - .UseLauncherConfig() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) .SetUserAgent(_userAgent) .SetAllowedDecompression(DecompressionMethods.None) .Create(); - // Use HttpClient instance on fetching - using Http httpClient = new Http(true, 5, 1000, _userAgent, httpClientNew); - try - { - // Subscribe the event listener - httpClient.DownloadProgress += _httpClient_FetchAssetProgress; + // Use a new DownloadClient for fetching + DownloadClient downloadClient = DownloadClient.CreateInstance(httpClientNew); - // Build _gameRepoURL from loading Dispatcher and Gateway - await BuildGameRepoURL(token); + // Build _gameRepoURL from loading Dispatcher and Gateway + await BuildGameRepoURL(downloadClient, token); - // Iterate type and do fetch - foreach (CacheAssetType type in Enum.GetValues()) + // Iterate type and do fetch + foreach (CacheAssetType type in Enum.GetValues()) + { + // Skip for unused type + switch (type) { - // Skip for unused type - switch (type) - { - case CacheAssetType.Unused: - case CacheAssetType.Dispatcher: - case CacheAssetType.Gateway: - case CacheAssetType.General: - case CacheAssetType.IFix: - case CacheAssetType.DesignData: - case CacheAssetType.Lua: - continue; - } + case CacheAssetType.Unused: + case CacheAssetType.Dispatcher: + case CacheAssetType.Gateway: + case CacheAssetType.General: + case CacheAssetType.IFix: + case CacheAssetType.DesignData: + case CacheAssetType.Lua: + continue; + } - // uint = Count of the assets available - // long = Total size of the assets available - (int, long) count = await FetchByType(type, httpClient, returnAsset, token); + // uint = Count of the assets available + // long = Total size of the assets available + (int, long) count = await FetchByType(type, downloadClient, returnAsset, token); - // Write a log about the metadata - LogWriteLine($"Cache Metadata [T: {type}]:", LogType.Default, true); - LogWriteLine($" Cache Count = {count.Item1}", LogType.NoTag, true); - LogWriteLine($" Cache Size = {SummarizeSizeSimple(count.Item2)}", LogType.NoTag, true); + // Write a log about the metadata + LogWriteLine($"Cache Metadata [T: {type}]:", LogType.Default, true); + LogWriteLine($" Cache Count = {count.Item1}", LogType.NoTag, true); + LogWriteLine($" Cache Size = {SummarizeSizeSimple(count.Item2)}", LogType.NoTag, true); - // Increment the Total Size and Count - _progressAllCountTotal += count.Item1; - _progressAllSizeTotal += count.Item2; - } - } - finally - { - // Unsubscribe the event listener and dispose Http client - httpClient.DownloadProgress -= _httpClient_FetchAssetProgress; + // Increment the Total Size and Count + _progressAllCountTotal += count.Item1; + _progressAllSizeTotal += count.Item2; } // Return asset index return returnAsset; } - private async Task BuildGameRepoURL(CancellationToken token) + private async Task BuildGameRepoURL(DownloadClient downloadClient, CancellationToken token) { KianaDispatch dispatch = null; Exception lastException = null; @@ -93,7 +83,7 @@ private async Task BuildGameRepoURL(CancellationToken token) { try { - // Init the key and decrypt it if exist. + // Init the key and decrypt it if existed. if (string.IsNullOrEmpty(_gameVersionManager.GamePreset.DispatcherKey)) { throw new NullReferenceException("Dispatcher key is null or empty!"); @@ -102,7 +92,7 @@ private async Task BuildGameRepoURL(CancellationToken token) string key = _gameVersionManager.GamePreset.DispatcherKey; // Try assign dispatcher - dispatch = await KianaDispatch.GetDispatch(baseURL, + dispatch = await KianaDispatch.GetDispatch(downloadClient, baseURL, _gameVersionManager.GamePreset.GameDispatchURLTemplate, _gameVersionManager.GamePreset.GameDispatchChannelName, key, _gameVersion.VersionArray, token); @@ -120,13 +110,13 @@ private async Task BuildGameRepoURL(CancellationToken token) // Get gatewayURl and fetch the gateway _gameGateway = - await KianaDispatch.GetGameserver(dispatch!, _gameVersionManager.GamePreset.GameGatewayDefault!, token); + await KianaDispatch.GetGameserver(downloadClient, dispatch!, _gameVersionManager.GamePreset.GameGatewayDefault!, token); _gameRepoURL = BuildAssetBundleURL(_gameGateway); } - private string BuildAssetBundleURL(KianaDispatch gateway) => CombineURLFromString(gateway!.AssetBundleUrls![0], "/{0}/editor_compressed/"); + private static string BuildAssetBundleURL(KianaDispatch gateway) => CombineURLFromString(gateway!.AssetBundleUrls![0], "/{0}/editor_compressed/"); - private async Task<(int, long)> FetchByType(CacheAssetType type, Http httpClient, List assetIndex, CancellationToken token) + private async Task<(int, long)> FetchByType(CacheAssetType type, DownloadClient downloadClient, List assetIndex, CancellationToken token) { // Set total activity string as "Fetching Caches Type: " _status!.ActivityStatus = string.Format(Lang!._CachesPage!.CachesStatusFetchingType!, type); @@ -145,9 +135,9 @@ private async Task BuildGameRepoURL(CancellationToken token) // Get a direct HTTP Stream await using HttpResponseInputStream remoteStream = await HttpResponseInputStream.CreateStreamAsync( - httpClient.GetHttpClient(), assetIndexURL, null, null, token); + downloadClient.GetHttpClient(), assetIndexURL, null, null, null, null, null, token); - using XORStream stream = new XORStream(remoteStream); + await using XORStream stream = new XORStream(remoteStream); // Build the asset index and return the count and size of each type (int, long) returnValue = await BuildAssetIndex(type, baseURL, stream, assetIndex, token); @@ -189,7 +179,7 @@ private IEnumerable EnumerateCacheTextAsset(CacheAssetType type, IEn // If isFirst flag set to true, then get the _gameSalt. if (isFirst) { - _gameSalt = GetAssetIndexSalt(line.ToString()); + _gameSalt = GetAssetIndexSalt(line); isFirst = false; continue; } @@ -210,11 +200,11 @@ private IEnumerable EnumerateCacheTextAsset(CacheAssetType type, IEn continue; } - CacheAsset content = null; + CacheAsset content; try { // Deserialize the line and set the type - content = line.Deserialize(InternalAppJSONContext.Default); + content = line.Deserialize(InternalAppJSONContext.Default.CacheAsset); } catch (Exception ex) { @@ -248,19 +238,25 @@ private IEnumerable EnumerateCacheTextAsset(CacheAssetType type, IEn // Set isFirst flag as true if type is Data and // also convert type as lowered string. - bool isFirst = type == CacheAssetType.Data; - bool isNeedReadLuckyNumber = type == CacheAssetType.Data; + + // Unused as of Aug 4th 2024, bonk @bagusnl if not true + // bool isFirst = type == CacheAssetType.Data; + // bool isNeedReadLuckyNumber = type == CacheAssetType.Data; // Parse asset index file from UABT BundleFile bundleFile = new BundleFile(stream); SerializedFile serializeFile = new SerializedFile(bundleFile.fileList!.FirstOrDefault()!.stream); - // Try get the asset index file as byte[] and load it as TextAsset + // Try to get the asset index file as byte[] and load it as TextAsset byte[] dataRaw = serializeFile.GetDataFirstOrDefaultByName("packageversion.txt"); TextAsset dataTextAsset = new TextAsset(dataRaw); // Initialize local HTTP client - using HttpClient client = new HttpClient(new HttpClientHandler { MaxConnectionsPerServer = _threadCount }); + using HttpClient client = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) + .SetUserAgent(_userAgent) + .SetAllowedDecompression(DecompressionMethods.None) + .Create(); // Iterate lines of the TextAsset in parallel await Parallel.ForEachAsync(EnumerateCacheTextAsset(type, dataTextAsset.GetStringList(), baseURL), @@ -333,16 +329,16 @@ private bool IsValidRegionFile(string input, string lang) public KianaDispatch GetCurrentGateway() => _gameGateway; public async Task<(List, string, string, int)> GetCacheAssetList( - Http httpClient, CacheAssetType type, CancellationToken token) + DownloadClient downloadClient, CacheAssetType type, CancellationToken token) { // Initialize asset index for the return List returnAsset = new(); // Build _gameRepoURL from loading Dispatcher and Gateway - await BuildGameRepoURL(token); + await BuildGameRepoURL(downloadClient, token); // Fetch the progress - _ = await FetchByType(type, httpClient, returnAsset, token); + _ = await FetchByType(type, downloadClient, returnAsset, token); // Return the list and base asset bundle repo URL return (returnAsset, _gameGateway!.ExternalAssetUrls!.FirstOrDefault(), BuildAssetBundleURL(_gameGateway), diff --git a/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs b/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs index 417fba5d9..b54a56456 100644 --- a/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs +++ b/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs @@ -1,10 +1,10 @@ -using CollapseLauncher.Helper; +using CollapseLauncher.Helper; using CollapseLauncher.Interfaces; using Hi3Helper; -using Hi3Helper.Data; using Hi3Helper.Http; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Net; using System.Net.Http; @@ -20,32 +20,51 @@ internal partial class HonkaiCache private async Task Update(List updateAssetIndex, List assetIndex, CancellationToken token) { // Initialize new proxy-aware HttpClient - using HttpClient httpClientNew = new HttpClientBuilder() - .UseLauncherConfig() + using HttpClient client = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) .SetUserAgent(_userAgent) .SetAllowedDecompression(DecompressionMethods.None) .Create(); - // Assign Http client - using Http httpClient = new Http(true, 5, 1000, _userAgent, httpClientNew); + // Use the new DownloadClient instance + DownloadClient downloadClient = DownloadClient.CreateInstance(client); try { // Set IsProgressAllIndetermined as false and update the status _status!.IsProgressAllIndetermined = true; UpdateStatus(); - // Subscribe the event listener - httpClient.DownloadProgress += _httpClient_UpdateAssetProgress; // Iterate the asset index and do update operation - foreach (CacheAsset asset in -#if ENABLEHTTPREPAIR - EnforceHTTPSchemeToAssetIndex(updateAssetIndex!) + ObservableCollection assetProperty = new ObservableCollection(AssetEntry); + if (_isBurstDownloadEnabled) + { + await Parallel.ForEachAsync( + PairEnumeratePropertyAndAssetIndexPackage( +#if ENABLEHTTPREPAIR + EnforceHTTPSchemeToAssetIndex(updateAssetIndex) #else - updateAssetIndex! + updateAssetIndex #endif - ) + , assetProperty), + new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = _downloadThreadCount }, + async (asset, innerToken) => + { + await UpdateCacheAsset(asset, downloadClient, _httpClient_UpdateAssetProgress, innerToken); + }); + } + else { - await UpdateCacheAsset(asset, httpClient, token); + foreach ((CacheAsset, IAssetProperty) asset in + PairEnumeratePropertyAndAssetIndexPackage( +#if ENABLEHTTPREPAIR + EnforceHTTPSchemeToAssetIndex(updateAssetIndex) +#else + updateAssetIndex +#endif + , assetProperty)) + { + await UpdateCacheAsset(asset, downloadClient, _httpClient_UpdateAssetProgress, token); + } } // Reindex the asset index in Verify.txt @@ -60,11 +79,6 @@ private async Task Update(List updateAssetIndex, List assetIndex) @@ -74,31 +88,31 @@ private void UpdateCacheVerifyList(List assetIndex) // Initialize listFile File Stream using (FileStream fs = new FileStream(listFile, FileMode.Create, FileAccess.Write)) - using (StreamWriter sw = new StreamWriter(fs)) - { - // Iterate asset index and generate the path for the cache path - foreach (CacheAsset asset in assetIndex!) + using (StreamWriter sw = new StreamWriter(fs)) { - // Yes, the path is written in this way. Idk why miHoYo did this... - // Update 6.8: They finally notices that they use "//" instead of "/" - string basePath = GetAssetBasePathByType(asset!.DataType)!.Replace('\\', '/'); - string path = basePath + "/" + asset.ConcatN; - sw.WriteLine(path); + // Iterate asset index and generate the path for the cache path + foreach (CacheAsset asset in assetIndex!) + { + // Yes, the path is written in this way. Idk why miHoYo did this... + // Update 6.8: They finally notices that they use "//" instead of "/" + string basePath = GetAssetBasePathByType(asset!.DataType)!.Replace('\\', '/'); + string path = basePath + "/" + asset.ConcatN; + sw.WriteLine(path); + } } - } } - private async Task UpdateCacheAsset(CacheAsset asset, Http httpClient, CancellationToken token) + private async Task UpdateCacheAsset((CacheAsset AssetIndex, IAssetProperty AssetProperty) asset, DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token) { // Increment total count and update the status _progressAllCountCurrent++; - _status!.ActivityStatus = string.Format(Lang!._Misc!.Downloading + " {0}: {1}", asset!.DataType, asset.N); + _status!.ActivityStatus = string.Format(Lang!._Misc!.Downloading + " {0}: {1}", asset!.AssetIndex.DataType, asset.AssetIndex.N); UpdateAll(); // This is a action for Unused asset. - if (asset.DataType == CacheAssetType.Unused) + if (asset.AssetIndex.DataType == CacheAssetType.Unused) { - FileInfo fileInfo = new FileInfo(asset.ConcatPath!); + FileInfo fileInfo = new FileInfo(asset.AssetIndex.ConcatPath!); if (fileInfo.Exists) { fileInfo.IsReadOnly = false; @@ -110,63 +124,19 @@ private async Task UpdateCacheAsset(CacheAsset asset, Http httpClient, Cancellat // Other than unused file, do this action else { - // Assign and check the path of the asset directory - string assetDir = Path.GetDirectoryName(asset.ConcatPath); - if (!Directory.Exists(assetDir)) - { - Directory.CreateDirectory(assetDir!); - } - #if DEBUG - LogWriteLine($"Downloading cache [T: {asset.DataType}]: {asset.N} at URL: {asset.ConcatURL}", LogType.Debug, true); + LogWriteLine($"Downloading cache [T: {asset.AssetIndex.DataType}]: {asset.AssetIndex.N} at URL: {asset.AssetIndex.ConcatURL}", LogType.Debug, true); #endif - // Do multi-session download for asset that has applicable size - if (asset.CS >= _sizeForMultiDownload) - { - await httpClient!.Download(asset.ConcatURL, asset.ConcatPath, _downloadThreadCount, true, token); - await httpClient.Merge(token); - } - // Do single-session download for others - else - { - await httpClient!.Download(asset.ConcatURL, asset.ConcatPath, true, null, null, token); - } + await RunDownloadTask(asset.AssetIndex.CS, asset.AssetIndex.ConcatPath, asset.AssetIndex.ConcatURL, downloadClient, downloadProgress, token); #if !DEBUG - LogWriteLine($"Downloaded cache [T: {asset.DataType}]: {asset.N}", LogType.Default, true); + LogWriteLine($"Downloaded cache [T: {asset.AssetIndex.DataType}]: {asset.AssetIndex.N}", LogType.Default, true); #endif } // Remove Asset Entry display - Dispatch(() => { if (AssetEntry!.Count > 0) AssetEntry.RemoveAt(0); }); - } - - private async void _httpClient_UpdateAssetProgress(object sender, DownloadEvent e) - { - // Update current progress percentages and speed - _progress!.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? - ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal) : - 0; - - if (e!.State != DownloadState.Merging) - { - _progressAllSizeCurrent += e.Read; - } - long speed = (long)(_progressAllSizeCurrent / _stopwatch!.Elapsed.TotalSeconds); - - if (await CheckIfNeedRefreshStopwatch()) - { - // Update current activity status - _status!.IsProgressAllIndetermined = false; - string timeLeftString = string.Format(Lang!._Misc!.TimeRemainHMSFormat!, ((_progressAllSizeCurrent - _progressAllSizeTotal) / ConverterTool.Unzeroed(speed)).ToTimeSpanNormalized()); - _status.ActivityAll = string.Format(Lang!._Misc!.Downloading + ": {0}/{1} ", _progressAllCountCurrent, _progressAllCountTotal) - + string.Format($"({Lang._Misc.SpeedPerSec})", ConverterTool.SummarizeSizeSimple(speed)) - + $" | {timeLeftString}"; - - // Trigger update - UpdateAll(); - } + PopRepairAssetEntry(asset.AssetProperty); } } } diff --git a/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs b/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs index 61f8f47a1..d78c9ea7c 100644 --- a/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs +++ b/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs @@ -66,10 +66,6 @@ private async Task> Check(List assetIndex, CancellationTo { throw ex.Flatten().InnerExceptions.First(); } - catch (Exception) - { - throw; - } // Return the asset index return returnAsset; @@ -108,16 +104,15 @@ private async ValueTask CheckAsset(SRAsset asset, List returnAsset, str } // If above passes, then run the CRC check - using (FileStream fs = new FileStream(asset.LocalName, FileMode.Open, FileAccess.Read, FileShare.None, _bufferBigLength)) - { - // Calculate the asset CRC (MD5) - byte[] hashArray = await CheckHashAsync(fs, MD5.Create(), token); + await using FileStream fs = await NaivelyOpenFileStreamAsync(UsePersistent ? fileInfoPersistent : fileInfoStreaming, + FileMode.Open, FileAccess.Read, FileShare.Read); + // Calculate the asset CRC (MD5) + byte[] hashArray = await CheckHashAsync(fs, MD5.Create(), token); - // If the asset CRC doesn't match, then add the file to asset index. - if (!IsArrayMatch(asset.Hash, hashArray)) - { - AddGenericCheckAsset(asset, CacheAssetStatus.Obsolete, returnAsset, hashArray, asset.Hash); - } + // If the asset CRC doesn't match, then add the file to asset index. + if (!IsArrayMatch(asset.Hash, hashArray)) + { + AddGenericCheckAsset(asset, CacheAssetStatus.Obsolete, returnAsset, hashArray, asset.Hash); } } diff --git a/CollapseLauncher/Classes/CachesManagement/StarRail/Fetch.cs b/CollapseLauncher/Classes/CachesManagement/StarRail/Fetch.cs index 68e83db65..8fe493f45 100644 --- a/CollapseLauncher/Classes/CachesManagement/StarRail/Fetch.cs +++ b/CollapseLauncher/Classes/CachesManagement/StarRail/Fetch.cs @@ -1,8 +1,12 @@ -using Hi3Helper; +using CollapseLauncher.Helper; +using Hi3Helper; using Hi3Helper.EncTool.Parser.AssetMetadata.SRMetadataAsset; +using Hi3Helper.Http; using System; using System.Collections.Generic; using System.IO; +using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,59 +24,58 @@ private async Task> Fetch(CancellationToken token) // Initialize asset index for the return List returnAsset = new List(); - try - { - // Subscribe the event listener - _innerGameVersionManager!.StarRailMetadataTool!.HttpEvent += _httpClient_FetchAssetProgress; + // Initialize new proxy-aware HttpClient + using HttpClient client = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) + .SetUserAgent(_userAgent) + .SetAllowedDecompression(DecompressionMethods.None) + .Create(); + + // Initialize the new DownloadClient + DownloadClient downloadClient = DownloadClient.CreateInstance(client); - // Initialize metadata - // Set total activity string as "Fetching Caches Type: Dispatcher" - _status!.ActivityStatus = string.Format(Lang!._CachesPage!.CachesStatusFetchingType!, CacheAssetType.Dispatcher); - _status!.IsProgressAllIndetermined = true; - _status!.IsIncludePerFileIndicator = false; - UpdateStatus(); + // Initialize metadata + // Set total activity string as "Fetching Caches Type: Dispatcher" + _status!.ActivityStatus = string.Format(Lang!._CachesPage!.CachesStatusFetchingType!, CacheAssetType.Dispatcher); + _status!.IsProgressAllIndetermined = true; + _status!.IsIncludePerFileIndicator = false; + UpdateStatus(); - if (!await _innerGameVersionManager!.StarRailMetadataTool.Initialize(token, GetExistingGameRegionID(), Path.Combine(_gamePath!, $"{Path.GetFileNameWithoutExtension(_gameVersionManager!.GamePreset!.GameExecutableName)}_Data\\Persistent"))) - throw new InvalidDataException("The dispatcher response is invalid! Please open an issue to our GitHub page to report this issue."); + if (!await _innerGameVersionManager!.StarRailMetadataTool.Initialize(token, downloadClient, _httpClient_FetchAssetProgress, GetExistingGameRegionID(), Path.Combine(_gamePath!, $"{Path.GetFileNameWithoutExtension(_gameVersionManager!.GamePreset!.GameExecutableName)}_Data\\Persistent"))) + throw new InvalidDataException("The dispatcher response is invalid! Please open an issue to our GitHub page to report this issue."); - // Iterate type and do fetch - foreach (SRAssetType type in Enum.GetValues()) + // Iterate type and do fetch + await Parallel.ForEachAsync(Enum.GetValues(), token, async (type, innerCancelToken) => + { + // Skip for unused type + switch (type) { - // Skip for unused type - switch (type) - { - case SRAssetType.Audio: - case SRAssetType.Video: - case SRAssetType.Block: - case SRAssetType.Asb: - continue; - } - - // uint = Count of the assets available - // long = Total size of the assets available - (int, long) count = await FetchByType(type, returnAsset, token); - - // Write a log about the metadata - LogWriteLine($"Cache Metadata [T: {type}]:", LogType.Default, true); - LogWriteLine($" Cache Count = {count.Item1}", LogType.NoTag, true); - LogWriteLine($" Cache Size = {SummarizeSizeSimple(count.Item2)}", LogType.NoTag, true); - - // Increment the Total Size and Count - _progressAllCountTotal += count.Item1; - _progressAllSizeTotal += count.Item2; + case SRAssetType.Audio: + case SRAssetType.Video: + case SRAssetType.Block: + case SRAssetType.Asb: + return; } - } - finally - { - // Unsubscribe the event listener and dispose Http client - _innerGameVersionManager!.StarRailMetadataTool!.HttpEvent -= _httpClient_FetchAssetProgress; - } + + // uint = Count of the assets available + // long = Total size of the assets available + (int, long) count = await FetchByType(downloadClient, _httpClient_FetchAssetProgress, type, returnAsset, innerCancelToken); + + // Write a log about the metadata + LogWriteLine($"Cache Metadata [T: {type}]:", LogType.Default, true); + LogWriteLine($" Cache Count = {count.Item1}", LogType.NoTag, true); + LogWriteLine($" Cache Size = {SummarizeSizeSimple(count.Item2)}", LogType.NoTag, true); + + // Increment the Total Size and Count + Interlocked.Add(ref _progressAllCountTotal, count.Item1); + Interlocked.Add(ref _progressAllSizeTotal, count.Item2); + }).ConfigureAwait(false); // Return asset index return returnAsset; } - private async Task<(int, long)> FetchByType(SRAssetType type, List assetIndex, CancellationToken token) + private async Task<(int, long)> FetchByType(DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, SRAssetType type, List assetIndex, CancellationToken token) { // Set total activity string as "Fetching Caches Type: " _status!.ActivityStatus = string.Format(Lang!._CachesPage!.CachesStatusFetchingType!, type); @@ -80,32 +83,28 @@ private async Task> Fetch(CancellationToken token) _status!.IsIncludePerFileIndicator = false; UpdateStatus(); - try + // Start reading the metadata and build the asset index of each type + SRAssetProperty assetProperty; + switch (type) { - // Start reading the metadata and build the asset index of each type - SRAssetProperty assetProperty; - switch (type) - { - case SRAssetType.IFix: - await _innerGameVersionManager!.StarRailMetadataTool!.ReadIFixMetadataInformation(token); - assetProperty = _innerGameVersionManager!.StarRailMetadataTool!.MetadataIFix!.GetAssets(); - assetIndex!.AddRange(assetProperty!.AssetList!); - return (assetProperty.AssetList.Count, assetProperty.AssetTotalSize); - case SRAssetType.DesignData: - await _innerGameVersionManager!.StarRailMetadataTool!.ReadDesignMetadataInformation(token); - assetProperty = _innerGameVersionManager.StarRailMetadataTool.MetadataDesign!.GetAssets(); - assetIndex!.AddRange(assetProperty!.AssetList!); - return (assetProperty.AssetList.Count, assetProperty.AssetTotalSize); - case SRAssetType.Lua: - await _innerGameVersionManager!.StarRailMetadataTool!.ReadLuaMetadataInformation(token); - assetProperty = _innerGameVersionManager.StarRailMetadataTool.MetadataLua!.GetAssets(); - assetIndex!.AddRange(assetProperty!.AssetList!); - return (assetProperty.AssetList.Count, assetProperty.AssetTotalSize); - } - - return (0, 0); + case SRAssetType.IFix: + await _innerGameVersionManager!.StarRailMetadataTool!.ReadIFixMetadataInformation(downloadClient, downloadProgress, token); + assetProperty = _innerGameVersionManager!.StarRailMetadataTool!.MetadataIFix!.GetAssets(); + assetIndex!.AddRange(assetProperty!.AssetList!); + return (assetProperty.AssetList.Count, assetProperty.AssetTotalSize); + case SRAssetType.DesignData: + await _innerGameVersionManager!.StarRailMetadataTool!.ReadDesignMetadataInformation(downloadClient, downloadProgress, token); + assetProperty = _innerGameVersionManager.StarRailMetadataTool.MetadataDesign!.GetAssets(); + assetIndex!.AddRange(assetProperty!.AssetList!); + return (assetProperty.AssetList.Count, assetProperty.AssetTotalSize); + case SRAssetType.Lua: + await _innerGameVersionManager!.StarRailMetadataTool!.ReadLuaMetadataInformation(downloadClient, downloadProgress, token); + assetProperty = _innerGameVersionManager.StarRailMetadataTool.MetadataLua!.GetAssets(); + assetIndex!.AddRange(assetProperty!.AssetList!); + return (assetProperty.AssetList.Count, assetProperty.AssetTotalSize); } - catch { throw; } + + return (0, 0); } #region Utilities diff --git a/CollapseLauncher/Classes/CachesManagement/StarRail/Update.cs b/CollapseLauncher/Classes/CachesManagement/StarRail/Update.cs index 41498405c..3ef249cce 100644 --- a/CollapseLauncher/Classes/CachesManagement/StarRail/Update.cs +++ b/CollapseLauncher/Classes/CachesManagement/StarRail/Update.cs @@ -1,10 +1,10 @@ using CollapseLauncher.Helper; using Hi3Helper; -using Hi3Helper.Data; using Hi3Helper.EncTool.Parser.AssetMetadata.SRMetadataAsset; using Hi3Helper.Http; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; @@ -23,33 +23,51 @@ internal partial class StarRailCache private async Task Update(List updateAssetIndex, List assetIndex, CancellationToken token) { // Initialize new proxy-aware HttpClient - using HttpClient httpClientNew = new HttpClientBuilder() - .UseLauncherConfig() + using HttpClient client = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) .SetUserAgent(_userAgent) .SetAllowedDecompression(DecompressionMethods.None) .Create(); - // Assign Http client - Http httpClient = new Http(true, 5, 1000, _userAgent, httpClientNew); + // Assign DownloadClient + DownloadClient downloadClient = DownloadClient.CreateInstance(client); try { // Set IsProgressAllIndetermined as false and update the status _status.IsProgressAllIndetermined = true; UpdateStatus(); - // Subscribe the event listener - httpClient.DownloadProgress += _httpClient_UpdateAssetProgress; - // Iterate the asset index and do update operation - foreach (SRAsset asset in -#if ENABLEHTTPREPAIR - EnforceHTTPSchemeToAssetIndex(updateAssetIndex!) + ObservableCollection assetProperty = new ObservableCollection(AssetEntry); + if (_isBurstDownloadEnabled) + { + await Parallel.ForEachAsync( + PairEnumeratePropertyAndAssetIndexPackage( +#if ENABLEHTTPREPAIR + EnforceHTTPSchemeToAssetIndex(updateAssetIndex) #else - updateAssetIndex! + updateAssetIndex #endif - ) + , assetProperty), + new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = _downloadThreadCount }, + async (asset, innerToken) => + { + await UpdateCacheAsset(asset, downloadClient, _httpClient_UpdateAssetProgress, innerToken); + }); + } + else { - await UpdateCacheAsset(asset, httpClient, token); + foreach ((SRAsset, IAssetProperty) asset in + PairEnumeratePropertyAndAssetIndexPackage( +#if ENABLEHTTPREPAIR + EnforceHTTPSchemeToAssetIndex(updateAssetIndex) +#else + updateAssetIndex +#endif + , assetProperty)) + { + await UpdateCacheAsset(asset, downloadClient, _httpClient_UpdateAssetProgress, token); + } } return true; @@ -61,72 +79,21 @@ private async Task Update(List updateAssetIndex, List as LogWriteLine($"An error occured while updating cache file!\r\n{ex}", LogType.Error, true); throw; } - finally - { - // Unsubscribe the event listener and dispose Http client - httpClient.DownloadProgress -= _httpClient_UpdateAssetProgress; - httpClient.Dispose(); - } } - private async Task UpdateCacheAsset(SRAsset asset, Http httpClient, CancellationToken token) + private async Task UpdateCacheAsset((SRAsset AssetIndex, IAssetProperty AssetProperty) asset, DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token) { // Increment total count and update the status _progressAllCountCurrent++; - _status.ActivityStatus = string.Format(Lang._Misc.Downloading + " {0}: {1}", asset.AssetType, Path.GetFileName(asset.LocalName)); + _status.ActivityStatus = string.Format(Lang._Misc.Downloading + " {0}: {1}", asset.AssetIndex.AssetType, Path.GetFileName(asset.AssetIndex.LocalName)); UpdateAll(); - // Assign and check the path of the asset directory - string assetDir = Path.GetDirectoryName(asset.LocalName); - if (!Directory.Exists(assetDir)) - { - Directory.CreateDirectory(assetDir); - } - - // Do multi-session download for asset that has applicable size - if (asset.Size >= _sizeForMultiDownload) - { - await httpClient.Download(asset.RemoteURL, asset.LocalName, _downloadThreadCount, true, token); - await httpClient.Merge(token); - } - // Do single-session download for others - else - { - await httpClient.Download(asset.RemoteURL, asset.LocalName, true, null, null, token); - } - - LogWriteLine($"Downloaded cache [T: {asset.AssetType}]: {Path.GetFileName(asset.LocalName)}", LogType.Default, true); - + // Run download task + await RunDownloadTask(asset.AssetIndex.Size, asset.AssetIndex.LocalName!, asset.AssetIndex.RemoteURL, downloadClient, downloadProgress, token); + LogWriteLine($"Downloaded cache [T: {asset.AssetIndex.AssetType}]: {Path.GetFileName(asset.AssetIndex.LocalName)}", LogType.Default, true); // Remove Asset Entry display - Dispatch(() => { if (AssetEntry.Count != 0) AssetEntry.RemoveAt(0); }); - } - - private async void _httpClient_UpdateAssetProgress(object sender, DownloadEvent e) - { - // Update current progress percentages and speed - _progress.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? - ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal) : - 0; - - if (e.State != DownloadState.Merging) - { - _progressAllSizeCurrent += e.Read; - } - long speed = (long)(_progressAllSizeCurrent / _stopwatch.Elapsed.TotalSeconds); - - if (await CheckIfNeedRefreshStopwatch()) - { - // Update current activity status - _status.IsProgressAllIndetermined = false; - string timeLeftString = string.Format(Lang._Misc.TimeRemainHMSFormat, ((_progressAllSizeCurrent - _progressAllSizeTotal) / ConverterTool.Unzeroed(speed)).ToTimeSpanNormalized()); - _status.ActivityAll = string.Format(Lang._Misc.Downloading + ": {0}/{1} ", _progressAllCountCurrent, _progressAllCountTotal) - + string.Format($"({Lang._Misc.SpeedPerSec})", ConverterTool.SummarizeSizeSimple(speed)) - + $" | {timeLeftString}"; - - // Trigger update - UpdateAll(); - } + PopRepairAssetEntry(asset.AssetProperty); } } } diff --git a/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs new file mode 100644 index 000000000..51163446c --- /dev/null +++ b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs @@ -0,0 +1,16 @@ +using CollapseLauncher.GameSettings.Zenless; +using CollapseLauncher.Interfaces; +using Microsoft.UI.Xaml; +using System.Threading.Tasks; + +namespace CollapseLauncher +{ + internal class ZenlessCache(UIElement parentUI, IGameVersionCheck gameVersionManager, ZenlessSettings gameSettings) + : ZenlessRepair(parentUI, gameVersionManager, gameSettings, false, null, true), ICache, ICacheBase + { + public ZenlessCache AsBaseType() => this; + + public async Task StartUpdateRoutine(bool showInteractivePrompt = false) + => await StartRepairRoutine(showInteractivePrompt); + } +} diff --git a/CollapseLauncher/Classes/CoCreateInstance.cs b/CollapseLauncher/Classes/CoCreateInstance.cs index 14a8a93dc..a182bccfa 100644 --- a/CollapseLauncher/Classes/CoCreateInstance.cs +++ b/CollapseLauncher/Classes/CoCreateInstance.cs @@ -2,7 +2,9 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; #pragma warning disable IL2050 namespace CollapseLauncher @@ -135,18 +137,40 @@ internal HRESULT ThrowOnFailure(IntPtr errorInfo = default) internal static partial class PInvoke { - internal static unsafe HRESULT CoCreateInstance(Guid rclsid, object pUnkOuter, CLSCTX dwClsContext, out T ppv) - where T : class +#nullable enable + internal static unsafe HRESULT CoCreateInstance(Guid rclsid, nint pUnkOuter, CLSCTX dwClsContext, out T? ppv) { - Guid refTGuid = typeof(T).GUID; - HRESULT hr = CoCreateInstanceInvoke(ref rclsid, pUnkOuter, dwClsContext, ref refTGuid, out object o); - ppv = (T)o; + Guid refTGuid = typeof(T).GUID; + HRESULT hr = CoCreateInstance(in rclsid, pUnkOuter, dwClsContext, in refTGuid, out void* o); + ppv = ComInterfaceMarshaller.ConvertToManaged(o); return hr; } [DllImport("OLE32.dll", EntryPoint = "CoCreateInstance", ExactSpelling = true)] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static unsafe extern HRESULT CoCreateInstanceInvoke(ref Guid rclsid, [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, CLSCTX dwClsContext, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppObj); + public static unsafe extern HRESULT CoCreateInstance(in Guid rclsid, IntPtr pUnkOuter, CLSCTX dwClsContext, in Guid riid, out void* ppObj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe TInterfaceTo? CastComInterfaceAs(this TInterfaceFrom interfaceFrom, in Guid interfaceToGuid) + where TInterfaceFrom : class + where TInterfaceTo : class + { + void* interfaceFromPtr = ComInterfaceMarshaller.ConvertToUnmanaged(interfaceFrom); + + Marshal.QueryInterface((nint)interfaceFromPtr, in interfaceToGuid, out nint ppv); + void* interfaceToPtr = (void*)ppv; + + TInterfaceTo? interfaceTo = ComInterfaceMarshaller.ConvertToManaged(interfaceToPtr); + return interfaceTo; + } + + internal static unsafe void Free(T? obj) + where T : class + { + void* objPtr = ComInterfaceMarshaller.ConvertToUnmanaged(obj); + ComInterfaceMarshaller.Free(objPtr); + } +#nullable restore } } #pragma warning restore IL2050 diff --git a/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs b/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs index 4dbe21c20..e0a188eda 100644 --- a/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs +++ b/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs @@ -34,10 +34,10 @@ public class DiscordPresenceManager : IDisposable private RichPresence _presence; private ActivityType _activityType; - private ActivityType _lastAttemptedActivityType; + //private ActivityType _lastAttemptedActivityType; private DateTime? _lastPlayTime; private bool _firstTimeConnect = true; - private ActionBlock _presenceUpdateQueue = null; + private ActionBlock _presenceUpdateQueue; private bool _cachedIsIdleEnabled = true; @@ -95,13 +95,14 @@ private void EnablePresence(long applicationId) // Initialize Discord RPC client _client = new DiscordRpcClient(applicationId.ToString()); _presenceUpdateQueue = new ActionBlock( - presence => _client?.SetPresence(_presence), - new ExecutionDataflowBlockOptions - { - MaxMessagesPerTask = 1, - MaxDegreeOfParallelism = 1, - EnsureOrdered = true - }); + // ReSharper disable once UnusedParameter.Local + presence => _client?.SetPresence(_presence), + new ExecutionDataflowBlockOptions + { + MaxMessagesPerTask = 1, + MaxDegreeOfParallelism = 1, + EnsureOrdered = true + }); _client.OnReady += (_, msg) => { @@ -185,11 +186,11 @@ public void SetupPresence() EnablePresence(AppDiscordApplicationID); } - public void SetActivity(ActivityType activity) + public void SetActivity(ActivityType activity, DateTime? activityOffset = null) { if (GetAppConfigValue("EnableDiscordRPC").ToBool()) { - _lastAttemptedActivityType = activity; + //_lastAttemptedActivityType = activity; _activityType = activity; switch (activity) @@ -198,7 +199,7 @@ public void SetActivity(ActivityType activity) { bool isGameStatusEnabled = GetAppConfigValue("EnableDiscordGameStatus").ToBool(); BuildActivityGameStatus(isGameStatusEnabled ? Lang._Misc.DiscordRP_InGame : Lang._Misc.DiscordRP_Play, - isGameStatusEnabled); + isGameStatusEnabled, activityOffset); break; } case ActivityType.Update: @@ -249,7 +250,7 @@ public void SetActivity(ActivityType activity) } } - private void BuildActivityGameStatus(string activityName, bool isGameStatusEnabled) + private void BuildActivityGameStatus(string activityName, bool isGameStatusEnabled, DateTime? activityOffset = null) { var curGameName = LauncherMetadataHelper.CurrentMetadataConfigGameName; var curGameRegion = LauncherMetadataHelper.CurrentMetadataConfigGameRegion; @@ -278,14 +279,14 @@ private void BuildActivityGameStatus(string activityName, bool isGameStatusEnabl }, Timestamps = new Timestamps { - Start = GetCachedStartPlayTime() + Start = GetCachedStartPlayTime(activityOffset) } }; } - private DateTime GetCachedStartPlayTime() + private DateTime GetCachedStartPlayTime(DateTime? activityOffset) { - _lastPlayTime ??= DateTime.UtcNow; + _lastPlayTime ??= activityOffset ??= DateTime.UtcNow; return _lastPlayTime.Value; } diff --git a/CollapseLauncher/Classes/EventsManagement/EventsHandler.cs b/CollapseLauncher/Classes/EventsManagement/EventsHandler.cs index 989320cdd..2942a6a44 100644 --- a/CollapseLauncher/Classes/EventsManagement/EventsHandler.cs +++ b/CollapseLauncher/Classes/EventsManagement/EventsHandler.cs @@ -124,7 +124,7 @@ internal class ThemeProperty } #endregion #region ErrorSenderRegion - public enum ErrorType { Unhandled, GameError, Connection, Warning } + public enum ErrorType { Unhandled, GameError, Connection, Warning, DiskCrc } internal static class ErrorSender { @@ -164,6 +164,10 @@ public static void SetPageTitle(ErrorType errorType) ExceptionTitle = _locUnhandledException.UnhandledTitle4; ExceptionSubtitle = _locUnhandledException.UnhandledSubtitle4; break; + case ErrorType.DiskCrc: + ExceptionTitle = _locUnhandledException.UnhandledTitleDiskCrc; + ExceptionSubtitle = _locUnhandledException.UnhandledSubDiskCrc; + break; } } } diff --git a/CollapseLauncher/Classes/Extension/CancellationTokenExtensions.cs b/CollapseLauncher/Classes/Extension/CancellationTokenExtensions.cs index 4588d19c4..9d07c5a53 100644 --- a/CollapseLauncher/Classes/Extension/CancellationTokenExtensions.cs +++ b/CollapseLauncher/Classes/Extension/CancellationTokenExtensions.cs @@ -6,11 +6,11 @@ namespace CollapseLauncher.Extension public class CancellationTokenSourceWrapper : CancellationTokenSource { public bool IsDisposed; - public bool IsCancelled = false; + public bool IsCancelled; public new void Cancel() { - if (!base.IsCancellationRequested) base.Cancel(); + if (!IsCancellationRequested) base.Cancel(); IsCancelled = true; } diff --git a/CollapseLauncher/Classes/Extension/NumberExtensions.cs b/CollapseLauncher/Classes/Extension/NumberExtensions.cs new file mode 100644 index 000000000..66256384d --- /dev/null +++ b/CollapseLauncher/Classes/Extension/NumberExtensions.cs @@ -0,0 +1,15 @@ +using Hi3Helper.Shared.Region; +using System; +using System.Runtime.CompilerServices; + +namespace CollapseLauncher.Extension +{ + internal static class NumberExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double ClampLimitedSpeedNumber(this double speed) + => LauncherConfig.DownloadSpeedLimitCached > 0 ? + Math.Min(LauncherConfig.DownloadSpeedLimitCached, speed) : + speed; + } +} diff --git a/CollapseLauncher/Classes/Extension/TaskExtensions.cs b/CollapseLauncher/Classes/Extension/TaskExtensions.cs index 83cee18d0..01ed2a487 100644 --- a/CollapseLauncher/Classes/Extension/TaskExtensions.cs +++ b/CollapseLauncher/Classes/Extension/TaskExtensions.cs @@ -6,18 +6,24 @@ #nullable enable namespace CollapseLauncher.Extension { - internal delegate ValueTask ActionTimeoutValueTaskCallback(CancellationToken token); + internal delegate Task ActionTimeoutValueTaskCallback(CancellationToken token); internal delegate void ActionOnTimeOutRetry(int retryAttemptCount, int retryAttemptTotal, int timeOutSecond, int timeOutStep); internal static class TaskExtensions { internal const int DefaultTimeoutSec = 10; internal const int DefaultRetryAttempt = 5; - internal static async ValueTask WaitForRetryAsync(this ActionTimeoutValueTaskCallback funcCallback, int? timeout = null, + internal static async Task AsTaskAndDoAction(this Task taskResult, Action doAction) + { + TResult? result = await taskResult; + doAction(result); + } + + internal static async Task WaitForRetryAsync(this ActionTimeoutValueTaskCallback funcCallback, int? timeout = null, int? timeoutStep = null, int? retryAttempt = null, ActionOnTimeOutRetry? actionOnRetry = null, CancellationToken fromToken = default) => await WaitForRetryAsync(() => funcCallback, timeout, timeoutStep, retryAttempt, actionOnRetry, fromToken); - internal static async ValueTask WaitForRetryAsync(Func> funcCallback, int? timeout = null, + internal static async Task WaitForRetryAsync(Func> funcCallback, int? timeout = null, int? timeoutStep = null, int? retryAttempt = null, ActionOnTimeOutRetry? actionOnRetry = null, CancellationToken fromToken = default) { timeout ??= DefaultTimeoutSec; @@ -45,7 +51,7 @@ internal static class TaskExtensions catch (Exception ex) { lastException = ex; - actionOnRetry?.Invoke(retryAttemptCurrent, retryAttempt ?? 0, timeout ?? 0, timeoutStep ?? 0); + actionOnRetry?.Invoke(retryAttemptCurrent, (int)retryAttempt, timeout ?? 0, timeoutStep ?? 0); if (ex is TimeoutException) { @@ -60,7 +66,6 @@ internal static class TaskExtensions retryAttemptCurrent++; timeout += timeoutStep; - continue; } finally { @@ -79,7 +84,7 @@ internal static class TaskExtensions } internal static async - ValueTask + Task TimeoutAfter(this Task task, CancellationToken token = default, int timeout = DefaultTimeoutSec) { Task completedTask = await Task.WhenAny(task, ThrowExceptionAfterTimeout(timeout, task, token)); diff --git a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs index 0e10a5726..c53519858 100644 --- a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs +++ b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs @@ -1,11 +1,13 @@ using CommunityToolkit.WinUI; -using CommunityToolkit.WinUI.Controls; using Hi3Helper; +using Hi3Helper.CommunityToolkit.WinUI.Controls; using Microsoft.UI; +using Microsoft.UI.Input; using Microsoft.UI.Text; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Media; using System; @@ -19,8 +21,200 @@ namespace CollapseLauncher.Extension { internal enum CornerRadiusKind { Normal, Rounded } + internal class NavigationViewItemLocaleTextProperty + { + public string LocaleSetName { get; set; } + public string LocalePropertyName { get; set; } + } + internal static class UIElementExtensions { + /// + /// Set the cursor for the element. + /// + /// The member of an element + /// The cursor you want to set. Use to choose the cursor you want to set. + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_ProtectedCursor")] + internal static extern void SetCursor(this UIElement element, InputCursor inputCursor); + + /// + /// Set the cursor for the element. + /// + /// The member of an element + /// The cursor you want to set. Use to choose the cursor you want to set. + internal static ref T WithCursor(this T element, InputCursor inputCursor) where T : UIElement + { + element.SetCursor(inputCursor); + return ref Unsafe.AsRef(ref element); + } + +#nullable enable + /// + /// Set the initial navigation view item's locale binding before getting set with + /// + /// The instance to set the initial text binding to. + /// The instance to set the initial text binding to. + /// The instance name of a members. + /// Name of the locale property + /// A reference of the + internal static ref T BindNavigationViewItemText(this T element, string localeSetName, string localePropertyName) + where T : NavigationViewItemBase + { + NavigationViewItemLocaleTextProperty property = new NavigationViewItemLocaleTextProperty + { + LocaleSetName = localeSetName, + LocalePropertyName = localePropertyName + }; + + if (element is NavigationViewItemHeader elementAsHeader) + { + elementAsHeader.Tag = property; + } + else + { + TextBlock textBlock = new TextBlock().WithTag(property); + element.Content = textBlock; + } + return ref Unsafe.AsRef(ref element); + } + + internal static void SetAllControlsCursorRecursive(this UIElement element, InputSystemCursor toCursor) + { + if (element == null) + { + return; + } + + if (element is Panel panelKind) + { + foreach (UIElement panelElements in panelKind.Children) + { + SetAllControlsCursorRecursive(panelElements, toCursor); + } + } + + if (element is RadioButtons radioButtonsKind) + { + foreach (UIElement radioButtonContent in radioButtonsKind.Items.OfType()) + { + radioButtonContent.SetCursor(toCursor); + } + } + + if (element is Border borderKind) + { + SetAllControlsCursorRecursive(borderKind.Child, toCursor); + } + + if (element is ComboBox comboBoxKind) + { + comboBoxKind.SetCursor(toCursor); + } + + if (element is UserControl userControlKind) + { + SetAllControlsCursorRecursive(userControlKind.Content, toCursor); + } + + if (element is ContentControl contentControlKind + && contentControlKind.Content is UIElement contentControlKindInner) + { + SetAllControlsCursorRecursive(contentControlKindInner, toCursor); + } + + if (element is NavigationView navigationViewKind) + { + foreach (UIElement navigationViewElements in navigationViewKind.FindDescendants()) + { + if (navigationViewElements is NavigationViewItem) + { + navigationViewElements.SetCursor(toCursor); + continue; + } + SetAllControlsCursorRecursive(navigationViewElements, toCursor); + } + } + + if (element is ButtonBase buttonBaseKind) + { + buttonBaseKind.SetCursor(toCursor); + if (buttonBaseKind is Button buttonKind && buttonKind.Flyout != null && buttonKind.Flyout is Flyout buttonKindFlyout) + { + SetAllControlsCursorRecursive(buttonKindFlyout.Content, toCursor); + } + } + + if (element is ToggleSwitch) + { + element.SetCursor(toCursor); + } + + if (element.ContextFlyout != null && element.ContextFlyout is Flyout elementFlyoutKind) + { + SetAllControlsCursorRecursive(elementFlyoutKind.Content, toCursor); + } + } + + internal static void ApplyNavigationViewItemLocaleTextBindings(this NavigationView navViewControl) + { + foreach (NavigationViewItemBase navItem in navViewControl + .FindDescendants() + .OfType()) + { + string? localeValue = null; + if (navItem.Content is TextBlock navItemTextBlock + && navItemTextBlock.Tag is NavigationViewItemLocaleTextProperty localeProperty) + { + navItemTextBlock.BindProperty( + TextBlock.TextProperty, + Locale.Lang, + $"{localeProperty.LocaleSetName}.{localeProperty.LocalePropertyName}"); + localeValue = navItemTextBlock.GetValue(TextBlock.TextProperty) as string; + } + else if (navItem is NavigationViewItemHeader navItemAsHeader + && navItemAsHeader.Tag is NavigationViewItemLocaleTextProperty localePropertyOnHeader) + { + navItemAsHeader.BindProperty( + ContentControl.ContentProperty, + Locale.Lang, + $"{localePropertyOnHeader.LocaleSetName}.{localePropertyOnHeader.LocalePropertyName}"); + localeValue = navItemAsHeader.GetValue(ContentControl.ContentProperty) as string; + } + + if (!string.IsNullOrEmpty(localeValue)) + { + ToolTipService.SetToolTip(navItem, localeValue); + } + } + + navViewControl.UpdateLayout(); + } + + internal static ref T BindProperty(this T element, DependencyProperty dependencyProperty, object objectToBind, string propertyName, IValueConverter? converter = null, BindingMode bindingMode = BindingMode.OneWay) + where T : FrameworkElement + { + // Create a new binding instance + Binding binding = new Binding + { + Source = objectToBind, + Mode = bindingMode, + Path = new PropertyPath(propertyName), + UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged + }; + + // If the converter is assigned, then add the converter + if (converter != null) + { + binding.Converter = converter; + } + + // Set binding to the element + element.SetBinding(dependencyProperty, binding); + + return ref Unsafe.AsRef(ref element); + } +#nullable restore + internal static TButtonBase CreateButtonWithIcon(string text = null, string iconGlyph = null, string iconFontFamily = "FontAwesome", string buttonStyle = "DefaultButtonStyle", double iconSize = 16d, double? textSize = null, CornerRadius? cornerRadius = null, FontWeight? textWeight = null) where TButtonBase : ButtonBase, new() @@ -101,10 +295,11 @@ internal static void AddGridColumns(this Grid grid, params GridLength[] columnWi if (columnWidths.Length == 0) throw new IndexOutOfRangeException($"\"columnWidth\" cannot be empty!"); - for (int i = 0; i < columnWidths.Length; i++) grid.ColumnDefinitions.Add(new ColumnDefinition() - { - Width = columnWidths[i] - }); + for (int i = 0; i < columnWidths.Length; i++) + grid.ColumnDefinitions.Add(new ColumnDefinition() + { + Width = columnWidths[i] + }); } internal static void AddGridColumns(this Grid grid, int count, GridLength? columnWidth = null) @@ -221,11 +416,15 @@ internal static CornerRadius GetElementCornerRadius(FrameworkElement element, Co internal static CornerRadius AttachRoundedKindCornerRadius(FrameworkElement element) { CornerRadius initialRadius = GetElementCornerRadius(element, CornerRadiusKind.Rounded); - element.SizeChanged += (sender, _) => InnerSetCornerRadius(element, GetElementCornerRadius(element, CornerRadiusKind.Rounded)); + element.SizeChanged += (_, _) => InnerSetCornerRadius(element, GetElementCornerRadius(element, CornerRadiusKind.Rounded)); return initialRadius; } - internal static void FindAndSetTextBlockWrapping(this UIElement element, TextWrapping wrap = TextWrapping.Wrap, HorizontalAlignment posAlign = HorizontalAlignment.Center, TextAlignment textAlign = TextAlignment.Center, bool recursiveAssignment = false, bool isParentAButton = false) + internal static void FindAndSetTextBlockWrapping(this UIElement element, + TextWrapping wrap = TextWrapping.Wrap, + HorizontalAlignment posAlign = HorizontalAlignment.Center, + TextAlignment textAlign = TextAlignment.Center, + bool recursiveAssignment = false, bool isParentAButton = false) { if (element is not null && element is TextBlock textBlock) { @@ -242,28 +441,28 @@ internal static void FindAndSetTextBlockWrapping(this UIElement element, TextWra if (element is ButtonBase button) { if (button.Content is UIElement buttonContent) - buttonContent.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, recursiveAssignment, true); + buttonContent.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, true, true); else if (button.Content is string buttonString) button.Content = new TextBlock { Text = buttonString, TextWrapping = wrap, HorizontalAlignment = HorizontalAlignment.Center }; } if (element is Panel panel) foreach (UIElement childrenElement in panel.Children!) - childrenElement.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, recursiveAssignment, isParentAButton); + childrenElement.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, true, isParentAButton); if (element is ScrollViewer scrollViewer && scrollViewer.Content is UIElement elementInner) - elementInner.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, recursiveAssignment, isParentAButton); + elementInner.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, true, isParentAButton); if (element is ContentControl contentControl && (element is SettingsCard || element is Expander) && contentControl.Content is UIElement contentControlInner) { - contentControlInner.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, recursiveAssignment, isParentAButton); + contentControlInner.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, true, isParentAButton); if (contentControl is Expander expander && expander.Header is UIElement expanderHeader) - expanderHeader.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, recursiveAssignment, isParentAButton); + expanderHeader.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, true, isParentAButton); } if (element is InfoBar infoBar && infoBar.Content is UIElement infoBarInner) - infoBarInner.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, recursiveAssignment, isParentAButton); + infoBarInner.FindAndSetTextBlockWrapping(wrap, posAlign, textAlign, true, isParentAButton); } internal static ref TElement WithWidthAndHeight(this TElement element, double uniform) @@ -657,9 +856,7 @@ private static void InnerSetCornerRadius(TElement element, CornerRadiu internal static void ApplyDropShadow(this FrameworkElement element, Color? shadowColor = null, double blurRadius = 10, double opacity = 0.25, bool isMasked = true, Vector3? offset = null) { - FrameworkElement shadowPanel = null; - - shadowPanel = element.FindDescendant("ShadowGrid"); + var shadowPanel = element.FindDescendant("ShadowGrid"); if (shadowPanel == null) { shadowPanel = CreateGrid() @@ -692,13 +889,16 @@ void AssignShadowAttachment(FrameworkElement thisElement, bool innerMasked) case Image imageElement: AttachShadow(imageElement, true, offset); break; + case ImageEx.ImageEx imageExElement: + AttachShadow(imageExElement, true, offset); + break; default: AttachShadow(element, innerMasked, offset); break; } } - void AttachShadow(FrameworkElement thisElement, bool innerMask, Vector3? offset) + void AttachShadow(FrameworkElement thisElement, bool innerMask, Vector3? _offset) { FrameworkElement xamlRoot = (thisElement.Parent as FrameworkElement) ?? thisElement.FindDescendant(); @@ -717,7 +917,7 @@ void AttachShadow(FrameworkElement thisElement, bool innerMask, Vector3? offset) if (xamlRoot == null || xamlRoot is not Panel) throw new NullReferenceException("The element must be inside of a Grid or StackPanel or any \"Panel\" elements"); - thisElement.ApplyDropShadow(shadowPanel, shadowColor, blurRadius, opacity, innerMask, offset); + thisElement.ApplyDropShadow(shadowPanel, shadowColor, blurRadius, opacity, innerMask, _offset); } catch (Exception ex) { @@ -731,7 +931,9 @@ internal static void ApplyDropShadow(this FrameworkElement from, FrameworkElemen double blurRadius = 10, double opacity = 0.25, bool isMasked = false, Vector3? offset = null) { offset ??= Vector3.Zero; - string passedValue = $"{offset?.X ?? 0},{offset?.Y ?? 0},{offset?.Z ?? 0}"; + // ReSharper disable ConstantConditionalAccessQualifier + string passedValue = $"{offset?.X},{offset?.Y},{offset?.Z}"; + // ReSharper restore ConstantConditionalAccessQualifier AttachedDropShadow shadow = new AttachedDropShadow() { diff --git a/CollapseLauncher/Classes/FileDialogCOM/FileDialogHelper.cs b/CollapseLauncher/Classes/FileDialogCOM/FileDialogHelper.cs new file mode 100644 index 000000000..3d4ec0a79 --- /dev/null +++ b/CollapseLauncher/Classes/FileDialogCOM/FileDialogHelper.cs @@ -0,0 +1,194 @@ +using CollapseLauncher.Dialogs; +using CollapseLauncher.Extension; +using CollapseLauncher.FileDialogCOM; +using CollapseLauncher.Helper; +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.Shared.Region; +using Microsoft.UI.Text; +using Microsoft.UI.Xaml.Controls; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +#nullable enable +namespace CollapseLauncher.Classes.FileDialogCOM +{ + internal static class FileDialogHelper + { + /// + /// Get only folder paths that's allowed by Collapse Launcher + /// + /// The title of the folder dialog + /// Override path check and returns the actual string + /// If is empty, then it's cancelled. Otherwise, returns a selected path + internal static async Task GetRestrictedFolderPathDialog(string? title = null, Func? checkOverride = null) + { + StartGet: + string dirPath = await FileDialogNative.GetFolderPicker(title); + + if (string.IsNullOrEmpty(dirPath)) + return dirPath; + + string? existingCheckOverride; + if (checkOverride != null && !string.IsNullOrEmpty(existingCheckOverride = checkOverride(dirPath))) + { + return existingCheckOverride; + } + + if (!ConverterTool.IsUserHasPermission(dirPath)) + { + await SpawnInvalidDialog( + Locale.Lang._Dialogs.InvalidGameDirNew2Title, + Locale.Lang._Dialogs.InvalidGameDirNew2Subtitle, + dirPath); + goto StartGet; + } + + if (IsRootPath(dirPath)) + { + await SpawnInvalidDialog( + Locale.Lang._Dialogs.InvalidGameDirNew3Title, + Locale.Lang._Dialogs.InvalidGameDirNew3Subtitle, + dirPath); + goto StartGet; + } + + if (IsSystemDirPath(dirPath)) + { + await SpawnInvalidDialog( + Locale.Lang._Dialogs.InvalidGameDirNew4Title, + Locale.Lang._Dialogs.InvalidGameDirNew4Subtitle, + dirPath); + goto StartGet; + } + + if (IsProgramDataPath(dirPath)) + { + await SpawnInvalidDialog( + Locale.Lang._Dialogs.InvalidGameDirNew5Title, + Locale.Lang._Dialogs.InvalidGameDirNew5Subtitle, + dirPath); + goto StartGet; + } + + if (IsCollapseProgramPath(dirPath) || !CheckIfFolderIsValidLegacy(dirPath)) + { + await SpawnInvalidDialog( + Locale.Lang._Dialogs.CannotUseAppLocationForGameDirTitle, + Locale.Lang._Dialogs.CannotUseAppLocationForGameDirSubtitle, + dirPath, + true); + goto StartGet; + } + + if (IsProgramFilesPath(dirPath)) + { + await SpawnInvalidDialog( + Locale.Lang._Dialogs.InvalidGameDirNew6Title, + Locale.Lang._Dialogs.InvalidGameDirNew6Subtitle, + dirPath); + goto StartGet; + } + + return dirPath; + + + async Task SpawnInvalidDialog(string title, string message, string selectedPath, bool isUseLegacyFormatting = false) + { + TextBlock textBlock = new TextBlock() + { + TextWrapping = Microsoft.UI.Xaml.TextWrapping.Wrap + }; + textBlock.AddTextBlockLine(message) + .AddTextBlockNewLine(2) + .AddTextBlockLine(Locale.Lang._Dialogs.InvalidGameDirNewSubtitleSelectedPath, weight: FontWeights.Bold) + .AddTextBlockNewLine() + .AddTextBlockLine(selectedPath); + if (!isUseLegacyFormatting) + { + textBlock.AddTextBlockNewLine(2) + .AddTextBlockLine(Locale.Lang._Dialogs.InvalidGameDirNewSubtitleSelectOther); + } + + await SimpleDialogs.SpawnDialog( + isUseLegacyFormatting ? title : string.Format(Locale.Lang._Dialogs.InvalidGameDirNewTitleFormat, title), + textBlock, + (WindowUtility.CurrentWindow as MainWindow)?.Content, + Locale.Lang._Misc.Okay, + defaultButton: ContentDialogButton.Close, + dialogTheme: CustomControls.ContentDialogTheme.Error); + } + } + + private static bool IsSystemDirPath(ReadOnlySpan path) + { + string? systemRootPath = Environment.GetEnvironmentVariable("SystemRoot"); + if (path.StartsWith(systemRootPath)) + return true; + + return false; + } + + private static bool IsProgramDataPath(ReadOnlySpan path) + { + string? programDataPath = Environment.GetEnvironmentVariable("ProgramData"); + if (path.StartsWith(programDataPath)) + return true; + + return false; + } + + private static bool IsProgramFilesPath(ReadOnlySpan path) + { + string? programFilesPath = Environment.GetEnvironmentVariable("ProgramFiles"); + if (path.StartsWith(programFilesPath)) + return true; + + return false; + } + + private static bool IsCollapseProgramPath(ReadOnlySpan path) + { + string collapseProgramPath = LauncherConfig.AppFolder; + if (path.StartsWith(collapseProgramPath)) + return true; + + return false; + } + + /// + /// Determines if the path given is a drive root + /// + /// Path to check + /// True if path is root of the drive + internal static bool IsRootPath(ReadOnlySpan path) + { + ReadOnlySpan rootPath = Path.GetPathRoot(path); + return rootPath.SequenceEqual(path); + } + + private static bool CheckIfFolderIsValidLegacy(string basePath) + { + bool isInAppFolderExist = File.Exists(Path.Combine(basePath, LauncherConfig.AppExecutableName)) + || File.Exists(Path.Combine($"{basePath}\\..\\", LauncherConfig.AppExecutableName)) + || File.Exists(Path.Combine($"{basePath}\\..\\..\\", LauncherConfig.AppExecutableName)) + || File.Exists(Path.Combine($"{basePath}\\..\\..\\..\\", LauncherConfig.AppExecutableName)); + + string? driveLetter = Path.GetPathRoot(basePath); + if (string.IsNullOrEmpty(driveLetter)) + { + return false; + } + + bool isInAppFolderExist2 = basePath.EndsWith("Collapse Launcher") + || basePath.StartsWith(Path.Combine(driveLetter, "Program Files")) + || basePath.StartsWith(Path.Combine(driveLetter, "Program Files (x86)")) + || basePath.StartsWith(Path.Combine(driveLetter, "Windows")); + + return !(isInAppFolderExist || isInAppFolderExist2); + } + + } +} diff --git a/CollapseLauncher/Classes/FileDialogCOM/FileDialogNative.cs b/CollapseLauncher/Classes/FileDialogCOM/FileDialogNative.cs index b4bfbee85..b0649ec4c 100644 --- a/CollapseLauncher/Classes/FileDialogCOM/FileDialogNative.cs +++ b/CollapseLauncher/Classes/FileDialogCOM/FileDialogNative.cs @@ -44,7 +44,7 @@ private static ValueTask GetPickerOpenTask(object defaultValue, Dicti { PInvoke.CoCreateInstance( new Guid(CLSIDGuid.FileOpenDialog), - null, + IntPtr.Zero, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure(); @@ -85,6 +85,8 @@ private static ValueTask GetPickerOpenTask(object defaultValue, Dicti finally { if (titlePtr != IntPtr.Zero) Marshal.FreeCoTaskMem(titlePtr); + // Free the COM instance + PInvoke.Free(dialog); } } @@ -92,7 +94,7 @@ private static ValueTask GetPickerSaveTask(string defaultValue, Dicti { PInvoke.CoCreateInstance( new Guid(CLSIDGuid.FileSaveDialog), - null, + IntPtr.Zero, CLSCTX.CLSCTX_INPROC_SERVER, out IFileSaveDialog dialog).ThrowOnFailure(); @@ -121,6 +123,8 @@ private static ValueTask GetPickerSaveTask(string defaultValue, Dicti finally { if (titlePtr != IntPtr.Zero) Marshal.FreeCoTaskMem(titlePtr); + // Free the COM instance + PInvoke.Free(dialog); } } diff --git a/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs index cbff8f619..a51d7d209 100644 --- a/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs +++ b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs @@ -1,4 +1,5 @@ -using Hi3Helper; +using CollapseLauncher.Classes.FileDialogCOM; +using Hi3Helper; using Hi3Helper.Shared.Region; using Microsoft.UI.Xaml; using System; @@ -52,6 +53,11 @@ internal async Task StartRoutine() if (!await IsOutputPathSpaceSufficient(this.inputPath, this.outputPath)) throw new OperationCanceledException($"Disk space is not sufficient. Cancelling!"); + if (FileDialogHelper.IsRootPath(outputPath)) + { + throw new NotSupportedException("Cannot move game to the root of the drive!"); + } + uiRef = BuildMainMigrationUI(); string _outputPath = await StartRoutineInner(uiRef.Value); uiRef.Value.mainDialogWindow!.Hide(); diff --git a/CollapseLauncher/Classes/FileMigrationProcess/IO.cs b/CollapseLauncher/Classes/FileMigrationProcess/IO.cs index 665aed3fa..da4d21060 100644 --- a/CollapseLauncher/Classes/FileMigrationProcess/IO.cs +++ b/CollapseLauncher/Classes/FileMigrationProcess/IO.cs @@ -23,52 +23,52 @@ private async Task MoveWriteFile(FileMigrationProcessUIRef uiRef, FileInfo input byte[] buffer = new byte[bufferSize]; await using (FileStream inputStream = inputFile.OpenRead()) - await using (FileStream outputStream = outputFile!.Exists && outputFile.Length <= inputFile.Length ? - outputFile.Open(FileMode.Open) : outputFile.Create()) - { - // Set the output file size to inputStream's if the length is more than inputStream - if (outputStream.Length > inputStream.Length) - outputStream.SetLength(inputStream.Length); - - // Just in-case if the previous move is incomplete, then update and seek to the last position. - if (outputStream.Length <= inputStream.Length && outputStream.Length >= bufferSize) + await using (FileStream outputStream = outputFile!.Exists && outputFile.Length <= inputFile.Length ? + outputFile.Open(FileMode.Open) : outputFile.Create()) { - // Do check by comparing the first and last 128K data of the file - Memory firstCompareInputBytes = new byte[bufferSize]; - Memory firstCompareOutputBytes = new byte[bufferSize]; - Memory lastCompareInputBytes = new byte[bufferSize]; - Memory lastCompareOutputBytes = new byte[bufferSize]; - - // Seek to the first data - inputStream.Position = 0; - await inputStream.ReadExactlyAsync(firstCompareInputBytes); - outputStream.Position = 0; - await outputStream.ReadExactlyAsync(firstCompareOutputBytes); - - // Seek to the last data - long lastPos = outputStream.Length - bufferSize; - inputStream.Position = lastPos; - await inputStream.ReadExactlyAsync(lastCompareInputBytes); - outputStream.Position = lastPos; - await outputStream.ReadExactlyAsync(lastCompareOutputBytes); - - bool isMatch = firstCompareInputBytes.Span.SequenceEqual(firstCompareOutputBytes.Span) - && lastCompareInputBytes.Span.SequenceEqual(lastCompareOutputBytes.Span); - - // If the buffers don't match, then start the copy from the beginning - if (!isMatch) + // Set the output file size to inputStream's if the length is more than inputStream + if (outputStream.Length > inputStream.Length) + outputStream.SetLength(inputStream.Length); + + // Just in-case if the previous move is incomplete, then update and seek to the last position. + if (outputStream.Length <= inputStream.Length && outputStream.Length >= bufferSize) { + // Do check by comparing the first and last 128K data of the file + Memory firstCompareInputBytes = new byte[bufferSize]; + Memory firstCompareOutputBytes = new byte[bufferSize]; + Memory lastCompareInputBytes = new byte[bufferSize]; + Memory lastCompareOutputBytes = new byte[bufferSize]; + + // Seek to the first data inputStream.Position = 0; + await inputStream.ReadExactlyAsync(firstCompareInputBytes); outputStream.Position = 0; + await outputStream.ReadExactlyAsync(firstCompareOutputBytes); + + // Seek to the last data + long lastPos = outputStream.Length - bufferSize; + inputStream.Position = lastPos; + await inputStream.ReadExactlyAsync(lastCompareInputBytes); + outputStream.Position = lastPos; + await outputStream.ReadExactlyAsync(lastCompareOutputBytes); + + bool isMatch = firstCompareInputBytes.Span.SequenceEqual(firstCompareOutputBytes.Span) + && lastCompareInputBytes.Span.SequenceEqual(lastCompareOutputBytes.Span); + + // If the buffers don't match, then start the copy from the beginning + if (!isMatch) + { + inputStream.Position = 0; + outputStream.Position = 0; + } + else + { + UpdateSizeProcessed(uiRef, outputStream.Length); + } } - else - { - UpdateSizeProcessed(uiRef, outputStream.Length); - } - } - await MoveWriteFileInner(uiRef, inputStream, outputStream, buffer, token); - } + await MoveWriteFileInner(uiRef, inputStream, outputStream, buffer, token); + } inputFile.IsReadOnly = false; inputFile.Delete(); @@ -109,8 +109,9 @@ private async ValueTask IsOutputPathSpaceSufficient(string _inputPath, str }); return returnSize; }); - - if (IsSameOutputDrive = (inputDriveInfo.Name == outputDriveInfo.Name)) + + IsSameOutputDrive = inputDriveInfo.Name == outputDriveInfo.Name; + if (IsSameOutputDrive) return true; bool isSpaceSufficient = outputDriveInfo.TotalFreeSpace > TotalFileSize; diff --git a/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs b/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs index 3add3c2bd..8e6d4237b 100644 --- a/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs +++ b/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs @@ -1,4 +1,6 @@ -using CollapseLauncher.Dialogs; +using CollapseLauncher.Classes.FileDialogCOM; +using CollapseLauncher.CustomControls; +using CollapseLauncher.Dialogs; using Hi3Helper.Data; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -6,6 +8,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using static Hi3Helper.Locale; namespace CollapseLauncher { @@ -21,6 +24,16 @@ internal static async Task CreateJob(UIElement parentUI, s outputPath = await InitializeAndCheckOutputPath(parentUI, dialogTitle, inputPath, outputPath, isFileTransfer); if (outputPath == null) return null; + if (FileDialogHelper.IsRootPath(outputPath)) + { + await SimpleDialogs.SpawnDialog(Lang._HomePage.InstallFolderRootTitle, + Lang._HomePage.InstallFolderRootSubtitle, + parentUI, + Lang._Misc.Close, + null, null, ContentDialogButton.Close, ContentDialogTheme.Error); + return null; + } + if (showWarningMessage) if (await ShowNotCancellableProcedureMessage(parentUI) == ContentDialogResult.None) return null; diff --git a/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs b/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs index 21f84aec4..d64ff8bb7 100644 --- a/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs +++ b/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs @@ -1,4 +1,5 @@ -using CollapseLauncher.CustomControls; +using CollapseLauncher.Classes.FileDialogCOM; +using CollapseLauncher.CustomControls; using CollapseLauncher.Dialogs; using CollapseLauncher.Extension; using CollapseLauncher.FileDialogCOM; @@ -71,7 +72,7 @@ private static async ValueTask BuildCheckOutputPathUI(UIElement parentUI choosePathButton.Click += async (_, _) => { string pathResult = isFileTransfer ? await FileDialogNative.GetFileSavePicker(null, dialogTitle) : - await FileDialogNative.GetFolderPicker(dialogTitle); + await FileDialogHelper.GetRestrictedFolderPathDialog(dialogTitle); choosePathTextBox!.Text = string.IsNullOrEmpty(pathResult) ? null : pathResult; }; diff --git a/CollapseLauncher/Classes/GameManagement/GamePlaytime/Context.cs b/CollapseLauncher/Classes/GameManagement/GamePlaytime/Context.cs new file mode 100644 index 000000000..843d46ae3 --- /dev/null +++ b/CollapseLauncher/Classes/GameManagement/GamePlaytime/Context.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace CollapseLauncher.GamePlaytime +{ + [JsonSourceGenerationOptions(IncludeFields = false, GenerationMode = JsonSourceGenerationMode.Metadata, IgnoreReadOnlyFields = true)] + [JsonSerializable(typeof(CollapsePlaytime))] + internal sealed partial class UniversalPlaytimeJSONContext : JsonSerializerContext { } +} diff --git a/CollapseLauncher/Classes/GameManagement/GamePlaytime/Playtime.cs b/CollapseLauncher/Classes/GameManagement/GamePlaytime/Playtime.cs new file mode 100644 index 000000000..3e5038c25 --- /dev/null +++ b/CollapseLauncher/Classes/GameManagement/GamePlaytime/Playtime.cs @@ -0,0 +1,154 @@ +using CollapseLauncher.Extension; +using CollapseLauncher.Helper.Database; +using CollapseLauncher.Interfaces; +using Hi3Helper; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Timers; +using static Hi3Helper.Logger; +using static CollapseLauncher.Dialogs.SimpleDialogs; +using static CollapseLauncher.InnerLauncherConfig; + +namespace CollapseLauncher.GamePlaytime +{ + internal class Playtime : IGamePlaytime + { + #region Properties + public event EventHandler PlaytimeUpdated; +#nullable enable + public CollapsePlaytime CollapsePlaytime => _playtime; + + private static HashSet _activeSessions = []; + private RegistryKey? _registryRoot; + private CollapsePlaytime _playtime; + private IGameVersionCheck _gameVersionManager; + + private CancellationTokenSourceWrapper _token = new(); + #endregion + + public Playtime(IGameVersionCheck gameVersionManager, IGameSettings gameSettings) + { + string registryPath = Path.Combine($"Software\\{gameVersionManager.VendorTypeProp.VendorType}", gameVersionManager.GamePreset.InternalGameNameInConfig!); + _registryRoot = Registry.CurrentUser.OpenSubKey(registryPath, true); + + _registryRoot ??= Registry.CurrentUser.CreateSubKey(registryPath, true, RegistryOptions.None); + + _gameVersionManager = gameVersionManager; + + _playtime = CollapsePlaytime.Load(_registryRoot, + _gameVersionManager.GamePreset.HashID, + _gameVersionManager, + gameSettings); + + + if (DbHandler.IsEnabled && gameSettings.AsIGameSettingsUniversal().SettingsCollapseMisc.IsSyncPlaytimeToDatabase) + CheckDb(); + } +#nullable disable + + public async void CheckDb() + { + var needUpdate = await _playtime.DbSync(); + if (needUpdate is not { IsUpdated: true, PlaytimeData: not null }) return; + + _playtime = needUpdate.PlaytimeData; + PlaytimeUpdated?.Invoke(this, _playtime); + } + + public void Update(TimeSpan timeSpan, bool forceUpdateDb = false) + { + TimeSpan oldTimeSpan = _playtime.TotalPlaytime; + + _playtime.Update(timeSpan, true, true); + PlaytimeUpdated?.Invoke(this, _playtime); + + LogWriteLine($"Playtime counter changed to {TimeSpanToString(timeSpan)}. (Previous value: {TimeSpanToString(oldTimeSpan)})", writeToLog: true); + } + + public void Reset() + { + TimeSpan oldTimeSpan = _playtime.TotalPlaytime; + + _playtime.Reset(); + PlaytimeUpdated?.Invoke(this, _playtime); + + LogWriteLine($"Playtime counter was reset! (Previous value: {TimeSpanToString(oldTimeSpan)})", writeToLog: true); + } + + public async void StartSession(Process proc, DateTime? begin = null) + { + int hashId = _gameVersionManager.GamePreset.HashID; + + // If a playtime HashSet has already tracked, then return (do not track the playtime more than once) + if (_activeSessions.Contains(hashId)) return; + + // Otherwise, add it to track list + _activeSessions.Add(hashId); + + begin ??= DateTime.Now; + + TimeSpan initialTimeSpan = _playtime.TotalPlaytime; + + _playtime.LastPlayed = begin; + _playtime.LastSession = TimeSpan.Zero; + _playtime.Save(); + PlaytimeUpdated?.Invoke(this, _playtime); + +#if DEBUG + LogWriteLine($"{_gameVersionManager.GamePreset.ProfileName} - Started session at {begin.Value.ToLongTimeString()}."); +#endif + int elapsedSeconds = 0; + + using (var inGameTimer = new Timer()) + { + inGameTimer.Interval = 60000; + inGameTimer.Elapsed += (_, _) => + { + elapsedSeconds += 60; + DateTime now = DateTime.Now; + + _playtime.AddMinute(); + PlaytimeUpdated?.Invoke(this, _playtime); +#if DEBUG + LogWriteLine($"{_gameVersionManager.GamePreset.ProfileName} - {elapsedSeconds}s elapsed. ({now.ToLongTimeString()})"); +#endif + }; + + inGameTimer.Start(); + await proc.WaitForExitAsync(_token.Token); + inGameTimer.Stop(); + } + + DateTime end = DateTime.Now; + double totalElapsedSeconds = (end - begin.Value).TotalSeconds; + if (totalElapsedSeconds < 0) + { + LogWriteLine($"[HomePage::StartPlaytimeCounter] Date difference cannot be lower than 0. ({totalElapsedSeconds}s)", LogType.Error); + Dialog_InvalidPlaytime(m_mainPage?.Content, elapsedSeconds); + totalElapsedSeconds = elapsedSeconds; + } + + TimeSpan totalTimeSpan = TimeSpan.FromSeconds(totalElapsedSeconds); + LogWriteLine($"Added {totalElapsedSeconds}s [{totalTimeSpan.Hours}h {totalTimeSpan.Minutes}m {totalTimeSpan.Seconds}s] " + + $"to {_gameVersionManager.GamePreset.ProfileName} playtime.", LogType.Default, true); + + _playtime.Update(initialTimeSpan.Add(totalTimeSpan), false, true); + PlaytimeUpdated?.Invoke(this, _playtime); + + _activeSessions.Remove(hashId); + } + + private static string TimeSpanToString(TimeSpan timeSpan) => $"{timeSpan.Days * 24 + timeSpan.Hours}h {timeSpan.Minutes}m"; + + public void Dispose() + { + _token.Cancel(); + _playtime.Save(true); + _playtime.LastDbUpdate = DateTime.MinValue; + _registryRoot = null; + } + } +} diff --git a/CollapseLauncher/Classes/GameManagement/GamePlaytime/RegistryClass/CollapsePlaytime.cs b/CollapseLauncher/Classes/GameManagement/GamePlaytime/RegistryClass/CollapsePlaytime.cs new file mode 100644 index 000000000..bf4bbf1e2 --- /dev/null +++ b/CollapseLauncher/Classes/GameManagement/GamePlaytime/RegistryClass/CollapsePlaytime.cs @@ -0,0 +1,415 @@ +using CollapseLauncher.Interfaces; +using CollapseLauncher.Helper.Database; +using Hi3Helper; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using static Hi3Helper.Logger; + +namespace CollapseLauncher.GamePlaytime +{ + internal class CollapsePlaytime + { + #region Fields + private static DateTime BaseDate => new(2012, 2, 13, 0, 0, 0, DateTimeKind.Utc); + + private const string TotalTimeValueName = "CollapseLauncher_Playtime"; + private const string LastPlayedValueName = "CollapseLauncher_LastPlayed"; + private const string StatsValueName = "CollapseLauncher_PlaytimeStats"; + + // ReSharper disable once FieldCanBeMadeReadOnly.Local + private static HashSet _isDeserializing = []; + private RegistryKey _registryRoot; + private int _hashID; + private IGameVersionCheck _gameVersion; + private IGameSettings _gameSettings; + + #endregion + + #region Properties + /// + /// Represents the total time a game was played.

+ /// Default: TimeSpan.Zero + ///
+ [JsonIgnore] + public TimeSpan TotalPlaytime { get; set; } = TimeSpan.Zero; + + /// + /// Represents the daily playtime.
+ /// The ControlDate field is used to check if this value should be reset.

+ /// Default: TimeSpan.Zero + ///
+ public TimeSpan DailyPlaytime { get; set; } = TimeSpan.Zero; + + /// + /// Represents the weekly playtime.
+ /// The ControlDate field is used to check if this value should be reset.

+ /// Default: TimeSpan.Zero + ///
+ public TimeSpan WeeklyPlaytime { get; set; } = TimeSpan.Zero; + + /// + /// Represents the monthly playtime.
+ /// The ControlDate field is used to check if this value should be reset.

+ /// Default: TimeSpan.Zero + ///
+ public TimeSpan MonthlyPlaytime { get; set; } = TimeSpan.Zero; + + /// + /// Represents the total time the last/current session lasted.

+ /// Default: TimeSpan.Zero + ///
+ public TimeSpan LastSession { get; set; } = TimeSpan.Zero; + + /// + /// Represents the last time the game was launched.

+ /// Default: null + ///
+ [JsonIgnore] + public DateTime? LastPlayed { get; set; } + + /// + /// Represents a control date.
+ /// This date is used to check if a specific playtime statistic should be reset.

+ /// Default: DateTime.Today + ///
+ public DateTime ControlDate { get; set; } = DateTime.Today; + #endregion + + #region Methods +#nullable enable + /// + /// Reads from the Registry and deserializes the contents. + /// + public static CollapsePlaytime Load(RegistryKey root, int hashID, + IGameVersionCheck gameVersion, + IGameSettings gameSettings) + { + try + { + _isDeserializing.Add(hashID); + if (root == null) throw new NullReferenceException($"Cannot load playtime. RegistryKey is unexpectedly not initialized!"); + + int? totalTime = (int?)root.GetValue(TotalTimeValueName,null); + int? lastPlayed = (int?)root.GetValue(LastPlayedValueName,null); + object? stats = root.GetValue(StatsValueName, null); + + CollapsePlaytime? playtimeInner; + + if (stats != null) + { + ReadOnlySpan byteStr = (byte[])stats; +#if DEBUG + LogWriteLine($"Loaded Playtime:\r\nTotal: {totalTime}s\r\nLastPlayed: {lastPlayed}\r\nStats: {Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); +#endif + playtimeInner = byteStr.Deserialize(UniversalPlaytimeJSONContext.Default.CollapsePlaytime, new CollapsePlaytime())!; + } + else + { + playtimeInner = new CollapsePlaytime(); + } + + playtimeInner._gameVersion = gameVersion; + playtimeInner._gameSettings = gameSettings; + playtimeInner._registryRoot = root; + playtimeInner._hashID = hashID; + playtimeInner.TotalPlaytime = TimeSpan.FromSeconds(totalTime ?? 0); + playtimeInner.LastPlayed = lastPlayed != null ? BaseDate.AddSeconds((int)lastPlayed) : null; + playtimeInner.CheckStatsReset(); + + return playtimeInner; + } + catch (Exception ex) + { + LogWriteLine($"Failed while reading playtime.\r\n{ex}", LogType.Error, true); + } + finally + { + _isDeserializing.Remove(hashID); + } + + return new CollapsePlaytime + { + _hashID = hashID, + _registryRoot = root, + _gameVersion = gameVersion, + _gameSettings = gameSettings + }; + } + + /// + /// Serializes all fields and saves them to the Registry. + /// + public void Save(bool forceUpdateDb = false) + { + try + { + if (_registryRoot == null) throw new NullReferenceException($"Cannot save playtime since RegistryKey is unexpectedly not initialized!"); + + string data = this.Serialize(UniversalPlaytimeJSONContext.Default.CollapsePlaytime); + byte[] dataByte = Encoding.UTF8.GetBytes(data); +#if DEBUG + LogWriteLine($"Saved Playtime:\r\n{data}", LogType.Debug, true); +#endif + _registryRoot.SetValue(StatsValueName, dataByte, RegistryValueKind.Binary); + _registryRoot.SetValue(TotalTimeValueName, TotalPlaytime.TotalSeconds, RegistryValueKind.DWord); + + double? lastPlayed = (LastPlayed?.ToUniversalTime() - BaseDate)?.TotalSeconds; + if (lastPlayed != null) + _registryRoot.SetValue(LastPlayedValueName, lastPlayed, RegistryValueKind.DWord); + + if (DbHandler.IsEnabled && _gameSettings.AsIGameSettingsUniversal().SettingsCollapseMisc.IsSyncPlaytimeToDatabase && + ((DateTime.Now - LastDbUpdate).TotalMinutes >= 5 || forceUpdateDb)) // Sync only every 5 minutes to reduce database usage + { + _ = UpdatePlaytime_Database_Push(data, TotalPlaytime.TotalSeconds, lastPlayed); + } + } + catch (Exception ex) + { + LogWriteLine($"Failed to save playtime!\r\n{ex}", LogType.Error, true); + } + } + + /// + /// Resets all fields and saves to the Registry. + /// + public void Reset() + { + TotalPlaytime = TimeSpan.Zero; + LastSession = TimeSpan.Zero; + DailyPlaytime = TimeSpan.Zero; + WeeklyPlaytime = TimeSpan.Zero; + MonthlyPlaytime = TimeSpan.Zero; + ControlDate = DateTime.Today; + LastPlayed = null; + + if (!_isDeserializing.Contains(_hashID)) Save(true); + } + + /// + /// Updates the current Playtime TimeSpan to the provided value and saves to the registry.

+ ///
+ /// New playtime value + /// Reset all other fields + /// Force update database data + public void Update(TimeSpan timeSpan, bool reset = true, bool forceUpdateDb = false) + { + if (reset) + { + LastSession = TimeSpan.Zero; + DailyPlaytime = TimeSpan.Zero; + WeeklyPlaytime = TimeSpan.Zero; + MonthlyPlaytime = TimeSpan.Zero; + ControlDate = DateTime.Today; + } + + TotalPlaytime = timeSpan; + + if (!_isDeserializing.Contains(_hashID)) Save(forceUpdateDb); + } + + /// + /// Checks if any stats should be reset.
+ /// Afterwards, the values are saved to the registry.

+ ///
+ private void CheckStatsReset() + { + DateTime today = DateTime.Today; + + if (ControlDate == today) + return; + + DailyPlaytime = TimeSpan.Zero; + if (IsDifferentWeek(ControlDate, today)) + WeeklyPlaytime = TimeSpan.Zero; + if (IsDifferentMonth(ControlDate, today)) + MonthlyPlaytime = TimeSpan.Zero; + + ControlDate = today; + } + + /// + /// Adds a minute to all fields, and checks if any should be reset.
+ /// Afterwards, the values are saved to the registry.

+ ///
+ public void AddMinute() + { + TimeSpan minute = TimeSpan.FromMinutes(1); + + TotalPlaytime = TotalPlaytime.Add(minute); + LastSession = LastSession.Add(minute); + + CheckStatsReset(); + + DailyPlaytime = DailyPlaytime.Add(minute); + WeeklyPlaytime = WeeklyPlaytime.Add(minute); + MonthlyPlaytime = MonthlyPlaytime.Add(minute); + + if (!_isDeserializing.Contains(_hashID)) Save(); + } + + #endregion + + #region Utility + private static bool IsDifferentMonth(DateTime date1, DateTime date2) => date1.Year != date2.Year || date1.Month != date2.Month; + + private static bool IsDifferentWeek(DateTime date1, DateTime date2) => date1.Year != date2.Year || ISOWeek.GetWeekOfYear(date1) != ISOWeek.GetWeekOfYear(date2); + #endregion + + #region Database Extension + // processing flags, prevents double task + private bool _isDbSyncing; + private bool _isDbPulling; + private bool _isDbPullSuccess; + + public DateTime LastDbUpdate = DateTime.MinValue; + + // value store + private string? _jsonDataDb; + private double? _totalTimeDb; + private double? _lastPlayedDb; + private int? _unixStampDb; + + // Key names + private string KeyPlaytimeJson => $"{_gameVersion.GameType.ToString()}-{_gameVersion.GameRegion}-pt-js"; + private string KeyTotalTime => $"{_gameVersion.GameType.ToString()}-{_gameVersion.GameRegion}-pt-total"; + private string KeyLastPlayed => $"{_gameVersion.GameType.ToString()}-{_gameVersion.GameRegion}-pt-lastPlayed"; + private string KeyLastUpdated => $"{_gameVersion.GameType.ToString()}-{_gameVersion.GameRegion}-pt-lu"; + + + #region Sync Methods + /// + /// Sync from/to DB at init + /// + /// true if require refresh, false if dont. + public async ValueTask<(bool IsUpdated, CollapsePlaytime? PlaytimeData)> DbSync() + { + LogWriteLine("[CollapsePlaytime::DbSync] Starting sync operation...", LogType.Default, true); + try + { + // Fetch database last update stamp + var stampDbStr = await DbHandler.QueryKey(KeyLastUpdated); + _unixStampDb = !string.IsNullOrEmpty(stampDbStr) ? Convert.ToInt32(stampDbStr) : null; + + // Compare unix stamp from config + var unixStampLocal = Convert.ToInt32(DbConfig.GetConfig(KeyLastUpdated).ToString()); + if (_unixStampDb == unixStampLocal) + { + LogWriteLine("[CollapsePlaytime::DbSync] Sync stamp equal, nothing needs to be done~", LogType.Default, true); + return (false, null); // Do nothing if stamp is equal + } + + // When Db stamp is newer, sync from Db + if (_unixStampDb > unixStampLocal) + { + // Pull values from DB + await UpdatePlaytime_Database_Pull(); + if (!_isDbPullSuccess) + { + LogWriteLine("[CollapsePlaytime::DbSync] Database pull failed, skipping sync~", LogType.Error); + return (false, null); // Return if pull failed + } + + if (string.IsNullOrEmpty(_jsonDataDb)) + { + LogWriteLine("[CollapsePlaytime::DbSync] _jsonDataDb is empty, skipping sync~", default, true); + return (false, null); + } + LogWriteLine("[CollapsePlaytime::DbSync] Database data is newer! Pulling data~", LogType.Default, true); + CollapsePlaytime? playtimeInner = _jsonDataDb.Deserialize(UniversalPlaytimeJSONContext.Default.CollapsePlaytime, + new CollapsePlaytime()); + + playtimeInner!.TotalPlaytime = TimeSpan.FromSeconds(_totalTimeDb ?? 0); + if (_lastPlayedDb != null) playtimeInner.LastPlayed = BaseDate.AddSeconds((int)_lastPlayedDb); + playtimeInner._registryRoot = _registryRoot; + playtimeInner._gameSettings = _gameSettings; + playtimeInner._hashID = _hashID; + playtimeInner._gameVersion = _gameVersion; + + playtimeInner.CheckStatsReset(); + playtimeInner.Save(); + + DbConfig.SetAndSaveValue(KeyLastUpdated, _unixStampDb.ToString()); + LastDbUpdate = DateTime.Now; + + return (true, playtimeInner); + } + + if (_unixStampDb < unixStampLocal) + { + LogWriteLine("[CollapsePlaytime::DbSync] Database data is older! Pushing data~", default, true); + Save(true); + } + return (false, null); + } + catch (Exception ex) + { + LogWriteLine($"[CollapsePlaytime::DbSync] Failed when trying to do sync operation\r\n{ex}", + LogType.Error, true); + return (false, null); + } + } + #endregion + + #region DB Operation Methods + private async Task UpdatePlaytime_Database_Push(string jsonData, double totalTime, double? lastPlayed) + { + if (_isDbSyncing) return; + var curDateTime = DateTime.Now; + _isDbSyncing = true; + try + { + var unixStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + await DbHandler.StoreKeyValue(KeyPlaytimeJson, jsonData); + await DbHandler.StoreKeyValue(KeyTotalTime, totalTime.ToString(CultureInfo.InvariantCulture)); + await DbHandler.StoreKeyValue(KeyLastPlayed, lastPlayed != null ? lastPlayed.Value.ToString(CultureInfo.InvariantCulture) : "null"); + await DbHandler.StoreKeyValue(KeyLastUpdated, unixStamp.ToString()); + DbConfig.SetAndSaveValue(KeyLastUpdated, unixStamp); + _unixStampDb = Convert.ToInt32(unixStamp); + LastDbUpdate = curDateTime; + } + catch (Exception e) + { + LogWriteLine($"Failed when syncing Playtime to DB!\r\n{e}", LogType.Error, true); + } + finally + { + _isDbSyncing = false; + } + } + + private async Task UpdatePlaytime_Database_Pull() + { + if (_isDbPulling) return; + _isDbPullSuccess = false; + try + { + _isDbPulling = true; + + _jsonDataDb = await DbHandler.QueryKey(KeyPlaytimeJson); + + var totalTimeDbStr = await DbHandler.QueryKey(KeyTotalTime); + _totalTimeDb = string.IsNullOrEmpty(totalTimeDbStr) ? null : Convert.ToDouble(totalTimeDbStr, CultureInfo.InvariantCulture); + + var lpDb = await DbHandler.QueryKey(KeyLastPlayed); + _lastPlayedDb = !string.IsNullOrEmpty(lpDb) && !lpDb.Contains("null") ? Convert.ToDouble(lpDb, CultureInfo.InvariantCulture) : null; // if Db data is null, return null + + _isDbPullSuccess = true; + } + catch (Exception e) + { + LogWriteLine($"Failed when syncing Playtime to DB!\r\n{e}", LogType.Error, true); + } + finally + { + _isDbPulling = false; + } + } + #endregion + #endregion + } +} diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs index 22b3888be..f03ed7616 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs @@ -33,7 +33,7 @@ internal class ImportExportBase using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) { byte[] head = new byte[6]; - fs.Read(head, 0, head.Length); + _ = fs.Read(head, 0, head.Length); Logger.LogWriteLine($"Importing registry {RegistryPath}..."); @@ -67,10 +67,10 @@ private void ReadNewerValues(Stream fs, string? gameBasePath) { case 1: using (XORStream xorS = new XORStream(fs, xorKey, true)) - using (BrotliStream comp = new BrotliStream(xorS, CompressionMode.Decompress, true)) - { - ReadLegacyValues(comp); - } + using (BrotliStream comp = new BrotliStream(xorS, CompressionMode.Decompress, true)) + { + ReadLegacyValues(comp); + } break; case 2: ReadV2Values(fs); @@ -174,7 +174,7 @@ private void ReadV3Values(Stream fs, string? gameBasePath) foreach (string valueName in names) { - object? val = RegistryRoot!.GetValue(valueName); + object? val = RegistryRoot.GetValue(valueName); RegistryValueKind valueType = RegistryRoot.GetValueKind(valueName); Logger.LogWriteLine($"Writing value V3 {valueName}..."); @@ -196,7 +196,7 @@ private void ReadV3Values(Stream fs, string? gameBasePath) return null; } - private unsafe void EnsureReadImpostorData(Stream stream, string valueName) + private void EnsureReadImpostorData(Stream stream, string valueName) { int sizeOf = ReadValueKindAndSize(stream, out RegistryValueKind realValueType); object result = realValueType switch @@ -205,7 +205,7 @@ private unsafe void EnsureReadImpostorData(Stream stream, string valueName) RegistryValueKind.DWord => ReadTypeNumberUnsafe(stream, sizeOf), RegistryValueKind.String => ReadTypeString(stream, sizeOf), RegistryValueKind.Binary => WriteTypeBinary(stream, sizeOf), - _ => throw new InvalidCastException($"Cast of the object cannot be determined!") + _ => throw new InvalidCastException("Cast of the object cannot be determined!") }; if (realValueType == RegistryValueKind.QWord && sizeOf == sizeof(int) @@ -222,17 +222,10 @@ private unsafe void EnsureReadImpostorData(Stream stream, string valueName) result = numberAsBuffer; } - try - { - RegistryRoot?.SetValue(valueName, result, realValueType); - } - catch - { - throw; - } + RegistryRoot?.SetValue(valueName, result, realValueType); } - private unsafe void EnsureWriteImpostorData(Stream stream, RegistryValueKind realValueType, object? value) + private void EnsureWriteImpostorData(Stream stream, RegistryValueKind realValueType, object? value) { Type valueType = value!.GetType(); if (valueType == typeof(int) || valueType == typeof(float)) @@ -275,7 +268,7 @@ private unsafe object ReadTypeNumberUnsafe(Stream stream, int sizeOf) private unsafe void WriteTypeNumberUnsafe(Stream stream, ref T value, int sizeOf, RegistryValueKind realValueType) where T : struct { - ReadOnlySpan byteNumberAsSpan = new ReadOnlySpan((byte*)Unsafe.AsPointer(ref value), sizeOf); + ReadOnlySpan byteNumberAsSpan = new ReadOnlySpan((byte*)Unsafe.AsPointer(ref value), sizeOf); WriteValueKindAndSize(stream, sizeOf, realValueType); WriteToStream(stream, byteNumberAsSpan); } @@ -336,7 +329,7 @@ private void ImportStreamToFiles(Stream readStream, string? gamePath) } } - private unsafe string ReadTypeString(Stream stream, int length) + private string ReadTypeString(Stream stream, int length) { bool isUsePool = length <= 64 << 10; byte[] buffer = isUsePool ? ArrayPool.Shared.Rent(length) : new byte[length]; @@ -352,7 +345,7 @@ private unsafe string ReadTypeString(Stream stream, int length) } } - private unsafe void WriteTypeString(Stream stream, ReadOnlySpan stringSpan, RegistryValueKind realValueType) + private void WriteTypeString(Stream stream, ReadOnlySpan stringSpan, RegistryValueKind realValueType) { bool isUsePool = stringSpan.Length <= 64 << 10; byte[] buffer = isUsePool ? ArrayPool.Shared.Rent(stringSpan.Length) : new byte[stringSpan.Length]; @@ -404,7 +397,7 @@ private void EnsureFileSaveHasExtension(ref string path, string exte) { if (string.IsNullOrEmpty(path)) return; string ext = Path.GetExtension(path); - if (ext == null) return; + if (string.IsNullOrEmpty(ext)) { path += exte; @@ -432,7 +425,7 @@ private string ReadValueName(Stream stream) byte[] buffer = ArrayPool.Shared.Rent(length); try { - stream.Read(buffer, 0, length); + _ = stream.Read(buffer, 0, length); return Encoding.UTF8.GetString(buffer, 0, length); } finally @@ -441,24 +434,26 @@ private string ReadValueName(Stream stream) } } + // ReSharper disable once UnusedMember.Local + // just in case, i aint wanna dig around commit history if somehow this needed in the future private long ReadInt64(Stream stream) { Span buffer = stackalloc byte[4]; - stream.Read(buffer); + _ = stream.Read(buffer); return MemoryMarshal.Read(buffer); } private int ReadInt32(Stream stream) { Span buffer = stackalloc byte[4]; - stream.Read(buffer); + _ = stream.Read(buffer); return MemoryMarshal.Read(buffer); } private short ReadInt16(Stream stream) { Span buffer = stackalloc byte[2]; - stream.Read(buffer); + _ = stream.Read(buffer); return MemoryMarshal.Read(buffer); } @@ -486,7 +481,7 @@ private void ReadBinary(EndianBinaryReader reader, string valueName) { int leng = reader.ReadInt32(); byte[] val = new byte[leng]; - reader.Read(val, 0, leng); + _ = reader.Read(val, 0, leng); RegistryRoot?.SetValue(valueName, val, RegistryValueKind.Binary); } } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs index 9e1848897..752c72acd 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs @@ -289,14 +289,14 @@ internal class MagicNodeBaseValues : IGameSettingsValueMagic public JsonNode? SettingsJsonNode { get; protected set; } [JsonIgnore] - public JsonSerializerContext Context { get; protected set; } + public JsonTypeInfo TypeInfo { get; protected set; } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. [Obsolete("Loading settings with Load() is not supported for IGameSettingsValueMagic member. Use LoadWithMagic() instead!", true)] public static T Load() => throw new NotSupportedException("Loading settings with Load() is not supported for IGameSettingsValueMagic member. Use LoadWithMagic() instead!"); - public static T LoadWithMagic(byte[] magic, SettingsGameVersionManager versionManager, JsonSerializerContext context) + public static T LoadWithMagic(byte[] magic, SettingsGameVersionManager versionManager, JsonTypeInfo typeInfo) { if (magic == null || magic.Length == 0) throw new NullReferenceException($"Magic cannot be an empty array!"); @@ -314,28 +314,28 @@ public static T LoadWithMagic(byte[] magic, SettingsGameVersionManager versionMa #endif JsonNode? node = raw.DeserializeAsJsonNode(); T data = new T(); - data.InjectNodeAndMagic(node, magic, versionManager, context); + data.InjectNodeAndMagic(node, magic, versionManager, typeInfo); return data; } catch (Exception ex) { Logger.LogWriteLine($"Failed to parse MagicNodeBaseValues settings\r\n{ex}", LogType.Error, true); - return DefaultValue(magic, versionManager, context); + return DefaultValue(magic, versionManager, typeInfo); } } - private static T DefaultValue(byte[] magic, SettingsGameVersionManager versionManager, JsonSerializerContext context) + private static T DefaultValue(byte[] magic, SettingsGameVersionManager versionManager, JsonTypeInfo typeInfo) { // Generate dummy data T data = new T(); // Generate raw JSON string - string rawJson = data.Serialize(context, false, false); + string rawJson = data.Serialize(typeInfo, false, false); // Deserialize it back to JSON Node and inject // the node and magic JsonNode? defaultJsonNode = rawJson.DeserializeAsJsonNode(); - data.InjectNodeAndMagic(defaultJsonNode, magic, versionManager, context); + data.InjectNodeAndMagic(defaultJsonNode, magic, versionManager, typeInfo); // Return return data; @@ -352,18 +352,18 @@ public void Save() Directory.CreateDirectory(fileDirPath!); // Write into the file - string jsonString = SettingsJsonNode.SerializeJsonNode(Context, false, false); + string jsonString = SettingsJsonNode.SerializeJsonNode(TypeInfo, false, false); Sleepy.WriteString(filePath, jsonString, Magic); } public bool Equals(T? other) => JsonNode.DeepEquals(this.SettingsJsonNode, other?.SettingsJsonNode); - protected virtual void InjectNodeAndMagic(JsonNode? jsonNode, byte[] magic, SettingsGameVersionManager versionManager, JsonSerializerContext context) + protected virtual void InjectNodeAndMagic(JsonNode? jsonNode, byte[] magic, SettingsGameVersionManager versionManager, JsonTypeInfo typeInfo) { SettingsJsonNode = jsonNode; GameVersionManager = versionManager; Magic = magic; - Context = context; + TypeInfo = typeInfo; } } } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/SettingsBase.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/SettingsBase.cs index 7c0dcac4e..856d772c8 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/SettingsBase.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/SettingsBase.cs @@ -5,7 +5,7 @@ namespace CollapseLauncher.GameSettings.Base { - internal class SettingsBase : ImportExportBase, IGameSettings, IGameSettingsUniversal + internal class SettingsBase : ImportExportBase, IGameSettings { #region Base Properties public virtual CustomArgs SettingsCustomArgument { get; set; } @@ -15,13 +15,11 @@ internal class SettingsBase : ImportExportBase, IGameSettings, IGameSettingsUniv #endregion #nullable enable - private static string? _registryPath = null; - private static RegistryKey? _registryRoot = null; + private static RegistryKey? _registryRoot; internal static string? RegistryPath { - get => _registryPath = string.IsNullOrEmpty(_gameVersionManager?.GamePreset?.InternalGameNameInConfig) ? - null : + get => string.IsNullOrEmpty(_gameVersionManager?.GamePreset?.InternalGameNameInConfig) ? null : Path.Combine($"Software\\{_gameVersionManager.VendorTypeProp.VendorType}", _gameVersionManager.GamePreset.InternalGameNameInConfig); } @@ -42,7 +40,7 @@ internal static RegistryKey? RegistryRoot } } - public SettingsBase(IGameVersionCheck GameVersionManager) => _gameVersionManager = GameVersionManager; + protected SettingsBase(IGameVersionCheck GameVersionManager) => _gameVersionManager = GameVersionManager; public virtual void InitializeSettings() { @@ -52,11 +50,13 @@ public virtual void InitializeSettings() } #nullable disable - protected static IGameVersionCheck _gameVersionManager { get; set; } + public static IGameVersionCheck _gameVersionManager { get; set; } public virtual void ReloadSettings() => InitializeSettings(); - public virtual void SaveSettings() + public virtual void SaveSettings() => SaveBaseSettings(); + + public void SaveBaseSettings() { SettingsCustomArgument.Save(); SettingsCollapseScreen.Save(); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/Enums/Enums.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/Enums/Enums.cs index cf0624950..54108d6f2 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/Enums/Enums.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/Enums/Enums.cs @@ -19,7 +19,7 @@ internal static class DictionaryCategory }; } - enum FPSOption : int + enum FPSOption { f30, f60, diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GeneralData.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GeneralData.cs index 3234d1947..2c4a42d46 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GeneralData.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GeneralData.cs @@ -17,15 +17,15 @@ internal class GeneralData #region Fields private const string _ValueName = "GENERAL_DATA_h2389025596"; - // Default GENERAL_DATA_h2389025596 value taken from version 4.6.0 + // Default GENERAL_DATA_h2389025596 value taken from version 5.0.0 // For the next person, if there is any addition/deletion to the GeneralData props, please update this :) // Just delete the key from registry (back it up first) then run the game. After server selection is set then you will see this registry appear. // Open Collapse Genshin GSP in debug mode and you shall see the raw value. Paste them here. // Thank you. // Sincerely, bagel 🥯 // (hoyo why you did this to us) - private static readonly string _generalDataDefault = - "{\"deviceUUID\":\"\",\"userLocalDataVersionId\":\"\",\"deviceLanguageType\":1,\"deviceVoiceLanguageType\":1,\"selectedServerName\":\"os_usa\",\"localLevelIndex\":0,\"deviceID\":\"\",\"targetUID\":\"\",\"curAccountName\":\"\",\"uiSaveData\":\"\",\"inputData\":\"{\\\"scriptVersion\\\":\\\"OSRELWin4.8.0\\\",\\\"mouseSensitivity\\\":10.0,\\\"joypadSenseIndex\\\":2,\\\"joypadFocusSenseIndex\\\":2,\\\"joypadInvertCameraX\\\":false,\\\"joypadInvertCameraY\\\":false,\\\"joypadInvertFocusCameraX\\\":false,\\\"joypadInvertFocusCameraY\\\":false,\\\"mouseSenseIndex\\\":2,\\\"mouseFocusSenseIndex\\\":2,\\\"touchpadSenseIndex\\\":2,\\\"touchpadFocusSenseIndex\\\":5,\\\"enableTouchpadFocusAcceleration\\\":false,\\\"lastJoypadDefaultScale\\\":1.0,\\\"lastJoypadFocusScale\\\":1.0,\\\"lastPCDefaultScale\\\":0.75,\\\"lastPCFocusScale\\\":1.0,\\\"lastTouchDefaultScale\\\":1.0,\\\"lastTouchFcousScale\\\":1.0,\\\"switchWalkRunByBtn\\\":false,\\\"skiffCameraAutoFix\\\":true,\\\"skiffCameraAutoFixInCombat\\\":false,\\\"cameraDistanceRatio\\\":0.0,\\\"wwiseVibration\\\":true,\\\"isYInited\\\":true,\\\"joypadSenseIndexY\\\":2,\\\"joypadFocusSenseIndexY\\\":2,\\\"mouseSenseIndexY\\\":2,\\\"mouseFocusSenseIndexY\\\":2,\\\"touchpadSenseIndexY\\\":2,\\\"touchpadFocusSenseIndexY\\\":5,\\\"lastJoypadDefaultScaleY\\\":1.0,\\\"lastJoypadFocusScaleY\\\":1.0,\\\"lastPCDefaultScaleY\\\":0.75,\\\"lastPCFocusScaleY\\\":1.0,\\\"lastTouchDefaultScaleY\\\":1.0,\\\"lastTouchFcousScaleY\\\":1.0}\",\"graphicsData\":\"{\\\"currentVolatielGrade\\\":4,\\\"customVolatileGrades\\\":[],\\\"volatileVersion\\\":\\\"OSRELWin4.8.0\\\"}\",\"globalPerfData\":\"{\\\"saveItems\\\":[],\\\"truePortedFromGraphicData\\\":true,\\\"portedVersion\\\":\\\"OSRELWin4.8.0\\\",\\\"volatileUpgradeVersion\\\":0,\\\"portedFromGraphicData\\\":false}\",\"miniMapConfig\":1,\"enableCameraSlope\":true,\"enableCameraCombatLock\":true,\"completionPkg\":false,\"completionPlayGoPkg\":false,\"onlyPlayWithPSPlayer\":false,\"needPlayGoFullPkgPatch\":false,\"resinNotification\":true,\"exploreNotification\":true,\"volumeGlobal\":10,\"volumeSFX\":10,\"volumeMusic\":10,\"volumeVoice\":10,\"audioAPI\":-1,\"audioDynamicRange\":0,\"audioOutput\":0,\"_audioSuccessInit\":true,\"enableAudioChangeAndroidMinimumBufferCapacity\":true,\"audioAndroidMiniumBufferCapacity\":2048,\"vibrationLevel\":0,\"vibrationIntensity\":3,\"usingNewVibrationSetting\":true,\"motionBlur\":true,\"gyroAiming\":false,\"gyroHorMoveSpeedIndex\":2,\"gyroVerMoveSpeedIndex\":2,\"gyroHorReverse\":false,\"gyroVerReverse\":false,\"gyroRotateType\":0,\"gyroExcludeRightStickVerInput\":false,\"firstHDRSetting\":true,\"maxLuminosity\":0.0,\"uiPaperWhite\":0.0,\"scenePaperWhite\":0.0,\"gammaValue\":2.20000004768372,\"enableHDR\":false,\"_overrideControllerMapKeyList\":[],\"_overrideControllerMapValueList\":[],\"rewiredMapMigrateRecord\":[],\"rewiredDisableKeyboard\":false,\"rewiredEnableKeyboard\":false,\"rewiredEnableEDS\":false,\"disableRewiredDelayInit\":false,\"disableRewiredInitProtection\":false,\"conflictKeyBindingElementId\":[],\"conflictKeyBindingActionId\":[],\"lastSeenPreDownloadTime\":0,\"lastSeenSettingResourceTabScriptVersion\":\"\",\"enableEffectAssembleInEditor\":true,\"forceDisableQuestResourceManagement\":false,\"needReportQuestResourceDeleteStatusFiles\":false,\"disableTeamPageBackgroundSwitch\":false,\"disableHttpDns\":false,\"mtrCached\":true,\"mtrIsOpen\":false,\"mtrMaxTTL\":32,\"mtrTimeOut\":5000,\"mtrTraceCount\":5,\"mtrAbortTimeOutCount\":3,\"mtrAutoTraceInterval\":3600,\"mtrTraceCDEachReason\":600,\"mtrTimeInterval\":1000,\"mtrBanReasons\":[],\"_customDataKeyList\":[],\"_customDataValueList\":[],\"_serializedCodeSwitches\":[],\"urlCheckCached\":true,\"urlCheckIsOpen\":false,\"urlCheckAllIP\":false,\"urlCheckTimeOut\":5000,\"urlCheckSueecssTraceCount\":5,\"urlCheckErrorTraceCount\":30,\"urlCheckAbortTimeOutCount\":3,\"urlCheckTimeInterval\":1000,\"urlCheckCDEachReason\":600,\"urlCheckBanReasons\":[],\"mtrUseOldWinVersion\":false,\"greyTestDeviceUniqueId\":\"\",\"muteAudioOnAppMinimized\":false,\"disableFallbackControllerType\":false,\"lastShowDoorProgress\":-1.0,\"globalPerfSettingVersion\":2}"; + private static readonly string _generalDataDefault = + "{\"deviceUUID\":\"\",\"userLocalDataVersionId\":\"\",\"deviceLanguageType\":1,\"deviceVoiceLanguageType\":1,\"selectedServerName\":\"\",\"localLevelIndex\":0,\"deviceID\":\"\",\"targetUID\":\"\",\"curAccountName\":\"\",\"uiSaveData\":\"\",\"inputData\":\"{\\\"scriptVersion\\\":\\\"OSRELWin5.0.0\\\",\\\"mouseSensitivity\\\":10.0,\\\"joypadSenseIndex\\\":2,\\\"joypadFocusSenseIndex\\\":2,\\\"joypadInvertCameraX\\\":false,\\\"joypadInvertCameraY\\\":false,\\\"joypadInvertFocusCameraX\\\":false,\\\"joypadInvertFocusCameraY\\\":false,\\\"mouseSenseIndex\\\":2,\\\"mouseFocusSenseIndex\\\":2,\\\"touchpadSenseIndex\\\":2,\\\"touchpadFocusSenseIndex\\\":5,\\\"enableTouchpadFocusAcceleration\\\":false,\\\"lastJoypadDefaultScale\\\":1.0,\\\"lastJoypadFocusScale\\\":1.0,\\\"lastPCDefaultScale\\\":0.75,\\\"lastPCFocusScale\\\":1.0,\\\"lastTouchDefaultScale\\\":1.0,\\\"lastTouchFcousScale\\\":1.0,\\\"switchWalkRunByBtn\\\":false,\\\"skiffCameraAutoFix\\\":true,\\\"skiffCameraAutoFixInCombat\\\":false,\\\"cameraDistanceRatio\\\":0.0,\\\"wwiseVibration\\\":true,\\\"isYInited\\\":true,\\\"joypadSenseIndexY\\\":2,\\\"joypadFocusSenseIndexY\\\":2,\\\"mouseSenseIndexY\\\":2,\\\"mouseFocusSenseIndexY\\\":2,\\\"touchpadSenseIndexY\\\":2,\\\"touchpadFocusSenseIndexY\\\":5,\\\"lastJoypadDefaultScaleY\\\":1.0,\\\"lastJoypadFocusScaleY\\\":1.0,\\\"lastPCDefaultScaleY\\\":0.75,\\\"lastPCFocusScaleY\\\":1.0,\\\"lastTouchDefaultScaleY\\\":1.0,\\\"lastTouchFcousScaleY\\\":1.0}\",\"graphicsData\":\"{\\\"currentVolatielGrade\\\":4,\\\"customVolatileGrades\\\":[],\\\"volatileVersion\\\":\\\"OSRELWin5.0.0\\\"}\",\"globalPerfData\":\"{\\\"saveItems\\\":[],\\\"truePortedFromGraphicData\\\":true,\\\"portedVersion\\\":\\\"OSRELWin5.0.0\\\",\\\"volatileUpgradeVersion\\\":0,\\\"portedFromGraphicData\\\":false}\",\"miniMapConfig\":1,\"enableCameraSlope\":true,\"enableCameraCombatLock\":true,\"completionPkg\":false,\"completionPlayGoPkg\":false,\"onlyPlayWithPSPlayer\":false,\"needPlayGoFullPkgPatch\":false,\"resinNotification\":true,\"exploreNotification\":true,\"volumeGlobal\":10,\"volumeSFX\":10,\"volumeMusic\":10,\"volumeVoice\":10,\"audioAPI\":-1,\"audioDynamicRange\":0,\"audioOutput\":0,\"_audioSuccessInit\":true,\"enableAudioChangeAndroidMinimumBufferCapacity\":true,\"audioAndroidMiniumBufferCapacity\":2048,\"vibrationLevel\":0,\"vibrationIntensity\":3,\"usingNewVibrationSetting\":true,\"motionBlur\":true,\"gyroAiming\":false,\"gyroHorMoveSpeedIndex\":2,\"gyroVerMoveSpeedIndex\":2,\"gyroHorReverse\":false,\"gyroVerReverse\":false,\"gyroRotateType\":0,\"gyroExcludeRightStickVerInput\":false,\"firstHDRSetting\":true,\"maxLuminosity\":0.0,\"uiPaperWhite\":0.0,\"scenePaperWhite\":0.0,\"gammaValue\":2.200000047683716,\"enableHDR\":false,\"_overrideControllerMapKeyList\":[],\"_overrideControllerMapValueList\":[],\"rewiredMapMigrateRecord\":[],\"rewiredDisableKeyboard\":false,\"rewiredEnableKeyboard\":false,\"rewiredEnableEDS\":false,\"disableRewiredDelayInit\":false,\"disableRewiredInitProtection\":false,\"conflictKeyBindingElementId\":[],\"conflictKeyBindingActionId\":[],\"lastSeenPreDownloadTime\":0,\"lastSeenSettingResourceTabScriptVersion\":\"\",\"enableEffectAssembleInEditor\":true,\"forceDisableQuestResourceManagement\":false,\"needReportQuestResourceDeleteStatusFiles\":false,\"disableTeamPageBackgroundSwitch\":false,\"disableHttpDns\":false,\"mtrCached\":false,\"mtrIsOpen\":false,\"mtrMaxTTL\":32,\"mtrTimeOut\":5000,\"mtrTraceCount\":5,\"mtrAbortTimeOutCount\":3,\"mtrAutoTraceInterval\":0,\"mtrTraceCDEachReason\":600,\"mtrTimeInterval\":1000,\"mtrBanReasons\":[],\"_customDataKeyList\":[],\"_customDataValueList\":[],\"_serializedCodeSwitches\":[],\"urlCheckCached\":false,\"urlCheckIsOpen\":false,\"urlCheckAllIP\":false,\"urlCheckTimeOut\":5000,\"urlCheckSueecssTraceCount\":5,\"urlCheckErrorTraceCount\":30,\"urlCheckAbortTimeOutCount\":3,\"urlCheckTimeInterval\":1000,\"urlCheckCDEachReason\":600,\"urlCheckBanReasons\":[],\"mtrUseOldWinVersion\":false,\"greyTestDeviceUniqueId\":\"\",\"muteAudioOnAppMinimized\":false,\"disableFallbackControllerType\":false,\"lastShowDoorProgress\":-1.0,\"globalPerfSettingVersion\":2}"; #endregion #region Properties @@ -296,20 +296,20 @@ internal class GeneralData public bool disableTeamPageBackgroundSwitch { get; set; } = false; public bool disableHttpDns { get; set; } = false; - public bool mtrCached { get; set; } = true; + public bool mtrCached { get; set; } = false; public bool mtrIsOpen { get; set; } = true; public int mtrMaxTTL { get; set; } = 32; public int mtrTimeOut { get; set; } = 5000; public int mtrTraceCount { get; set; } = 5; public int mtrAbortTimeOutCount { get; set; } = 3; - public int mtrAutoTraceInterval { get; set; } = 3600; + public int mtrAutoTraceInterval { get; set; } = 0; public int mtrTraceCDEachReason { get; set; } = 600; public int mtrTimeInterval { get; set; } = 1000; public List mtrBanReasons { get; set; } public List _customDataKeyList { get; set; } public List _customDataValueList { get; set; } public List _serializedCodeSwitches { get; set; } - public bool urlCheckCached { get; set; } = true; + public bool urlCheckCached { get; set; } = false; public bool urlCheckIsOpen { get; set; } = false; public bool urlCheckAllIP { get; set; } = false; public int urlCheckTimeOut { get; set; } = 5000; @@ -351,15 +351,15 @@ public static GeneralData Load() // Dump GeneralData as indented JSON output using GeneralData properties LogWriteLine($"Deserialized Genshin Settings: {_ValueName}\r\n{byteStr - .Deserialize(GenshinSettingsJSONContext.Default) - .Serialize(GenshinSettingsJSONContext.Default, false, true)}", LogType.Debug, true); + .Deserialize(GenshinSettingsJSONContext.Default.GeneralData) + .Serialize(GenshinSettingsJSONContext.Default.GeneralData, false, true)}", LogType.Debug, true); #endif #if DEBUG LogWriteLine($"Loaded Genshin Settings: {_ValueName}", LogType.Debug, true); #else LogWriteLine($"Loaded Genshin Settings", LogType.Default, true); #endif - GeneralData data = byteStr.Deserialize(GenshinSettingsJSONContext.Default) ?? new GeneralData(); + GeneralData data = byteStr.Deserialize(GenshinSettingsJSONContext.Default.GeneralData) ?? new GeneralData(); if (data._graphicsData != null) data.graphicsData = GraphicsData.Load(data._graphicsData); if (data._globalPerfData != null) data.globalPerfData = GlobalPerfData.Load(data._globalPerfData, data.graphicsData)!; @@ -376,7 +376,7 @@ public static GeneralData Load() $"Unless you have never opened the game (fresh installation), please open the game and change any settings, then safely close the game. If the problem persist, report the issue on our GitHub\r\n\r\n" + $"{ex}", ex)); - GeneralData data = _generalDataDefault!.Deserialize(GenshinSettingsJSONContext.Default) ?? new GeneralData(); + GeneralData data = _generalDataDefault!.Deserialize(GenshinSettingsJSONContext.Default.GeneralData) ?? new GeneralData(); data.graphicsData = GraphicsData.Load(data._graphicsData!); data.globalPerfData = GlobalPerfData.Load(data._globalPerfData!, data.graphicsData)!; return data; @@ -392,13 +392,13 @@ public void Save() _graphicsData = graphicsData!.Create(globalPerfData!); _globalPerfData = globalPerfData!.Save()!; - string data = this.Serialize(GenshinSettingsJSONContext.Default); + string data = this.Serialize(GenshinSettingsJSONContext.Default.GeneralData); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); #if DUMPGIJSON //Dump saved GeneralData JSON from Collapse as indented output - LogWriteLine($"Saved Genshin Settings: {_ValueName}\r\n{this.Serialize(GenshinSettingsJSONContext.Default, false, true)}", LogType.Debug, true); + LogWriteLine($"Saved Genshin Settings: {_ValueName}\r\n{this.Serialize(GenshinSettingsJSONContext.Default.GeneralData, false, true)}", LogType.Debug, true); #endif #if DEBUG LogWriteLine($"Saved Genshin Settings: {_ValueName}" + diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GlobalPerfData.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GlobalPerfData.cs index a137cf607..7e11a3ace 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GlobalPerfData.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GlobalPerfData.cs @@ -307,7 +307,7 @@ public static GlobalPerfData Load(string globalPerfJson, GraphicsData graphics) } else { - tempData = globalPerfJson.Deserialize(GenshinSettingsJSONContext.Default) ?? new GlobalPerfData(); + tempData = globalPerfJson.Deserialize(GenshinSettingsJSONContext.Default.GlobalPerfData) ?? new GlobalPerfData(); } // Initialize globalPerf with a preset @@ -492,7 +492,7 @@ public string Save() new PerfDataItem(19, (int)GlobalIllumination, portedVersion), new PerfDataItem(21, (int)DynamicCharacterResolution, portedVersion), }; - string data = this.Serialize(GenshinSettingsJSONContext.Default, false); + string data = this.Serialize(GenshinSettingsJSONContext.Default.GlobalPerfData, false); #if DEBUG LogWriteLine($"Saved Genshin GlobalPerfData\r\n{data}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GraphicsData.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GraphicsData.cs index ef8ba8a26..7f4bfd12b 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GraphicsData.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/GraphicsData.cs @@ -17,7 +17,7 @@ internal class GraphicsData #nullable enable public static GraphicsData Load(string graphicsJson) { - return graphicsJson.Deserialize(GenshinSettingsJSONContext.Default) ?? new GraphicsData(); + return graphicsJson.Deserialize(GenshinSettingsJSONContext.Default.GraphicsData) ?? new GraphicsData(); } public string Create(GlobalPerfData globalPerf) @@ -45,13 +45,12 @@ public string Create(GlobalPerfData globalPerf) new GenshinKeyValuePair(21, (int)globalPerf.DynamicCharacterResolution + 1), }; - string data = this.Serialize(GenshinSettingsJSONContext.Default, false); + string data = this.Serialize(GenshinSettingsJSONContext.Default.GraphicsData, false); #if DEBUG LogWriteLine($"Saved Genshin GraphicsData\r\n{data}", LogType.Debug, true); #endif return data; } -#nullable disable #endregion } } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/VisibleBackground.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/VisibleBackground.cs index e5c5ccbbb..bfbc05925 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/VisibleBackground.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/VisibleBackground.cs @@ -19,7 +19,7 @@ internal class VisibleBackground /// Range: 0 - 1 /// Default: 0 /// - public int borderless { get; set; } = 0; + public int borderless { get; set; } /// /// Converted value from borderless integer inside UnityVisibleBackground registry to usable boolean. @@ -64,7 +64,7 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - RegistryRoot?.SetValue(_ValueName, borderless, RegistryValueKind.DWord); + RegistryRoot.SetValue(_ValueName, borderless, RegistryValueKind.DWord); #if DEBUG LogWriteLine($"Saved Genshin Settings: {_ValueName} : {borderless}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/WindowsHDR.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/WindowsHDR.cs index 7088528a2..9a3926050 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/WindowsHDR.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/WindowsHDR.cs @@ -19,7 +19,7 @@ internal class WindowsHDR /// Range: 0 - 1 /// Default: 0 /// - public int HDR { get; set; } = 0; + public int HDR { get; set; } /// /// Converted value from HDR integer inside WINDOWS_HDR_ON registry to usable boolean. @@ -64,7 +64,7 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - RegistryRoot?.SetValue(_ValueName, HDR, RegistryValueKind.DWord); + RegistryRoot.SetValue(_ValueName, HDR, RegistryValueKind.DWord); #if DEBUG LogWriteLine($"Saved Genshin Settings: {_ValueName} : {HDR}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/Settings.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/Settings.cs index c9923da08..44912cac6 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/Settings.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/Settings.cs @@ -32,14 +32,15 @@ public override void ReloadSettings() InitializeSettings(); } + #nullable enable public override void SaveSettings() { // Save Settings base.SaveSettings(); - SettingsScreen.Save(); - SettingsGeneralData.Save(); - SettingVisibleBackground.Save(); - SettingsWindowsHDR.Save(); + SettingsScreen?.Save(); + SettingsGeneralData?.Save(); + SettingVisibleBackground?.Save(); + SettingsWindowsHDR?.Save(); } public override IGameSettingsUniversal AsIGameSettingsUniversal() => this; diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Enums/Enums.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Enums/Enums.cs index 724fb2bf7..4e58360d1 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Enums/Enums.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Enums/Enums.cs @@ -5,10 +5,32 @@ namespace CollapseLauncher.GameSettings.Honkai.Enums { /// - /// This selection has 4 name types: Low (0), Middle (1), High (2), VHigh (3)
+ /// This selection has 10 name types: + /// - 0.6 (Quality06) + /// - 0.8 (Quality08) + /// - 0.9 (Quality09) + /// - 1.0 (Low) + /// - 1.1 (Quality11) + /// - 1.2 (Middle) + /// - 1.3 (Quality13) + /// - 1.4 (Quality14) + /// - 1.5 (High) + /// - 1.6 (VHigh) ///
[JsonConverter(typeof(JsonStringEnumConverter))] - internal enum SelectResolutionQuality { Low, Middle, High, VHigh } + internal enum SelectResolutionQuality + { + Quality06, + Quality08, + Quality09, + Low, + Quality11, + Middle, + Quality13, + Quality14, + High, + VHigh + } /// /// This selection has 5 name types: DISABLED (0), LOW (1), MIDDLE (2), HIGH (3), ULTRA (4)
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSetting.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSetting.cs index e70021a9e..34a7253a6 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSetting.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSetting.cs @@ -187,7 +187,7 @@ public static PersonalAudioSetting Load() #if DEBUG LogWriteLine($"Loaded HI3 Settings: {_ValueName}\r\n{Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); #endif - return byteStr.Deserialize(HonkaiSettingsJSONContext.Default) ?? new PersonalAudioSetting(); + return byteStr.Deserialize(HonkaiSettingsJSONContext.Default.PersonalAudioSetting) ?? new PersonalAudioSetting(); } } catch (Exception ex) @@ -211,7 +211,7 @@ public void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(HonkaiSettingsJSONContext.Default); + string data = this.Serialize(HonkaiSettingsJSONContext.Default.PersonalAudioSetting); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSettingVolume.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSettingVolume.cs index 72c515c3d..4ad2f1520 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSettingVolume.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalAudioSettingVolume.cs @@ -81,7 +81,7 @@ public static PersonalAudioSettingVolume Load() #if DEBUG LogWriteLine($"Loaded HI3 Settings: {_ValueName}\r\n{Encoding.UTF8.GetString((byte[])value, 0, ((byte[])value).Length - 1)}", LogType.Debug, true); #endif - return byteStr.Deserialize(HonkaiSettingsJSONContext.Default) ?? new PersonalAudioSettingVolume(); + return byteStr.Deserialize(HonkaiSettingsJSONContext.Default.PersonalAudioSettingVolume) ?? new PersonalAudioSettingVolume(); } } catch (Exception ex) @@ -104,7 +104,7 @@ public void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(HonkaiSettingsJSONContext.Default); + string data = this.Serialize(HonkaiSettingsJSONContext.Default.PersonalAudioSettingVolume); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalGraphicsSettingV2.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalGraphicsSettingV2.cs index 1b99d4920..c9c9a4f6e 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalGraphicsSettingV2.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/PersonalGraphicsSettingV2.cs @@ -23,7 +23,7 @@ internal class PersonalGraphicsSettingV2 : IGameSettingsValue /// This defines "Rendering Accuracy" combobox In-game settings -> Video.
- /// + ///
/// Default: Middle ///
public SelectResolutionQuality ResolutionQuality { get; set; } = SelectResolutionQuality.Middle; @@ -190,7 +190,7 @@ public static PersonalGraphicsSettingV2 Load() #if DEBUG LogWriteLine($"Loaded HI3 Settings: {_ValueName}\r\n{Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); #endif - return byteStr.Deserialize(HonkaiSettingsJSONContext.Default) ?? new PersonalGraphicsSettingV2(); + return byteStr.Deserialize(HonkaiSettingsJSONContext.Default.PersonalGraphicsSettingV2) ?? new PersonalGraphicsSettingV2(); } } catch (Exception ex) @@ -214,7 +214,7 @@ public void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(HonkaiSettingsJSONContext.Default); + string data = this.Serialize(HonkaiSettingsJSONContext.Default.PersonalGraphicsSettingV2); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Preset.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Preset.cs index ec7b20b18..6fb7082dc 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Preset.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/Preset.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using static CollapseLauncher.GameSettings.Base.SettingsBase; using static Hi3Helper.Shared.Region.LauncherConfig; @@ -16,12 +16,12 @@ internal class PresetConst public const string DefaultPresetName = "Custom"; } - internal class Preset where T1 : IGameSettingsValue where TObjectType : JsonSerializerContext + internal class Preset where T1 : IGameSettingsValue { #nullable enable #region Fields private string CurrentPresetName = PresetConst.DefaultPresetName; - private Dictionary? _Presets = null; + private Dictionary? _Presets; #endregion #region Properties @@ -51,11 +51,11 @@ public Dictionary? Presets #endregion #region Methods - public Preset(string presetJSONPath, TObjectType jsonContext) + public Preset(string presetJSONPath, JsonTypeInfo?> jsonType) { using (FileStream fs = new FileStream(presetJSONPath, FileMode.Open, FileAccess.Read)) { - Presets = fs.Deserialize>(jsonContext); + Presets = fs.Deserialize(jsonType); PresetKeys = GetPresetKeys(); } } @@ -66,10 +66,10 @@ public Preset(string presetJSONPath, TObjectType jsonContext) /// The type of the game /// JSON source generation context /// The instance of preset - public static Preset LoadPreset(GameNameType gameType, TObjectType jsonContext) + public static Preset LoadPreset(GameNameType gameType, JsonTypeInfo?> jsonType) { string presetPath = Path.Combine(AppFolder, $"Assets\\Presets\\{gameType}\\", $"{typeof(T1).Name}.json"); - return new Preset(presetPath, jsonContext); + return new Preset(presetPath, jsonType); } /// The key of the preset @@ -92,7 +92,7 @@ public static Preset LoadPreset(GameNameType gameType, TObjectT return result; } - /// Returns a List of the preset + /// Returns a List-string of the preset /// private List GetPresetKeys() { @@ -103,29 +103,24 @@ private List GetPresetKeys() return Presets.Keys.ToList(); } - + /// /// Set the preset name based on equality of the given value with the preset. If doesn't match, it will be set to DefaultPresetName /// /// The value to be compared with the preset /// If RegistryRoot is null - public void SetPresetKey(T1 value) + public void SetPresetKey(T1? value) { - string presetKey = PresetConst.DefaultPresetName; - string presetRegistryName = $"Preset_{typeof(T1).Name}"; - if (value != null) { KeyValuePair? foundPreset = Presets?.Where(x => x.Value.Equals(value)).FirstOrDefault(); if (foundPreset.HasValue) { - presetKey = CurrentPresetName = foundPreset.Value.Key; + CurrentPresetName = foundPreset.Value.Key; } - return; } - CurrentPresetName = PresetConst.DefaultPresetName; } @@ -163,6 +158,5 @@ public void SaveChanges() RegistryRoot.SetValue(presetRegistryName, CurrentPresetName, RegistryValueKind.String); } #endregion -#nullable disable } } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/ScreenSettingData.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/ScreenSettingData.cs index 566e73fdd..1b22264a0 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/ScreenSettingData.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/RegistryClass/ScreenSettingData.cs @@ -105,7 +105,7 @@ public static ScreenSettingData Load() #if DEBUG LogWriteLine($"Loaded HI3 Settings: {_ValueName}\r\n{Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); #endif - return byteStr.Deserialize(HonkaiSettingsJSONContext.Default) ?? new ScreenSettingData(); + return byteStr.Deserialize(HonkaiSettingsJSONContext.Default.ScreenSettingData) ?? new ScreenSettingData(); } } catch (Exception ex) @@ -129,7 +129,7 @@ public override void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(HonkaiSettingsJSONContext.Default); + string data = this.Serialize(HonkaiSettingsJSONContext.Default.ScreenSettingData); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/Settings.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/Settings.cs index 1e9e1adaf..c47446cb6 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/Settings.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Honkai/Settings.cs @@ -38,7 +38,7 @@ public sealed override void InitializeSettings() base.InitializeSettings(); // Load Preset - Preset_SettingsGraphics = Preset.LoadPreset(GameNameType.Honkai, HonkaiSettingsJSONContext.Default); + Preset_SettingsGraphics = Preset.LoadPreset(GameNameType.Honkai, HonkaiSettingsJSONContext.Default.DictionaryStringPersonalGraphicsSettingV2); } public override void ReloadSettings() => InitializeSettings(); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/BGMVolume.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/BGMVolume.cs index f60d14506..81835a026 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/BGMVolume.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/BGMVolume.cs @@ -61,7 +61,7 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - RegistryRoot?.SetValue(_ValueName, BGMVol, RegistryValueKind.DWord); + RegistryRoot.SetValue(_ValueName, BGMVol, RegistryValueKind.DWord); #if DEBUG LogWriteLine($"Saved StarRail Settings: {_ValueName} : {BGMVol}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalAudioLanguage.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalAudioLanguage.cs index 5520f01df..b26339a48 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalAudioLanguage.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalAudioLanguage.cs @@ -90,7 +90,7 @@ public void Save() if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); string data = LocalAudioLang + '\0'; byte[] dataByte = Encoding.UTF8.GetBytes(data); - RegistryRoot?.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); + RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); #if DEBUG LogWriteLine($"Saved StarRail Settings: {_ValueName} : {data}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalTextLanguage.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalTextLanguage.cs index 33c37592a..08dc1d605 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalTextLanguage.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/LocalTextLanguage.cs @@ -101,7 +101,7 @@ public void Save() if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); string data = LocalTextLang + '\0'; byte[] dataByte = Encoding.UTF8.GetBytes(data); - RegistryRoot?.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); + RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); #if DEBUG LogWriteLine($"Saved StarRail Settings: {_ValueName} : {data}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/MasterVolume.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/MasterVolume.cs index 9a1ca6dd8..3d295bf43 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/MasterVolume.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/MasterVolume.cs @@ -61,7 +61,7 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - RegistryRoot?.SetValue(_ValueName, MasterVol, RegistryValueKind.DWord); + RegistryRoot.SetValue(_ValueName, MasterVol, RegistryValueKind.DWord); #if DEBUG LogWriteLine($"Saved StarRail Settings: {_ValueName} : {MasterVol}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/Model.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/Model.cs index db0ff2871..0007a6f1e 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/Model.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/Model.cs @@ -1,5 +1,6 @@ using CollapseLauncher.GameSettings.StarRail.Context; using CollapseLauncher.Interfaces; +using CollapseLauncher.Pages; using Hi3Helper; using Hi3Helper.EncTool; using Microsoft.Win32; @@ -164,7 +165,7 @@ private static Dictionary GenerateStaticFPSIndexDict() /// Options: true, false
/// Default: false ///
- public bool EnableVSync { get; set; } = false; + public bool EnableVSync { get; set; } /// /// This defines "Render Scale" combobox In-game settings.
@@ -306,7 +307,7 @@ private static Model LoadCustom() LogWriteLine($"Loaded StarRail Settings: {_ValueName}\r\n{Encoding.UTF8.GetString((byte[])value, 0, ((byte[])value).Length - 1)}", LogType.Debug, true); #endif - return byteStr.Deserialize(StarRailSettingsJSONContext.Default) ?? new Model(); + return byteStr.Deserialize(StarRailSettingsJSONContext.Default.Model) ?? new Model(); } } catch (Exception ex) @@ -329,10 +330,17 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); + + if (StarRailGameSettingsPage.CheckAbTest()) + { + LogWriteLine("[StarRailGameSettings::Model] Graphics settings could not be saved due to A/B test flag is found!", + LogType.Error, true); + return; + } RegistryRoot.SetValue(_GraphicsQuality, Quality.Custom, RegistryValueKind.DWord); - string data = this.Serialize(StarRailSettingsJSONContext.Default); + string data = this.Serialize(StarRailSettingsJSONContext.Default.Model); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); #if DEBUG diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs index 33e9db67f..c763043e9 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs @@ -8,8 +8,6 @@ using System; using System.Drawing; using System.Text; -using System.Text.Encodings.Web; -using System.Text.Json; using System.Text.Json.Serialization; using static CollapseLauncher.GameSettings.Base.SettingsBase; using static Hi3Helper.Logger; @@ -117,13 +115,7 @@ public static PCResolution Load() #if DEBUG LogWriteLine($"Loaded StarRail Settings: {_ValueName}\r\n{Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); #endif - JsonSerializerOptions options = new JsonSerializerOptions() - { - TypeInfoResolver = StarRailSettingsJSONContext.Default, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - - return byteStr.Deserialize(StarRailSettingsJSONContext.Default) ?? new PCResolution(); + return byteStr.Deserialize(StarRailSettingsJSONContext.Default.PCResolution) ?? new PCResolution(); } } catch (Exception ex) @@ -147,7 +139,7 @@ public override void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(StarRailSettingsJSONContext.Default); + string data = this.Serialize(StarRailSettingsJSONContext.Default.PCResolution); byte[] dataByte = Encoding.UTF8.GetBytes(data); RegistryRoot.SetValue(_ValueName, dataByte, RegistryValueKind.Binary); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/SFXVolume.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/SFXVolume.cs index b2a2d68ce..4f23c912b 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/SFXVolume.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/SFXVolume.cs @@ -61,7 +61,7 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - RegistryRoot?.SetValue(_ValueName, SFXVol, RegistryValueKind.DWord); + RegistryRoot.SetValue(_ValueName, SFXVol, RegistryValueKind.DWord); #if DEBUG LogWriteLine($"Saved StarRail Settings: {_ValueName} : {SFXVol}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/VOVolume.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/VOVolume.cs index 2f766d761..97515807c 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/VOVolume.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/VOVolume.cs @@ -61,7 +61,7 @@ public void Save() try { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - RegistryRoot?.SetValue(_ValueName, VOVol, RegistryValueKind.DWord); + RegistryRoot.SetValue(_ValueName, VOVol, RegistryValueKind.DWord); #if DEBUG LogWriteLine($"Saved StarRail Settings: {_ValueName} : {VOVol}", LogType.Debug, true); #endif diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/Settings.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/Settings.cs index 98f3838f6..1f5b42580 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/Settings.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/Settings.cs @@ -41,18 +41,19 @@ public override void ReloadSettings() InitializeSettings(); } + #nullable enable public override void SaveSettings() { // Save Settings base.SaveSettings(); - GraphicsSettings.Save(); - SettingsScreen.Save(); - AudioSettings_BGM.Save(); - AudioSettings_Master.Save(); - AudioSettings_SFX.Save(); - AudioSettings_VO.Save(); - AudioLanguage.Save(); - TextLanguage.Save(); + GraphicsSettings?.Save(); + SettingsScreen?.Save(); + AudioSettings_BGM?.Save(); + AudioSettings_Master?.Save(); + AudioSettings_SFX?.Save(); + AudioSettings_VO?.Save(); + AudioLanguage?.Save(); + TextLanguage?.Save(); } public override IGameSettingsUniversal AsIGameSettingsUniversal() => this; diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs index 1a82be458..24264ebb6 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs @@ -17,6 +17,8 @@ internal class CollapseMiscSetting : IGameSettingsValue private const string _ValueName = "CollapseLauncher_Misc"; private bool _UseCustomArguments = true; + + private bool _UseCustomRegionBG = false; private static bool _IsDeserializing; #endregion @@ -88,6 +90,32 @@ public bool UseCustomArguments /// Use mobile layout. Currently only available for Genshin and StarRail. ///
public bool LaunchMobileMode { get; set; } = false; + + /// + /// Set custom background for given region for given game. + /// + public bool UseCustomRegionBG + { + get => _UseCustomRegionBG; + set + { + _UseCustomRegionBG = value; + if (!_IsDeserializing) Save(); + } + } + +#nullable enable + /// + /// The path of the custom BG for each region + /// + public string? CustomRegionBGPath { get; set; } +#nullable restore + + /// + /// Determines if the game playtime should be synced to the database + /// + public bool IsSyncPlaytimeToDatabase { get; set; } = true; + #endregion #region Methods @@ -107,7 +135,7 @@ public static CollapseMiscSetting Load() #if DEBUG LogWriteLine($"Loaded Collapse Misc Settings:\r\n{Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); #endif - return byteStr.Deserialize(UniversalSettingsJSONContext.Default) ?? new CollapseMiscSetting(); + return byteStr.Deserialize(UniversalSettingsJSONContext.Default.CollapseMiscSetting) ?? new CollapseMiscSetting(); } } catch ( Exception ex ) @@ -128,7 +156,7 @@ public void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(UniversalSettingsJSONContext.Default, true); + string data = this.Serialize(UniversalSettingsJSONContext.Default.CollapseMiscSetting, true); byte[] dataByte = Encoding.UTF8.GetBytes(data); #if DEBUG LogWriteLine($"Saved Collapse Misc Settings:\r\n{data}", LogType.Debug, true); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseScreenSetting.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseScreenSetting.cs index 581bc5335..440d7b646 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseScreenSetting.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseScreenSetting.cs @@ -70,7 +70,7 @@ public static CollapseScreenSetting Load() #if DEBUG LogWriteLine($"Loaded Collapse Screen Settings:\r\n{Encoding.UTF8.GetString(byteStr.TrimEnd((byte)0))}", LogType.Debug, true); #endif - return byteStr.Deserialize(UniversalSettingsJSONContext.Default) ?? new CollapseScreenSetting(); + return byteStr.Deserialize(UniversalSettingsJSONContext.Default.CollapseScreenSetting) ?? new CollapseScreenSetting(); } } catch (Exception ex) @@ -87,7 +87,7 @@ public void Save() { if (RegistryRoot == null) throw new NullReferenceException($"Cannot save {_ValueName} since RegistryKey is unexpectedly not initialized!"); - string data = this.Serialize(UniversalSettingsJSONContext.Default, true); + string data = this.Serialize(UniversalSettingsJSONContext.Default.CollapseScreenSetting, true); byte[] dataByte = Encoding.UTF8.GetBytes(data); #if DEBUG LogWriteLine($"Saved Collapse Screen Settings:\r\n{data}", LogType.Debug, true); @@ -101,7 +101,6 @@ public void Save() } public bool Equals(CollapseScreenSetting? comparedTo) => TypeExtensions.IsInstancePropertyEqual(this, comparedTo); -#nullable disable #endregion } } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CustomArgs.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CustomArgs.cs index 1381ac911..615628402 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CustomArgs.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CustomArgs.cs @@ -71,7 +71,6 @@ public bool Equals(CustomArgs? comparedTo) return comparedTo.CustomArgumentValue == this.CustomArgumentValue; } -#nullable disable #endregion } } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Enums.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Enums.cs index ec54e0aec..64ff47f07 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Enums.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Enums.cs @@ -123,7 +123,7 @@ public enum AudioPlaybackDevice { Headphones, Speakers, - TV + TV = 3 } public static class ServerName diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs index 5963c7b43..373fedf54 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs @@ -5,12 +5,54 @@ using System; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -// ReSharper disable ReturnTypeCanBeNotNullable +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; #nullable enable namespace CollapseLauncher.GameSettings.Zenless; -internal class GeneralData : MagicNodeBaseValues + +internal class GeneralData : MagicNodeBaseValues, IDisposable { + #region Disposer + + ~GeneralData() + { + _systemSettingDataMap = null; + _keyboardBindingMap = null; + _mouseBindingMap = null; + _gamepadBindingMap = null; + _graphicsPresData = null; + _resolutionIndexData = null; + _vSyncData = null; + _renderResolutionData = null; + _shadowQualityData = null; + _antiAliasingData = null; + _volFogQualityData = null; + _bloomData = null; + _reflQualityData = null; + _fxQualityData = null; + _colorFilterData = null; + _charQualityData = null; + _distortionData = null; + _shadingQualityData = null; + _envQualityData = null; + _envGlobalIllumination = null; + _vMotionBlur = null; + _fpsData = null; + _hpcaData = null; + _mainVolData = null; + _musicVolData = null; + _dialogVolData = null; + _sfxVolData = null; + _playDevData = null; + _muteAudOnMinimizeData = null; + + GC.Collect(); + } + + public void Dispose() => GC.SuppressFinalize(this); + + #endregion #region Node Based Properties private JsonNode? _systemSettingDataMap; private JsonNode? _keyboardBindingMap; @@ -19,7 +61,7 @@ internal class GeneralData : MagicNodeBaseValues [JsonPropertyName("SystemSettingDataMap")] [JsonIgnore] // We ignore this one from getting serialized to default JSON value - public JsonNode? SystemSettingDataMap + public JsonNode SystemSettingDataMap { // Cache the SystemSettingDataMap inside the parent SettingsJsonNode // and ensure that the node for SystemSettingDataMap exists. If not exist, @@ -29,7 +71,7 @@ public JsonNode? SystemSettingDataMap [JsonPropertyName("KeyboardBindingMap")] [JsonIgnore] // We ignore this one from getting serialized to default JSON value - public JsonNode? KeyboardBindingMap + public JsonNode KeyboardBindingMap { // Cache the KeyboardBindingMap inside the parent SettingsJsonNode // and ensure that the node for KeyboardBindingMap exists. If not exist, @@ -39,7 +81,7 @@ public JsonNode? KeyboardBindingMap [JsonPropertyName("MouseBindingMap")] [JsonIgnore] // We ignore this one from getting serialized to default JSON value - public JsonNode? MouseBindingMap + public JsonNode MouseBindingMap { // Cache the MouseBindingMap inside the parent SettingsJsonNode // and ensure that the node for MouseBindingMap exists. If not exist, @@ -49,7 +91,7 @@ public JsonNode? MouseBindingMap [JsonPropertyName("GamepadBindingMap")] [JsonIgnore] // We ignore this one from getting serialized to default JSON value - public JsonNode? GamepadBindingMap + public JsonNode GamepadBindingMap { // Cache the GamepadBindingMap inside the parent SettingsJsonNode // and ensure that the node for GamepadBindingMap exists. If not exist, @@ -98,14 +140,35 @@ public int SelectedServerIndex public LanguageText DeviceLanguageType { get => SettingsJsonNode.GetNodeValueEnum("DeviceLanguageType", LanguageText.Unset); - set => SettingsJsonNode.SetNodeValueEnum("DeviceLanguageType", value, JsonEnumStoreType.AsNumber); + set => SettingsJsonNode.SetNodeValueEnum("DeviceLanguageType", value); } [JsonPropertyName("DeviceLanguageVoiceType")] public LanguageVoice DeviceLanguageVoiceType { get => SettingsJsonNode.GetNodeValueEnum("DeviceLanguageVoiceType", LanguageVoice.Unset); - set => SettingsJsonNode.SetNodeValueEnum("DeviceLanguageVoiceType", value, JsonEnumStoreType.AsNumber); + set => SettingsJsonNode.SetNodeValueEnum("DeviceLanguageVoiceType", value); + } + + [JsonPropertyName("PlayerPrefs_StringContainer")] + public string? PlayerPrefsStringContainer + { + get => SettingsJsonNode.GetNodeValue("PlayerPrefs_StringContainer", ""); + set => SettingsJsonNode.SetNodeValue("DeviceLanguageVoiceType", value); + } + + [JsonPropertyName("PlayerPrefs_IntContainer")] + public string? PlayerPrefsIntContainer + { + get => SettingsJsonNode.GetNodeValue("PlayerPrefs_IntContainer", ""); + set => SettingsJsonNode.SetNodeValue("PlayerPrefs_IntContainer", value); + } + + [JsonPropertyName("PlayerPrefs_FloatContainer")] + public string? PlayerPrefsFloatContainer + { + get => SettingsJsonNode.GetNodeValue("PlayerPrefs_FloatContainer", ""); + set => SettingsJsonNode.SetNodeValue("PlayerPrefs_FloatContainer", value); } [JsonPropertyName("LocalUILayoutPlatform ")] @@ -185,12 +248,9 @@ public bool DisableBattleUIOptimization [JsonIgnore] public GraphicsPresetOption GraphicsPreset { - get => (_graphicsPresData.HasValue - ? _graphicsPresData - : _graphicsPresData = - SystemSettingDataMap! - .AsSystemSettingLocalData("3", GraphicsPresetOption.Medium)) - .Value.GetDataEnum(); + get => (_graphicsPresData ??= SystemSettingDataMap + .AsSystemSettingLocalData("3", GraphicsPresetOption.Medium)) + .GetDataEnum(); set => _graphicsPresData?.SetDataEnum(value); } @@ -203,12 +263,8 @@ public GraphicsPresetOption GraphicsPreset [JsonIgnore] public int ResolutionIndex { - get => (_resolutionIndexData.HasValue - ? _resolutionIndexData - : _resolutionIndexData = - SystemSettingDataMap! - .AsSystemSettingLocalData("5", -1)) - .Value.GetData(); + get => (_resolutionIndexData ??= SystemSettingDataMap + .AsSystemSettingLocalData("5", -1)).GetData(); set => _resolutionIndexData?.SetData(value); } @@ -223,10 +279,8 @@ public bool VSync { // Initialize the field under _vSyncData as SystemSettingLocalData get => - (_vSyncData.HasValue - ? _vSyncData - : _vSyncData = SystemSettingDataMap! - .AsSystemSettingLocalData("8", 1)).Value.GetData() == 1; + (_vSyncData ??= SystemSettingDataMap + .AsSystemSettingLocalData("8", 1)).GetData() == 1; set => _vSyncData?.SetData(value ? 1 : 0); } @@ -243,12 +297,9 @@ public RenderResOption RenderResolution { // Initialize the field under _renderResolutionData as SystemSettingLocalData get => - (_renderResolutionData.HasValue - ? _renderResolutionData - : _renderResolutionData = SystemSettingDataMap! - .AsSystemSettingLocalData("9", - RenderResOption.f10)).Value - .GetDataEnum(); + (_renderResolutionData ??= SystemSettingDataMap + .AsSystemSettingLocalData("9", + RenderResOption.f10)).GetDataEnum(); set => _renderResolutionData?.SetDataEnum(value); } @@ -265,11 +316,8 @@ public QualityOption3 ShadowQuality { // Initialize the field under _shadowQualityData as SystemSettingLocalData get => - (_shadowQualityData.HasValue - ? _shadowQualityData - : _shadowQualityData = SystemSettingDataMap!.AsSystemSettingLocalData("10", - QualityOption3.Medium)).Value - .GetDataEnum(); + (_shadowQualityData ??= SystemSettingDataMap.AsSystemSettingLocalData("10", + QualityOption3.Medium)).GetDataEnum(); set => _shadowQualityData?.SetDataEnum(value); } @@ -282,12 +330,9 @@ public AntiAliasingOption AntiAliasing { // Initialize the field under _antiAliasingData as SystemSettingLocalData get => - (_antiAliasingData.HasValue - ? _antiAliasingData - : _antiAliasingData = SystemSettingDataMap! - .AsSystemSettingLocalData("12", - AntiAliasingOption.TAA)).Value - .GetDataEnum(); + (_antiAliasingData ??= SystemSettingDataMap + .AsSystemSettingLocalData("12", + AntiAliasingOption.TAA)).GetDataEnum(); set => _antiAliasingData?.SetDataEnum(value); } @@ -302,11 +347,8 @@ public AntiAliasingOption AntiAliasing [JsonIgnore] public QualityOption4 VolumetricFogQuality { - get => (_volFogQualityData.HasValue - ? _volFogQualityData - : _volFogQualityData = SystemSettingDataMap! - .AsSystemSettingLocalData("13", QualityOption4.Medium)).Value - .GetDataEnum(); + get => (_volFogQualityData ??= SystemSettingDataMap + .AsSystemSettingLocalData("13", QualityOption4.Medium)).GetDataEnum(); set => _volFogQualityData?.SetDataEnum(value); } @@ -316,10 +358,7 @@ public QualityOption4 VolumetricFogQuality [JsonIgnore] public bool Bloom { - get => (_bloomData.HasValue - ? _bloomData - : _bloomData = SystemSettingDataMap!.AsSystemSettingLocalData("14", 1)).Value - .GetData() == 1; + get => (_bloomData ??= SystemSettingDataMap.AsSystemSettingLocalData("14", 1)).GetData() == 1; set => _bloomData?.SetData(value ? 1 : 0); } @@ -334,11 +373,8 @@ public bool Bloom public QualityOption4 ReflectionQuality { get => - (_reflQualityData.HasValue - ? _reflQualityData - : _reflQualityData = SystemSettingDataMap!.AsSystemSettingLocalData("15", - QualityOption4.Medium)).Value - .GetDataEnum(); + (_reflQualityData ??= SystemSettingDataMap.AsSystemSettingLocalData("15", + QualityOption4.Medium)).GetDataEnum(); set => _reflQualityData?.SetDataEnum(value); } @@ -354,11 +390,9 @@ public QualityOption4 ReflectionQuality public QualityOption3 FxQuality { get => - (_fxQualityData.HasValue - ? _fxQualityData - : _fxQualityData = SystemSettingDataMap!.AsSystemSettingLocalData("16", - QualityOption3.Medium)).Value - .GetDataEnum(); + (_fxQualityData ??= SystemSettingDataMap.AsSystemSettingLocalData("16", + QualityOption3.Medium)) + .GetDataEnum(); set => _fxQualityData?.SetDataEnum(value); } @@ -368,10 +402,7 @@ public QualityOption3 FxQuality public int ColorFilter { - get => (_colorFilterData.HasValue - ? _colorFilterData - : _colorFilterData = SystemSettingDataMap!.AsSystemSettingLocalData("95", 10)) - .Value.GetData(); + get => (_colorFilterData ??= SystemSettingDataMap.AsSystemSettingLocalData("95", 10)).GetData(); set => _colorFilterData?.SetData(value); } @@ -386,11 +417,8 @@ public int ColorFilter public QualityOption2 CharacterQuality { get => - (_charQualityData.HasValue - ? _charQualityData - : _charQualityData = SystemSettingDataMap!.AsSystemSettingLocalData("99", - QualityOption2.High)).Value - .GetDataEnum(); + (_charQualityData ??= SystemSettingDataMap.AsSystemSettingLocalData("99", + QualityOption2.High)).GetDataEnum(); set => _charQualityData?.SetDataEnum(value); } @@ -401,10 +429,8 @@ public QualityOption2 CharacterQuality [JsonIgnore] public bool Distortion { - get => (_distortionData.HasValue - ? _distortionData - : _distortionData = SystemSettingDataMap!.AsSystemSettingLocalData("107", 1)) - .Value.GetData() == 1; + get => (_distortionData ??= SystemSettingDataMap.AsSystemSettingLocalData("107", 1)) + .GetData() == 1; set => _distortionData?.SetData(value ? 1 : 0); } @@ -419,11 +445,8 @@ public bool Distortion public QualityOption3 ShadingQuality { get => - (_shadingQualityData.HasValue - ? _shadingQualityData - : _shadingQualityData = SystemSettingDataMap!.AsSystemSettingLocalData("108", - QualityOption3.Medium)).Value - .GetDataEnum(); + (_shadingQualityData ??= SystemSettingDataMap.AsSystemSettingLocalData("108", + QualityOption3.Medium)).GetDataEnum(); set => _shadingQualityData?.SetDataEnum(value); } @@ -439,15 +462,46 @@ public QualityOption3 ShadingQuality public QualityOption2 EnvironmentQuality { get => - (_envQualityData.HasValue - ? _envQualityData - : _envQualityData = SystemSettingDataMap!.AsSystemSettingLocalData("109", - QualityOption2.High)).Value - .GetDataEnum(); + (_envQualityData ??= SystemSettingDataMap.AsSystemSettingLocalData("109", + QualityOption2.High)) + .GetDataEnum(); set => _envQualityData?.SetDataEnum(value); } - + + // Key 12155 Global Illumination + private SystemSettingLocalData? _envGlobalIllumination; + + /// + /// Sets the in-game global illumination settings for Environment + /// + /// + [JsonIgnore] + public QualityOption3 GlobalIllumination + { + get => + (_envGlobalIllumination ??= SystemSettingDataMap.AsSystemSettingLocalData("12155", + QualityOption3.High)).GetDataEnum(); + set => + _envGlobalIllumination?.SetDataEnum(value); + } + + // Key 8 VSync + private SystemSettingLocalData? _vMotionBlur; + + /// + /// Set Motion Blur mode + /// + [JsonIgnore] + public bool MotionBlur + { + get => + (_vMotionBlur ??= SystemSettingDataMap + .AsSystemSettingLocalData("106", 1)).GetData() == 1; + set => + _vMotionBlur?.SetData(value ? 1 : 0); + } + // Key 110 FPS private SystemSettingLocalData? _fpsData; @@ -459,10 +513,24 @@ public QualityOption2 EnvironmentQuality public FpsOption Fps { // Initialize the field under _fpsData as SystemSettingLocalData - get => (_fpsData.HasValue ? _fpsData : _fpsData = SystemSettingDataMap! - .AsSystemSettingLocalData("110", FpsOption.Hi60)).Value.GetDataEnum(); + get => (_fpsData ??= SystemSettingDataMap + .AsSystemSettingLocalData("110", FpsOption.Hi60)).GetDataEnum(); set => _fpsData?.SetDataEnum(value); } + + // Key 13162 High-Precision Character Animation + private SystemSettingLocalData? _hpcaData; + + /// + /// Sets in-game settings for High-Precision Character Animation.
+ /// Whatever that is ¯\_(ツ)_/¯ + ///
+ [JsonIgnore] + public bool HiPrecisionCharaAnim + { + get => (_hpcaData ??= SystemSettingDataMap.AsSystemSettingLocalData("13162", true)).GetData(); + set => _hpcaData?.SetData(value); + } #endregion @@ -473,9 +541,7 @@ public FpsOption Fps [JsonIgnore] public int Audio_MainVolume { - get => (_mainVolData.HasValue - ? _mainVolData - : _mainVolData = SystemSettingDataMap!.AsSystemSettingLocalData("31", 10)).Value.GetData(); + get => (_mainVolData ??= SystemSettingDataMap.AsSystemSettingLocalData("31", 10)).GetData(); set => _mainVolData?.SetData(value); } @@ -485,9 +551,7 @@ public int Audio_MainVolume [JsonIgnore] public int Audio_MusicVolume { - get => (_musicVolData.HasValue - ? _musicVolData - : _musicVolData = SystemSettingDataMap!.AsSystemSettingLocalData("32", 10)).Value.GetData(); + get => (_musicVolData ??= SystemSettingDataMap.AsSystemSettingLocalData("32", 10)).GetData(); set => _musicVolData?.SetData(value); } @@ -497,9 +561,7 @@ public int Audio_MusicVolume [JsonIgnore] public int Audio_DialogVolume { - get => (_dialogVolData.HasValue - ? _dialogVolData - : _dialogVolData = SystemSettingDataMap!.AsSystemSettingLocalData("33", 10)).Value.GetData(); + get => (_dialogVolData ??= SystemSettingDataMap.AsSystemSettingLocalData("33", 10)).GetData(); set => _dialogVolData?.SetData(value); } @@ -509,9 +571,7 @@ public int Audio_DialogVolume [JsonIgnore] public int Audio_SfxVolume { - get => (_sfxVolData.HasValue - ? _sfxVolData - : _sfxVolData = SystemSettingDataMap!.AsSystemSettingLocalData("34", 10)).Value.GetData(); + get => (_sfxVolData ??= SystemSettingDataMap.AsSystemSettingLocalData("34", 10)).GetData(); set => _sfxVolData?.SetData(value); } @@ -521,11 +581,8 @@ public int Audio_SfxVolume [JsonIgnore] public AudioPlaybackDevice Audio_PlaybackDevice { - get => (_playDevData.HasValue - ? _playDevData - : _playDevData = - SystemSettingDataMap!.AsSystemSettingLocalData("10104", - AudioPlaybackDevice.Headphones)).Value + get => (_playDevData ??= SystemSettingDataMap.AsSystemSettingLocalData("10104", + AudioPlaybackDevice.Headphones)) .GetDataEnum(); set => _playDevData?.SetDataEnum(value); } @@ -536,10 +593,8 @@ public AudioPlaybackDevice Audio_PlaybackDevice [JsonIgnore] public bool Audio_MuteOnMinimize { - get => (_muteAudOnMinimizeData.HasValue - ? _muteAudOnMinimizeData - : _muteAudOnMinimizeData = SystemSettingDataMap!.AsSystemSettingLocalData("10113", 1)).Value.GetData() == 1; - set => _muteAudOnMinimizeData?.SetData((value ? 1 : 0)); + get => (_muteAudOnMinimizeData ??= SystemSettingDataMap.AsSystemSettingLocalData("10113", 1)).GetData() == 1; + set => _muteAudOnMinimizeData?.SetData(value ? 1 : 0); } #endregion @@ -548,10 +603,10 @@ public bool Audio_MuteOnMinimize [Obsolete("Loading settings with Load() is not supported for IGameSettingsValueMagic member. Use LoadWithMagic() instead!", true)] public new static GeneralData Load() => throw new NotSupportedException("Loading settings with Load() is not supported for IGameSettingsValueMagic member. Use LoadWithMagic() instead!"); - public new static GeneralData LoadWithMagic(byte[] magic, SettingsGameVersionManager versionManager, - JsonSerializerContext context) + public new static GeneralData LoadWithMagic(byte[] magic, SettingsGameVersionManager versionManager, + JsonTypeInfo typeInfo) { - var returnVal = MagicNodeBaseValues.LoadWithMagic(magic, versionManager, context); + var returnVal = MagicNodeBaseValues.LoadWithMagic(magic, versionManager, typeInfo); #if DEBUG const bool isPrintDebug = true; diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs index 8326e9aea..55afd208a 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs @@ -9,7 +9,7 @@ #nullable enable namespace CollapseLauncher.GameSettings.Zenless.JsonProperties; -public struct SystemSettingLocalData +public readonly struct SystemSettingLocalData where TData : struct { private const string TypeKey = "MoleMole.SystemSettingLocalData"; @@ -44,7 +44,7 @@ public SystemSettingLocalData([NotNull] JsonNode node, TData defaultData = defau public static class SystemSettingLocalDataExt { public static SystemSettingLocalData AsSystemSettingLocalData( - [NotNull] this JsonNode node, string keyName, TData defaultData = default, int defaultVersion = 1) + [NotNull] this JsonNode? node, string keyName, TData defaultData = default, int defaultVersion = 1) where TData : struct { ArgumentNullException.ThrowIfNull(node, nameof(node)); diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs index 5fdd3427e..35f69aeb0 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs @@ -53,11 +53,12 @@ public sealed override void InitializeSettings() GeneralData = GeneralData.LoadWithMagic( MagicReDo, SettingsGameVersionManager.Create(_gameVersionManager, ZZZSettingsConfigFile, "GENERAL_DATA.bin"), - ZenlessSettingsJSONContext.Default); + ZenlessSettingsJSONContext.Default.GeneralData); } public override void ReloadSettings() { + GeneralData.Dispose(); InitializeSettings(); } diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs index a4d0ea4cd..417adbd84 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs @@ -166,6 +166,13 @@ private static unsafe int InternalDecode(ReadOnlySpan magic, bool* evil, B internal static void WriteString(string filePath, ReadOnlySpan content, ReadOnlySpan magic) { + // Ensure the folder always exist + string? fileDir = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(fileDir) && !Directory.Exists(fileDir)) + { + Directory.CreateDirectory(fileDir); + } + // Get the FileInfo FileInfo fileInfo = new FileInfo(filePath); diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/BaseClass/GameVersionBase.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/BaseClass/GameVersionBase.cs index cf8f73608..a4af4465f 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/BaseClass/GameVersionBase.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/BaseClass/GameVersionBase.cs @@ -17,6 +17,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; // ReSharper disable CheckNamespace @@ -34,6 +35,7 @@ internal class GameVersionBase : IGameVersionCheck private int gameChannelID => GamePreset.ChannelID ?? 0; private int gameSubChannelID => GamePreset.SubChannelID ?? 0; + private string gameCps => GamePreset.LauncherCPSType; private IniSection _defaultIniProfile => new() @@ -81,7 +83,7 @@ private IniValue GenerateUAPCValue() } } }; - string uapcValue = uapc.Serialize(InternalAppJSONContext.Default, false); + string uapcValue = uapc.Serialize(InternalAppJSONContext.Default.DictionaryStringDictionaryStringString, false); return new IniValue(uapcValue); } @@ -395,31 +397,33 @@ public virtual List GetGameLatestZip(GameInstallStateEnum return returnList; } - public virtual List GetGamePreloadZip() +#nullable enable + public virtual List? GetGamePreloadZip() { // Initialize the return list List returnList = new(); // If the preload is not exist, then return null - if (GameAPIProp.data?.pre_download_game == null) + if (GameAPIProp.data?.pre_download_game == null + || ((GameAPIProp.data?.pre_download_game?.diffs?.Count ?? 0) == 0 + && GameAPIProp.data?.pre_download_game?.latest == null)) { return null; } // Try get the diff file by the first or default (null) - RegionResourceVersion diff = GameAPIProp.data.pre_download_game?.diffs? + RegionResourceVersion? diff = GameAPIProp.data?.pre_download_game?.diffs? .FirstOrDefault(x => x.version == GameVersionInstalled?.VersionString); // If the single entry of the diff is null, then return null // If the diff is null, then get the latest. // If diff is found, then add the diff one. - returnList.Add(diff ?? GameAPIProp.data.pre_download_game?.latest); + returnList.Add(diff ?? GameAPIProp.data?.pre_download_game?.latest ?? throw new NullReferenceException($"Preload neither have diff or latest package!")); // Return the list return returnList; } -#nullable enable public virtual List? GetGamePluginZip() { // Check if the plugin is not empty, then add it @@ -576,7 +580,7 @@ public virtual async ValueTask CheckSdkUpdate(string validatePath) if (string.IsNullOrEmpty(line)) continue; - PkgVersionProperties pkgVersion = line.Deserialize(CoreLibraryJSONContext.Default); + PkgVersionProperties pkgVersion = line.Deserialize(CoreLibraryJSONContext.Default.PkgVersionProperties); if (pkgVersion != null) { @@ -723,11 +727,11 @@ public virtual async ValueTask EnsureGameConfigIniCorrectiveness(UIElement .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle1, true) .AddTextBlockLine(gameNameTranslated, FontWeights.Bold).AddTextBlockNewLine(2) .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle2).AddTextBlockNewLine(2) - .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle3, null, 10) - .AddTextBlockLine(Locale.Lang._Misc.YesContinue, FontWeights.SemiBold, 10) - .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle4, null, 10) - .AddTextBlockLine(Locale.Lang._Misc.NoCancel, FontWeights.SemiBold, 10) - .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle5, null, 10); + .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle3) + .AddTextBlockLine(Locale.Lang._Misc.YesContinue, FontWeights.SemiBold) + .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle4) + .AddTextBlockLine(Locale.Lang._Misc.NoCancel, FontWeights.SemiBold) + .AddTextBlockLine(Locale.Lang._HomePage.GameStateInvalid_Subtitle5); ContentDialogResult dialogResult = await SimpleDialogs.SpawnDialog( Locale.Lang._HomePage.GameStateInvalid_Title, @@ -815,9 +819,12 @@ protected virtual bool IsGameConfigIdValid() || GamePreset.SubChannelID == null) return true; - if (!GameIniVersion[_defaultIniVersionSection].ContainsKey(ChannelIdKeyName) - || !GameIniVersion[_defaultIniVersionSection].ContainsKey(SubChannelIdKeyName) - || !GameIniVersion[_defaultIniVersionSection].ContainsKey(CpsKeyName)) + bool isContainsChannelId = GameIniVersion[_defaultIniVersionSection].ContainsKey(ChannelIdKeyName); + bool isContainsSubChannelId = GameIniVersion[_defaultIniVersionSection].ContainsKey(SubChannelIdKeyName); + bool isCps = GameIniVersion[_defaultIniVersionSection].ContainsKey(CpsKeyName); + if (!isContainsChannelId + || !isContainsSubChannelId + || !isCps) return false; string? channelIdCurrent = GameIniVersion[_defaultIniVersionSection][ChannelIdKeyName].ToString(); @@ -840,9 +847,9 @@ protected virtual bool IsGameExecDataDirValid(string? executableName) } protected virtual bool IsGameHasBilibiliStatus(string? executableName) - { + { bool isBilibili = GamePreset.LauncherCPSType? - .Equals("bilibili", StringComparison.OrdinalIgnoreCase) ?? false; + .IndexOf("bilibili", StringComparison.OrdinalIgnoreCase) >= 0; if (isBilibili) return true; @@ -862,12 +869,9 @@ protected virtual void FixInvalidGameVendor(string? executableName) if (!string.IsNullOrEmpty(appInfoFileDir) && !Directory.Exists(appInfoFileDir)) Directory.CreateDirectory(appInfoFileDir); - string[] strings = [ - GamePreset.VendorType.ToString(), - GamePreset.InternalGameNameInConfig! - ]; - - File.WriteAllLines(appInfoFilePath, strings, System.Text.Encoding.UTF8); + string appInfoString = $"{GamePreset.VendorType.ToString()}\n{GamePreset.InternalGameNameInConfig!}"; + byte[] buffer = Encoding.UTF8.GetBytes(appInfoString); + File.WriteAllBytes(appInfoFilePath, buffer); } protected virtual void FixInvalidGameExecDataDir(string? executableName) @@ -893,7 +897,7 @@ protected virtual void FixInvalidGameConfigId() protected virtual void FixInvalidGameBilibiliStatus(string? executableName) { bool isBilibili = GamePreset.LauncherCPSType? - .Equals("bilibili", StringComparison.OrdinalIgnoreCase) ?? false; + .IndexOf("bilibili", StringComparison.OrdinalIgnoreCase) >= 0; executableName = Path.GetFileNameWithoutExtension(executableName); string sdkDllPath = Path.Combine(GameDirPath, $"{executableName}_Data", "Plugins", "PCGameSDK.dll"); @@ -991,22 +995,24 @@ public void UpdateGameVersion(GameVersion? version, bool saveValue = true) public void UpdateGameChannels(bool saveValue = true) { - bool isBilibili = GamePreset.ZoneName == "Bilibili"; GameIniVersion[_defaultIniVersionSection]["channel"] = gameChannelID; GameIniVersion[_defaultIniVersionSection]["sub_channel"] = gameSubChannelID; + GameIniVersion[_defaultIniVersionSection]["cps"] = gameCps; - if (isBilibili) - { - GameIniVersion[_defaultIniVersionSection]["cps"] = "bilibili"; - } + /* Disable these lines as these will trigger some bugs (like Endless "Broken config.ini" dialog) + * and causes the cps field to be missing for other non-Bilibili games + * // Remove the contains section if the client is not Bilibili and it does have the value. // This to avoid an issue with HSR config.ini detection - else if (GameIniVersion.ContainsSection(_defaultIniVersionSection) - && GameIniVersion[_defaultIniVersionSection].ContainsKey("cps") - && GameIniVersion[_defaultIniVersionSection]["cps"].ToString() == "bilibili") + bool isBilibili = GamePreset.ZoneName == "Bilibili"; + if ( !isBilibili + && GameIniVersion.ContainsSection(_defaultIniVersionSection) + && GameIniVersion[_defaultIniVersionSection].ContainsKey("cps") + && GameIniVersion[_defaultIniVersionSection]["cps"].ToString().IndexOf("bilibili", StringComparison.OrdinalIgnoreCase) >= 0) { GameIniVersion[_defaultIniVersionSection].Remove("cps"); } + */ if (saveValue) { diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Genshin/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Genshin/VersionCheck.cs index 5073b7cf1..51e94930a 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Genshin/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Genshin/VersionCheck.cs @@ -38,7 +38,7 @@ public GameTypeGenshinVersion(UIElement parentUIElement, RegionResourceProp game return base.TryFindGamePathFromExecutableAndConfig(path, executableName); } - return null; + return basePath; } protected override bool IsExecutableFileExist(string? executableName) diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs index f1ce6bb50..fbbb8aa0e 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs @@ -1,17 +1,124 @@ +using CollapseLauncher.Helper.Metadata; +using Hi3Helper; using Microsoft.UI.Xaml; +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Buffers.Text; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; namespace CollapseLauncher.GameVersioning { internal sealed class GameTypeZenlessVersion : GameVersionBase { #region Properties + internal RSA SleepyInstance { get; set; } + internal string SleepyIdentity { get; set; } + internal string SleepyArea { get; set; } #endregion - public GameTypeZenlessVersion(UIElement parentUIElement, RegionResourceProp gameRegionProp, string gameName, string gameRegion) + #region Initialize Sleepy + private void InitializeSleepy(PresetConfig gamePreset) + { + // Go YEET + SleepyInstance = RSA.Create(); + goto StartCheck; + + // Go to the DOOM + QuitFail: + DisableRepairAndCacheInstance(gamePreset); + return; + + StartCheck: + // Check if the thing does not have thing, then DOOMED + if (gamePreset.DispatcherKey == null) + goto QuitFail; + + // We cannot pay the house so rent. + byte[] keyUtf8Base64 = ArrayPool.Shared.Rent(gamePreset.DispatcherKey.Length * 2); + + try + { + // Check if the data is an impostor, then eject (basically DOOMED) + if (!Encoding.UTF8.TryGetBytes(gamePreset.DispatcherKey, keyUtf8Base64, out int keyWrittenLen)) + goto QuitFail; + + // Also if the data is not a crew, then YEET. + OperationStatus base64DecodeStatus = Base64.DecodeFromUtf8InPlace(keyUtf8Base64.AsSpan(0, keyWrittenLen), out int keyFromBase64Len); + if (OperationStatus.Done != base64DecodeStatus) + { + Logger.LogWriteLine($"OOF, we cannot go to sleep as the bed is collapsing! :( Operation Status: {base64DecodeStatus}", LogType.Error, true); + goto QuitFail; + } + + // Try serve a dinner and if it fails, then GET OUT! + if (!DataCooker.IsServeV3Data(keyUtf8Base64)) + goto QuitFail; + + // Enjoy the meal (i guess?) + DataCooker.GetServeV3DataSize(keyUtf8Base64, out long servedCompressedSize, out long servedDecompressedSize); + Span outServeData = keyUtf8Base64.AsSpan(keyFromBase64Len, (int)servedDecompressedSize); + DataCooker.ServeV3Data(keyUtf8Base64.AsSpan(0, keyFromBase64Len), outServeData, (int)servedCompressedSize, (int)servedDecompressedSize, out int dataWritten); + + // Time for dessert!!! + ReadOnlySpan cheeseCake = outServeData.Slice(0, dataWritten); + int identityN = BinaryPrimitives.ReadInt16LittleEndian(cheeseCake.Slice(dataWritten - 4)); + int identityN2 = identityN * 2; + int areaN = BinaryPrimitives.ReadInt16LittleEndian(cheeseCake.Slice(dataWritten - 2)); + int areaN2 = areaN * 2; + + int nInBite = identityN2 + areaN2; + int wine = dataWritten - (4 + nInBite); + Span applePie = outServeData.Slice(wine, nInBite); + + // And eat good + int len = applePie.Length; + int i = 0; + NomNom: + int pos = wine % ((len - i) & unchecked((int)0xFFFFFFFF)); + applePie[i] ^= outServeData[0x10 | pos]; + if (++i < len) goto NomNom; + + // Then sleep + SleepyIdentity = MemoryMarshal.Cast(applePie.Slice(0, identityN2)).ToString(); + SleepyArea = MemoryMarshal.Cast(applePie.Slice(identityN2, areaN2)).ToString(); + + // Load the load + SleepyInstance.ImportRSAPrivateKey(outServeData.Slice(0, dataWritten), out int bytesRead); + + // If you felt food poisoned since last night's dinner, then go to the hospital + if (0 == bytesRead) + goto QuitFail; + + // Uh, what else? nothing to do? then go to sleep :amimir: + } + finally + { + // After you wake up, get out from the rent and pay for it. + ArrayPool.Shared.Return(keyUtf8Base64, true); + } + + return; + + // Close the door + void DisableRepairAndCacheInstance(PresetConfig config) + { +#if !DEBUG + config.IsRepairEnabled = false; + config.IsCacheUpdateEnabled = false; +#endif + } + } + #endregion + + public GameTypeZenlessVersion(UIElement parentUIElement, RegionResourceProp gameRegionProp, PresetConfig gamePreset, string gameName, string gameRegion) : base(parentUIElement, gameRegionProp, gameName, gameRegion) { // Try check for reinitializing game version. TryReinitializeGameVersion(); + InitializeSleepy(gamePreset); } public override bool IsGameHasDeltaPatch() => false; diff --git a/CollapseLauncher/Classes/GamePropertyVault.cs b/CollapseLauncher/Classes/GamePropertyVault.cs index 55ad2ae3a..d70587556 100644 --- a/CollapseLauncher/Classes/GamePropertyVault.cs +++ b/CollapseLauncher/Classes/GamePropertyVault.cs @@ -1,4 +1,5 @@ -using CollapseLauncher.GameSettings.Genshin; +using CollapseLauncher.GamePlaytime; +using CollapseLauncher.GameSettings.Genshin; using CollapseLauncher.GameSettings.Honkai; using CollapseLauncher.GameSettings.StarRail; using CollapseLauncher.GameSettings.Zenless; @@ -25,52 +26,59 @@ namespace CollapseLauncher.Statics { internal class GamePresetProperty : IDisposable { - internal GamePresetProperty(UIElement UIElementParent, RegionResourceProp APIResouceProp, string GameName, string GameRegion) + internal GamePresetProperty(UIElement uiElementParent, RegionResourceProp apiResourceProp, string gameName, string gameRegion) { - if (LauncherMetadataHelper.LauncherMetadataConfig != null) + if (LauncherMetadataHelper.LauncherMetadataConfig == null) { - PresetConfig GamePreset = LauncherMetadataHelper.LauncherMetadataConfig[GameName][GameRegion]; + return; + } - _APIResouceProp = APIResouceProp!.Copy(); - switch (GamePreset!.GameType) - { - case GameNameType.Honkai: - _GameVersion = new GameTypeHonkaiVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new HonkaiSettings(_GameVersion); - _GameCache = new HonkaiCache(UIElementParent, _GameVersion); - _GameRepair = new HonkaiRepair(UIElementParent, _GameVersion, _GameCache, _GameSettings); - _GameInstall = new HonkaiInstall(UIElementParent, _GameVersion, _GameCache, _GameSettings); - break; - case GameNameType.StarRail: - _GameVersion = new GameTypeStarRailVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new StarRailSettings(_GameVersion); - _GameCache = new StarRailCache(UIElementParent, _GameVersion); - _GameRepair = new StarRailRepair(UIElementParent, _GameVersion); - _GameInstall = new StarRailInstall(UIElementParent, _GameVersion); - break; - case GameNameType.Genshin: - _GameVersion = new GameTypeGenshinVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new GenshinSettings(_GameVersion); - _GameCache = null; - _GameRepair = new GenshinRepair(UIElementParent, _GameVersion, _GameVersion.GameAPIProp!.data!.game!.latest!.decompressed_path); - _GameInstall = new GenshinInstall(UIElementParent, _GameVersion); - break; - case GameNameType.Zenless: - _GameVersion = new GameTypeZenlessVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new ZenlessSettings(_GameVersion); - _GameCache = null; - _GameRepair = null; - _GameInstall = new ZenlessInstall(UIElementParent, _GameVersion); - break; - default: - throw new NotSupportedException($"[GamePresetProperty.Ctor] Game type: {GamePreset.GameType} ({GamePreset.ProfileName} - {GamePreset.ZoneName}) is not supported!"); - } + PresetConfig gamePreset = LauncherMetadataHelper.LauncherMetadataConfig[gameName][gameRegion]; + + _APIResouceProp = apiResourceProp!.Copy(); + switch (gamePreset!.GameType) + { + case GameNameType.Honkai: + _GameVersion = new GameTypeHonkaiVersion(uiElementParent, _APIResouceProp, gameName, gameRegion); + _GameSettings = new HonkaiSettings(_GameVersion); + _GameCache = new HonkaiCache(uiElementParent, _GameVersion); + _GameRepair = new HonkaiRepair(uiElementParent, _GameVersion, _GameCache, _GameSettings); + _GameInstall = new HonkaiInstall(uiElementParent, _GameVersion, _GameCache, _GameSettings); + break; + case GameNameType.StarRail: + _GameVersion = new GameTypeStarRailVersion(uiElementParent, _APIResouceProp, gameName, gameRegion); + _GameSettings = new StarRailSettings(_GameVersion); + _GameCache = new StarRailCache(uiElementParent, _GameVersion); + _GameRepair = new StarRailRepair(uiElementParent, _GameVersion); + _GameInstall = new StarRailInstall(uiElementParent, _GameVersion); + break; + case GameNameType.Genshin: + _GameVersion = new GameTypeGenshinVersion(uiElementParent, _APIResouceProp, gameName, gameRegion); + _GameSettings = new GenshinSettings(_GameVersion); + _GameCache = null; + _GameRepair = new GenshinRepair(uiElementParent, _GameVersion, _GameVersion.GameAPIProp!.data!.game!.latest!.decompressed_path); + _GameInstall = new GenshinInstall(uiElementParent, _GameVersion); + break; + case GameNameType.Zenless: + _GameVersion = new GameTypeZenlessVersion(uiElementParent, _APIResouceProp, gamePreset, gameName, gameRegion); + ZenlessSettings gameSettings = new ZenlessSettings(_GameVersion); + _GameSettings = gameSettings; + _GameCache = new ZenlessCache(uiElementParent, _GameVersion, gameSettings); + _GameRepair = new ZenlessRepair(uiElementParent, _GameVersion, gameSettings); + _GameInstall = new ZenlessInstall(uiElementParent, _GameVersion, gameSettings); + break; + case GameNameType.Unknown: + default: + throw new NotSupportedException($"[GamePresetProperty.Ctor] Game type: {gamePreset.GameType} ({gamePreset.ProfileName} - {gamePreset.ZoneName}) is not supported!"); } + + _GamePlaytime = new Playtime(_GameVersion, _GameSettings); } internal RegionResourceProp _APIResouceProp { get; set; } internal PresetConfig _GamePreset { get => _GameVersion.GamePreset; } internal IGameSettings _GameSettings { get; set; } + internal IGamePlaytime _GamePlaytime { get; set; } internal IRepair _GameRepair { get; set; } internal ICache _GameCache { get; set; } internal IGameVersionCheck _GameVersion { get; set; } @@ -87,6 +95,7 @@ internal string _GameExecutableName return _gameExecutableName; } } + internal string _GameExecutableNameWithoutExtension { get @@ -96,9 +105,10 @@ internal string _GameExecutableNameWithoutExtension return _gameExecutableNameWithoutExtension; } } + internal bool IsGameRunning { - get => InvokeProp.IsProcessExist(_GameExecutableName); + get => InvokeProp.IsProcessExist(_GameExecutableName, Path.Combine(_GameVersion?.GameDirPath ?? "", _GameExecutableName)); } #nullable enable @@ -106,10 +116,38 @@ internal bool IsGameRunning // The Process.GetProcessesByName(procName) will get an array of the process list. The output is piped into null-break operator "?" which will // returns a null if something goes wrong. If not, then pass it to .Where(x) method which will select the given value with the certain logic. // (in this case, we need to ensure that the MainWindowHandle is not a non-zero pointer) and then piped into null-break operator. - internal Process? GetGameProcessWithActiveWindow() => - Process - .GetProcessesByName(Path.GetFileNameWithoutExtension(_GamePreset!.GameExecutableName)) - .FirstOrDefault(x => x.MainWindowHandle != IntPtr.Zero); + internal Process? GetGameProcessWithActiveWindow() + { + Process[] processArr = Process.GetProcessesByName(_GameExecutableNameWithoutExtension); + int selectedIndex = -1; + try + { + for (int i = 0; i < processArr.Length; i++) + { + Process process = processArr[i]; + int processId = process.Id; + + string? processPath = InvokeProp.GetProcessPathByProcessId(processId); + string expectedProcessPath = Path.Combine(_GameVersion?.GameDirPath ?? "", _GameExecutableName); + if (string.IsNullOrEmpty(processPath) || !expectedProcessPath.Equals(processPath, StringComparison.OrdinalIgnoreCase) + || process.MainWindowHandle == IntPtr.Zero) + continue; + + selectedIndex = i; + return process; + } + } + finally + { + for (int i = 0; i < processArr.Length; i++) + { + if (i == selectedIndex) + continue; + processArr[i].Dispose(); + } + } + return null; + } #nullable disable /* @@ -131,13 +169,15 @@ public void Dispose() _GameRepair?.Dispose(); _GameCache?.Dispose(); _GameInstall?.Dispose(); + _GamePlaytime?.Dispose(); _APIResouceProp = null; - _GameSettings = null; - _GameRepair = null; - _GameCache = null; - _GameVersion = null; - _GameInstall = null; + _GameSettings = null; + _GameRepair = null; + _GameCache = null; + _GameVersion = null; + _GameInstall = null; + _GamePlaytime = null; } } diff --git a/CollapseLauncher/Classes/Helper/Animation/AnimationHelper.cs b/CollapseLauncher/Classes/Helper/Animation/AnimationHelper.cs index ee5329d0f..4f33ad75b 100644 --- a/CollapseLauncher/Classes/Helper/Animation/AnimationHelper.cs +++ b/CollapseLauncher/Classes/Helper/Animation/AnimationHelper.cs @@ -1,5 +1,5 @@ -using CommunityToolkit.WinUI.Controls; -using Hi3Helper; +using Hi3Helper; +using Hi3Helper.CommunityToolkit.WinUI.Controls; using Microsoft.UI.Composition; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -8,6 +8,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +// ReSharper disable CheckNamespace namespace CollapseLauncher.Helper.Animation { @@ -65,26 +66,26 @@ internal static async Task StartAnimation(this UIElement element, TimeSpan durat { foreach (KeyFrameAnimation anim in animBase!) { - if (element?.DispatcherQueue?.HasThreadAccess ?? false) + if (element.DispatcherQueue?.HasThreadAccess ?? false) { anim.Duration = duration; anim.StopBehavior = AnimationStopBehavior.LeaveCurrentValue; animGroup.Add(anim); } else - element?.DispatcherQueue?.TryEnqueue(() => + element.DispatcherQueue?.TryEnqueue(() => { anim.Duration = duration; anim.StopBehavior = AnimationStopBehavior.LeaveCurrentValue; animGroup.Add(anim); }); } - if (element?.DispatcherQueue?.HasThreadAccess ?? false) + if (element.DispatcherQueue?.HasThreadAccess ?? false) { element.StartAnimation(animGroup); } else - element?.DispatcherQueue?.TryEnqueue(() => + element.DispatcherQueue?.TryEnqueue(() => { element.StartAnimation(animGroup); }); @@ -123,69 +124,85 @@ internal static void EnableSingleImplicitAnimation(this UIElement element, rootFrameVisual.ImplicitAnimations = animationCollection; } - internal static void EnableImplicitAnimation(this UIElement element, - bool recursiveAssignment = false, - CompositionEasingFunction easingFunction = null) + internal static void EnableImplicitAnimation(this UIElement element, bool recursiveAssignment = false, CompositionEasingFunction easingFunction = null) { - try + while (true) { - Visual rootFrameVisual = ElementCompositionPreview.GetElementVisual(element); - Compositor compositor = CompositionTarget.GetCompositorForCurrentThread(); - - ImplicitAnimationCollection animationCollection = - rootFrameVisual.ImplicitAnimations != null ? - rootFrameVisual.ImplicitAnimations : compositor!.CreateImplicitAnimationCollection(); - - foreach (VisualPropertyType type in Enum.GetValues()) + try { - KeyFrameAnimation animation = CreateAnimationByType(compositor, type, 250, 0, easingFunction); + Visual rootFrameVisual = ElementCompositionPreview.GetElementVisual(element); + Compositor compositor = CompositionTarget.GetCompositorForCurrentThread(); - if (animation != null) - { - animationCollection[type.ToString()] = animation; - } - } + ImplicitAnimationCollection animationCollection = rootFrameVisual.ImplicitAnimations != null ? rootFrameVisual.ImplicitAnimations : compositor!.CreateImplicitAnimationCollection(); - rootFrameVisual.ImplicitAnimations = animationCollection; - element.EnableElementVisibilityAnimation(); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[AnimationHelper::EnableImplicitAnimation()] Error has occurred while assigning Implicit Animation to the element!\r\n{ex}", LogType.Error, true); - } + foreach (VisualPropertyType type in Enum.GetValues()) + { + KeyFrameAnimation animation = CreateAnimationByType(compositor, type, 250, 0, easingFunction); - if (!recursiveAssignment) return; + if (animation != null) + { + animationCollection[type.ToString()] = animation; + } + } - if (element is Button button && button.Content is UIElement buttonContent) - buttonContent.EnableImplicitAnimation(recursiveAssignment, easingFunction); + rootFrameVisual.ImplicitAnimations = animationCollection; + element.EnableElementVisibilityAnimation(); + } + catch (Exception ex) + { + Logger.LogWriteLine($"[AnimationHelper::EnableImplicitAnimation()] Error has occurred while assigning Implicit Animation to the element!\r\n{ex}", LogType.Error, true); + } - if (element is Page page && page.Content is UIElement pageContent) - pageContent.EnableImplicitAnimation(recursiveAssignment, easingFunction); + if (!recursiveAssignment) return; - if (element is NavigationView navigationView && navigationView.Content is UIElement navigationViewContent) - navigationViewContent.EnableImplicitAnimation(recursiveAssignment, easingFunction); + switch (element) + { + case Button { Content: UIElement buttonContent }: + buttonContent.EnableImplicitAnimation(true, easingFunction); + break; + case Page { Content: not null } page: + page.Content.EnableImplicitAnimation(true, easingFunction); + break; + case NavigationView { Content: UIElement navigationViewContent }: + navigationViewContent.EnableImplicitAnimation(true, easingFunction); + break; + case Panel panel: + { + foreach (UIElement childrenElement in panel.Children!) childrenElement.EnableImplicitAnimation(true, easingFunction); + break; + } + case ScrollViewer { Content: UIElement elementInner }: + elementInner.EnableImplicitAnimation(true, easingFunction); + break; + } - if (element is Panel panel) - foreach (UIElement childrenElement in panel.Children!) - childrenElement.EnableImplicitAnimation(recursiveAssignment, easingFunction); + switch (element) + { + case ContentControl { Content: UIElement contentControlInner } contentControl and (SettingsCard or Expander): + { + contentControlInner.EnableImplicitAnimation(true, easingFunction); - if (element is ScrollViewer scrollViewer && scrollViewer.Content is UIElement elementInner) - elementInner.EnableImplicitAnimation(recursiveAssignment, easingFunction); + if (contentControl is Expander { Header: UIElement expanderHeader }) + { + element = expanderHeader; + recursiveAssignment = true; + continue; + } - if (element is ContentControl contentControl && (element is SettingsCard || element is Expander) && contentControl.Content is UIElement contentControlInner) - { - contentControlInner.EnableImplicitAnimation(true, easingFunction); + break; + } + case InfoBar { Content: UIElement infoBarInner }: + element = infoBarInner; + recursiveAssignment = true; + continue; + } - if (contentControl is Expander expander && expander.Header is UIElement expanderHeader) - expanderHeader.EnableImplicitAnimation(true, easingFunction); + break; } - - if (element is InfoBar infoBar && infoBar.Content is UIElement infoBarInner) - infoBarInner.EnableImplicitAnimation(true, easingFunction); } private static KeyFrameAnimation CreateAnimationByType(Compositor compositor, VisualPropertyType type, - double duration = 800, double delay = 0, CompositionEasingFunction easing = null) + double duration = 800, double delay = 0, CompositionEasingFunction easing = null) { KeyFrameAnimation animation; @@ -202,6 +219,8 @@ private static KeyFrameAnimation CreateAnimationByType(Compositor compositor, Vi case VisualPropertyType.RotationAngleInDegrees: animation = compositor.CreateScalarKeyFrameAnimation(); break; + case VisualPropertyType.None: + case VisualPropertyType.All: default: return null; } @@ -221,25 +240,25 @@ public static void EnableElementVisibilityAnimation(this UIElement element, Comp ElementCompositionPreview.SetIsTranslationEnabled(element, true); - ScalarKeyFrameAnimation HideOpacityAnimation = compositor.CreateScalarKeyFrameAnimation(); - ScalarKeyFrameAnimation ShowOpacityAnimation = compositor.CreateScalarKeyFrameAnimation(); + ScalarKeyFrameAnimation hideOpacityAnimation = compositor.CreateScalarKeyFrameAnimation(); + ScalarKeyFrameAnimation showOpacityAnimation = compositor.CreateScalarKeyFrameAnimation(); - HideOpacityAnimation.InsertKeyFrame(1.0f, 0.0f); - HideOpacityAnimation.Duration = animDur; - HideOpacityAnimation.Target = "Opacity"; + hideOpacityAnimation.InsertKeyFrame(1.0f, 0.0f); + hideOpacityAnimation.Duration = animDur; + hideOpacityAnimation.Target = "Opacity"; - ShowOpacityAnimation.InsertKeyFrame(1.0f, 1.0f); - ShowOpacityAnimation.Duration = animDur; - ShowOpacityAnimation.Target = "Opacity"; + showOpacityAnimation.InsertKeyFrame(1.0f, 1.0f); + showOpacityAnimation.Duration = animDur; + showOpacityAnimation.Target = "Opacity"; - CompositionAnimationGroup HideAnimationGroup = compositor.CreateAnimationGroup(); - CompositionAnimationGroup ShowAnimationGroup = compositor.CreateAnimationGroup(); + CompositionAnimationGroup hideAnimationGroup = compositor.CreateAnimationGroup(); + CompositionAnimationGroup showAnimationGroup = compositor.CreateAnimationGroup(); - HideAnimationGroup.Add(HideOpacityAnimation); - ShowAnimationGroup.Add(ShowOpacityAnimation); + hideAnimationGroup.Add(hideOpacityAnimation); + showAnimationGroup.Add(showOpacityAnimation); - ElementCompositionPreview.SetImplicitHideAnimation(element, HideAnimationGroup); - ElementCompositionPreview.SetImplicitShowAnimation(element, ShowAnimationGroup); + ElementCompositionPreview.SetImplicitHideAnimation(element, hideAnimationGroup); + ElementCompositionPreview.SetImplicitShowAnimation(element, showAnimationGroup); } internal static Compositor GetElementCompositor(this UIElement element) diff --git a/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs b/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs index a2b0f1c81..909cf6302 100644 --- a/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs +++ b/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs @@ -37,7 +37,7 @@ internal enum MediaType internal static readonly string[] SupportedMediaPlayerExt = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".gif"]; - private FrameworkElement? _parentUI; + private static FrameworkElement? _parentUI; private ImageUI? _bgImageBackground; private ImageUI? _bgImageBackgroundLast; private MediaPlayerElement? _bgMediaPlayerBackground; @@ -48,7 +48,8 @@ internal enum MediaType private Grid? _parentBgImageBackgroundGrid; private Grid? _parentBgMediaPlayerBackgroundGrid; - internal MediaType CurrentAppliedMediaType = MediaType.Unknown; + internal static string? CurrentAppliedMediaPath; + internal static MediaType CurrentAppliedMediaType = MediaType.Unknown; private CancellationTokenSourceWrapper? _cancellationToken; private IBackgroundMediaLoader? _loaderStillImage; @@ -60,16 +61,45 @@ internal enum MediaType private delegate ValueTask AssignDefaultAction(T element) where T : class; internal delegate void ThrowExceptionAction(Exception element); - internal static ActionBlock? SharedActionBlockQueue = new ActionBlock(async (action) => { - await action; + internal static ActionBlock? SharedActionBlockQueue = new ActionBlock(async (action) => + { + try + { + await action; + } + catch (Exception ex) + { + _parentUI?.DispatcherQueue.TryEnqueue(() => + ErrorSender.SendException(ex)); + } }, new ExecutionDataflowBlockOptions { EnsureOrdered = true, MaxMessagesPerTask = 1, MaxDegreeOfParallelism = 1, - TaskScheduler = TaskScheduler.Default + BoundedCapacity = 1, + TaskScheduler = TaskScheduler.Current }); + internal ActionBlock SharedActionBlockQueueChange = new ActionBlock(static (action) => + { + try + { + _parentUI?.DispatcherQueue.TryEnqueue(() => action()); + } + catch (Exception ex) + { + _parentUI?.DispatcherQueue.TryEnqueue(() => + ErrorSender.SendException(ex)); + } + }, + new ExecutionDataflowBlockOptions + { + EnsureOrdered = true, + MaxMessagesPerTask = 1, + MaxDegreeOfParallelism = 1, + BoundedCapacity = 1 + }); /// /// Attach and register the of the page to be assigned with background utility. @@ -84,6 +114,14 @@ internal static async Task CreateInstanceAsync(Framework Grid bgOverlayTitleBar, Grid bgImageGridBackground, Grid bgMediaPlayerGrid) { + CurrentAppliedMediaPath = null; + CurrentAppliedMediaType = MediaType.Unknown; + if (_alternativeFileStream != null) + { + await _alternativeFileStream.DisposeAsync(); + _alternativeFileStream = null; + } + // Set the parent UI FrameworkElement? ui = parentUI; @@ -150,8 +188,6 @@ public void Dispose() _alternativeFileStream?.Dispose(); _alternativeFileStream = null; - CurrentAppliedMediaType = MediaType.Unknown; - _isCurrentRegistered = false; GC.SuppressFinalize(this); } @@ -287,19 +323,31 @@ private void EnsureCurrentMediaPlayerRegistered() /// Path of the background file /// Request an initialization before processing the background file /// Request a cache recreation if the background file properties have been cached + /// Action to do after exception occurred + /// Action to do after background is loaded /// Throws if the background file is not supported /// Throws if some instances aren't yet initialized - internal void LoadBackground(string mediaPath, bool isRequestInit = false, - bool isForceRecreateCache = false, ThrowExceptionAction? throwAction = null, - Action? actionAfterLoaded = null) + internal async void LoadBackground(string mediaPath, + bool isRequestInit = false, + bool isForceRecreateCache = false, + ThrowExceptionAction? throwAction = null, + Action? actionAfterLoaded = null) { - SharedActionBlockQueue?.Post(LoadBackgroundInner(mediaPath, isRequestInit, isForceRecreateCache, throwAction, actionAfterLoaded)); + while (!await SharedActionBlockQueue?.SendAsync(LoadBackgroundInner(mediaPath, isRequestInit, isForceRecreateCache, throwAction, actionAfterLoaded))!) + { + // Delay the invoke 1/4 second and wait until the action can + // be sent again. + await Task.Delay(250); + } } private async Task LoadBackgroundInner(string mediaPath, bool isRequestInit = false, - bool isForceRecreateCache = false, ThrowExceptionAction? throwAction = null, - Action? actionAfterLoaded = null) + bool isForceRecreateCache = false, ThrowExceptionAction? throwAction = null, + Action? actionAfterLoaded = null) { + if (mediaPath.Equals(CurrentAppliedMediaPath, StringComparison.OrdinalIgnoreCase)) + return; + try { while (!_isCurrentRegistered) @@ -349,7 +397,8 @@ private async Task LoadBackgroundInner(string mediaPath, bool _loaderMediaPlayer?.Show(); break; case MediaType.StillImage: - _loaderStillImage?.Show(); + _loaderStillImage?.Show(CurrentAppliedMediaType == MediaType.Media + || InnerLauncherConfig.m_appCurrentFrameName != "HomePage"); _loaderMediaPlayer?.Hide(); break; } @@ -362,6 +411,8 @@ private async Task LoadBackgroundInner(string mediaPath, bool CurrentAppliedMediaType = mediaType; actionAfterLoaded?.Invoke(); + + CurrentAppliedMediaPath = mediaPath; } catch (Exception ex) { @@ -474,4 +525,4 @@ public static MediaType GetMediaType(string mediaPath) return SupportedMediaPlayerExt.Contains(extension, StringComparer.OrdinalIgnoreCase) ? MediaType.Media : MediaType.Unknown; } } -} \ No newline at end of file +} diff --git a/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs b/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs index 1c540b262..3bc0cbf0a 100644 --- a/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs +++ b/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs @@ -210,7 +210,7 @@ private static async Task GetPaletteList(BitmapInputStruct bitmapInput, { LumaUtils.DarkThreshold = isLight ? 200f : 400f; LumaUtils.IgnoreWhiteThreshold = isLight ? 900f : 800f; - LumaUtils.ChangeCoeToBT601(); + // LumaUtils.ChangeCoeToBT601(); QuantizedColor averageColor = await Task.Run(() => ColorThief.GetColor(bitmapInput.Buffer, @@ -219,7 +219,7 @@ private static async Task GetPaletteList(BitmapInputStruct bitmapInput, ; WColor wColor = DrawingColorToColor(averageColor); - WColor adjustedColor = wColor.SetSaturation(1.5); + WColor adjustedColor = wColor.SetSaturation(1.2); adjustedColor = isLight ? adjustedColor.GetDarkColor() : adjustedColor.GetLightColor(); return adjustedColor; diff --git a/CollapseLauncher/Classes/Helper/Background/Loaders/IBackgroundMediaLoader.cs b/CollapseLauncher/Classes/Helper/Background/Loaders/IBackgroundMediaLoader.cs index c78108bf4..7277dc0ad 100644 --- a/CollapseLauncher/Classes/Helper/Background/Loaders/IBackgroundMediaLoader.cs +++ b/CollapseLauncher/Classes/Helper/Background/Loaders/IBackgroundMediaLoader.cs @@ -16,7 +16,7 @@ internal interface IBackgroundMediaLoader : IDisposable Task LoadAsync(string filePath, bool isForceRecreateCache = false, bool isRequestInit = false, CancellationToken token = default); void Dimm(); void Undimm(); - void Show(); + void Show(bool isForceShow = false); void Hide(); void Mute(); void Unmute(); diff --git a/CollapseLauncher/Classes/Helper/Background/Loaders/MediaPlayerLoader.cs b/CollapseLauncher/Classes/Helper/Background/Loaders/MediaPlayerLoader.cs index 950f9bb0b..00bfd65f1 100644 --- a/CollapseLauncher/Classes/Helper/Background/Loaders/MediaPlayerLoader.cs +++ b/CollapseLauncher/Classes/Helper/Background/Loaders/MediaPlayerLoader.cs @@ -3,31 +3,28 @@ using CommunityToolkit.WinUI.Animations; #if USEFFMPEGFORVIDEOBG using FFmpegInteropX; -using Hi3Helper; - #endif +using Hi3Helper; using Hi3Helper.Shared.Region; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.UI.Composition; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Windows.Graphics.Imaging; using Windows.Media.Playback; using Windows.Storage; using Windows.Storage.FileProperties; - -#if USEDYNAMICVIDEOPALETTE -using CollapseLauncher.Extension; -using Microsoft.Graphics.Canvas; -using Microsoft.Graphics.Canvas.UI.Xaml; -using Microsoft.UI.Xaml.Media; -using System.Diagnostics; -using Windows.Graphics.Imaging; +using Windows.UI; using ImageUI = Microsoft.UI.Xaml.Controls.Image; -#endif +using static Hi3Helper.Logger; #nullable enable namespace CollapseLauncher.Helper.Background.Loaders @@ -46,26 +43,24 @@ internal class MediaPlayerLoader : IBackgroundMediaLoader private MediaPlayerElement? CurrentMediaPlayerFrame { get; } private Grid CurrentMediaPlayerFrameParentGrid { get; } + private bool IsUseVideoBGDynamicColorUpdate { get => LauncherConfig.IsUseVideoBGDynamicColorUpdate && LauncherConfig.EnableAcrylicEffect; } private Grid AcrylicMask { get; } private Grid OverlayTitleBar { get; } - public bool IsBackgroundDimm { get; set; } + public bool IsBackgroundDimm { get; set; } private FileStream? CurrentMediaStream { get; set; } private MediaPlayer? CurrentMediaPlayer { get; set; } - private CancellationTokenSourceWrapper? InnerCancellationToken { get; set; } #if USEFFMPEGFORVIDEOBG private FFmpegMediaSource? CurrentFFmpegMediaSource { get; set; } #endif -#if USEDYNAMICVIDEOPALETTE private ImageUI? CurrentMediaImage { get; } - private Stopwatch? CurrentStopwatch { get; set; } private SoftwareBitmap? CurrentFrameBitmap { get; set; } private CanvasImageSource? CurrentCanvasImageSource { get; set; } private CanvasDevice? CanvasDevice { get; set; } private CanvasBitmap? CanvasBitmap { get; set; } -#endif + private bool IsCanvasCurrentlyDrawing { get; set; } internal MediaPlayerLoader( FrameworkElement parentUI, @@ -81,7 +76,6 @@ internal MediaPlayerLoader( CurrentMediaPlayerFrameParentGrid = mediaPlayerParentGrid; CurrentMediaPlayerFrame = mediaPlayerCurrent; -#if USEDYNAMICVIDEOPALETTE CurrentMediaImage = mediaPlayerParentGrid.AddElementToGridRowColumn(new ImageUI() .WithHorizontalAlignment(HorizontalAlignment .Center) @@ -89,21 +83,24 @@ internal MediaPlayerLoader( .Center) .WithStretch(Stretch .UniformToFill)); -#endif } - ~MediaPlayerLoader() => Dispose(); + ~MediaPlayerLoader() + { + LogWriteLine($"[~MediaPlayerLoader()] MediaPlayerLoader Deconstructor has been called!", LogType.Warning, true); + Dispose(); + } public void Dispose() { -#if USEDYNAMICVIDEOPALETTE - CurrentStopwatch?.Stop(); - CanvasDevice?.Dispose(); - CurrentFrameBitmap?.Dispose(); -#endif - CurrentMediaPlayer?.Dispose(); - InnerCancellationToken?.Dispose(); - CurrentMediaStream?.Dispose(); + try + { + DisposeMediaModules(); + } + catch (Exception ex) + { + LogWriteLine($"Error disposing Media Modules: {ex.Message}", LogType.Error, true); + } GC.SuppressFinalize(this); } @@ -111,69 +108,39 @@ public void Dispose() public async Task LoadAsync(string filePath, bool isImageLoadForFirstTime, bool isRequestInit, CancellationToken token) { - if (CurrentMediaStream != null) - { - await CurrentMediaStream.DisposeAsync(); - } - - if (CurrentMediaPlayer != null) - { - CurrentMediaPlayer.Dispose(); - } - try { - if (InnerCancellationToken != null) - { - await InnerCancellationToken.CancelAsync(); - InnerCancellationToken.Dispose(); - } - - InnerCancellationToken = new CancellationTokenSourceWrapper(); - - if (CurrentMediaPlayer != null) - { -#if USEDYNAMICVIDEOPALETTE - CurrentMediaPlayer.VideoFrameAvailable -= FrameGrabberEvent; -#endif -#if !USEDYNAMICVIDEOPALETTE - CurrentMediaPlayer.Dispose(); -#endif - } + DisposeMediaModules(); int canvasWidth = (int)CurrentMediaPlayerFrameParentGrid.ActualWidth; int canvasHeight = (int)CurrentMediaPlayerFrameParentGrid.ActualHeight; -#if USEDYNAMICVIDEOPALETTE - await BackgroundMediaUtility.AssignDefaultImage(CurrentMediaImage); - - CanvasDevice = CanvasDevice.GetSharedDevice(); - if (CurrentFrameBitmap == null) + if (IsUseVideoBGDynamicColorUpdate) { - // FrameServerImage in this example is a XAML image control - CurrentFrameBitmap = new SoftwareBitmap(BitmapPixelFormat.Rgba8, canvasWidth, canvasHeight, - BitmapAlphaMode.Ignore); + CanvasDevice ??= CanvasDevice.GetSharedDevice(); + CurrentFrameBitmap ??= new SoftwareBitmap(BitmapPixelFormat.Rgba8, canvasWidth, canvasHeight, + BitmapAlphaMode.Ignore); + CurrentCanvasImageSource ??= + new CanvasImageSource(CanvasDevice, canvasWidth, canvasHeight, 96, CanvasAlphaMode.Ignore); + + CurrentMediaImage!.Source = CurrentCanvasImageSource; + CurrentMediaImage.Visibility = Visibility.Visible; + App.ToggleBlurBackdrop(true); } - - if (CurrentCanvasImageSource == null) + else if (CurrentMediaImage != null) { - CurrentCanvasImageSource = - new CanvasImageSource(CanvasDevice, canvasWidth, canvasHeight, 96); //96); + CurrentMediaImage.Visibility = Visibility.Collapsed; + } - CurrentMediaImage!.Source = CurrentCanvasImageSource; - - CurrentStopwatch = Stopwatch.StartNew(); -#endif - await GetPreviewAsColorPalette(filePath); - CurrentMediaStream = BackgroundMediaUtility.GetAlternativeFileStream() ?? File.Open(filePath, StreamUtility.FileStreamOpenReadOpt); + CurrentMediaStream ??= BackgroundMediaUtility.GetAlternativeFileStream() ?? File.Open(filePath, StreamUtility.FileStreamOpenReadOpt); #if !USEFFMPEGFORVIDEOBG EnsureIfFormatIsDashOrUnsupported(CurrentMediaStream); #endif - CurrentMediaPlayer = new MediaPlayer(); + CurrentMediaPlayer ??= new MediaPlayer(); if (WindowUtility.IsCurrentWindowInFocus()) { @@ -190,11 +157,6 @@ public async Task LoadAsync(string filePath, bool isImageLoadF #if !USEFFMPEGFORVIDEOBG CurrentMediaPlayer.SetStreamSource(CurrentMediaStream.AsRandomAccessStream()); #else - if (CurrentFFmpegMediaSource != null) - { - CurrentFFmpegMediaSource.Dispose(); - CurrentFFmpegMediaSource = null; - } CurrentFFmpegMediaSource ??= await FFmpegMediaSource.CreateFromStreamAsync(CurrentMediaStream.AsRandomAccessStream()); @@ -231,17 +193,19 @@ public async Task LoadAsync(string filePath, bool isImageLoadF CurrentFFmpegMediaSource.CurrentVideoStream?.DecoderEngine ?? 0 // 12 ), LogType.Debug, true); #endif - -#if USEDYNAMICVIDEOPALETTE - CurrentMediaPlayer.IsVideoFrameServerEnabled = true; - CurrentMediaPlayer.VideoFrameAvailable += FrameGrabberEvent; -#endif + CurrentMediaPlayer.IsVideoFrameServerEnabled = IsUseVideoBGDynamicColorUpdate; + if (IsUseVideoBGDynamicColorUpdate) + { + CurrentMediaPlayer.VideoFrameAvailable += FrameGrabberEvent; + } CurrentMediaPlayerFrame?.SetMediaPlayer(CurrentMediaPlayer); + CurrentMediaPlayer.Play(); } catch { - CurrentMediaStream?.Dispose(); + DisposeMediaModules(); + await BackgroundMediaUtility.AssignDefaultImage(CurrentMediaImage); throw; } finally @@ -251,6 +215,39 @@ public async Task LoadAsync(string filePath, bool isImageLoadF } } + public void DisposeMediaModules() + { + if (CurrentMediaPlayer != null) + { + CurrentMediaPlayer.VideoFrameAvailable -= FrameGrabberEvent; + } + + if (IsUseVideoBGDynamicColorUpdate) + { + while (IsCanvasCurrentlyDrawing) + { + Thread.Sleep(100); + } + + CanvasDevice?.Dispose(); + CanvasDevice = null; + CurrentFrameBitmap?.Dispose(); + CurrentFrameBitmap = null; + CanvasBitmap?.Dispose(); + CanvasBitmap = null; + } + +#if USEFFMPEGFORVIDEOBG + CurrentFFmpegMediaSource?.Dispose(); + CurrentFFmpegMediaSource = null; +#endif + CurrentMediaPlayer?.Dispose(); + CurrentMediaPlayer = null; + CurrentCanvasImageSource = null; + CurrentMediaStream?.Dispose(); + CurrentMediaStream = null; + } + #if !USEFFMPEGFORVIDEOBG private void EnsureIfFormatIsDashOrUnsupported(Stream stream) { @@ -269,7 +266,6 @@ private void EnsureIfFormatIsDashOrUnsupported(Stream stream) stream.Position = 0; } } - #endif private async ValueTask GetPreviewAsColorPalette(string file) @@ -285,73 +281,39 @@ await ColorPaletteUtility.ApplyAccentColor(ParentUI, stream.AsRandomAccessStream private async ValueTask GetFileAsStorageFile(string filePath) => await StorageFile.GetFileFromPathAsync(filePath); -#if USEDYNAMICVIDEOPALETTE private void FrameGrabberEvent(MediaPlayer mediaPlayer, object args) { - ParentUI?.DispatcherQueue?.TryEnqueue(() => - { - int bitmapWidth = CurrentFrameBitmap?.PixelWidth ?? 0; - int bitmapHeight = CurrentFrameBitmap?.PixelHeight ?? 0; - if (CanvasBitmap == null) - { - CanvasBitmap = - CanvasBitmap.CreateFromSoftwareBitmap(CanvasDevice, - CurrentFrameBitmap); - } - - using CanvasDrawingSession? ds = - CurrentCanvasImageSource - ?.CreateDrawingSession(Color.FromArgb(255, 0, 0, 0)); - mediaPlayer.CopyFrameToVideoSurface(CanvasBitmap); - ds?.DrawImage(CanvasBitmap); - - if (!(CurrentStopwatch?.ElapsedMilliseconds > 5000)) - { - return; - } + IsCanvasCurrentlyDrawing = true; + int bitmapWidth = CurrentFrameBitmap?.PixelWidth ?? 0; + int bitmapHeight = CurrentFrameBitmap?.PixelHeight ?? 0; - CurrentStopwatch.Restart(); - byte[] bufferBytes = CanvasBitmap.GetPixelBytes(); - IntPtr ptr = ArrayToPtr(bufferBytes); - - if (bufferBytes.Length < bitmapWidth * bitmapHeight * 4) return; - BitmapInputStruct bitmapInput = new BitmapInputStruct - { - Buffer = ptr, - Channel = 4, - Width = bitmapWidth, - Height = bitmapHeight - }; - UpdatePalette(bitmapInput); - }); - } - - private async void UpdatePalette(BitmapInputStruct bitmapInput) - { - await Task.Run(() => + if (CurrentCanvasImageSource == null) { - QuantizedColor color = ColorThief.GetColor(bitmapInput.Buffer, - bitmapInput.Channel, bitmapInput.Width, - bitmapInput.Height, 10); - Color adjustedColor = Color - .FromArgb(255, color.Color.R, color.Color.G, color.Color.B) - .SetSaturation(1.5); - adjustedColor = InnerLauncherConfig.IsAppThemeLight - ? adjustedColor.GetDarkColor() - : adjustedColor.GetLightColor(); - - ParentUI?.DispatcherQueue?.TryEnqueue(() => ColorPaletteUtility.SetColorPalette(ParentUI, adjustedColor)); - }); - } + IsCanvasCurrentlyDrawing = false; + return; + } - private unsafe IntPtr ArrayToPtr(byte[] buffer) - { - fixed (byte* bufferPtr = &buffer[0]) + lock (this) { - return (IntPtr)bufferPtr; + CurrentCanvasImageSource?.DispatcherQueue.TryEnqueue(() => + { + try + { + // Check one more time due to high possibility of thread-race issue. + if (CurrentCanvasImageSource == null) + return; + + CanvasBitmap ??= CanvasBitmap.CreateFromSoftwareBitmap(CanvasDevice, CurrentFrameBitmap); + using CanvasDrawingSession canvasDrawingSession = CurrentCanvasImageSource.CreateDrawingSession(Color.FromArgb(0, 0, 0, 0)); + + mediaPlayer.CopyFrameToVideoSurface(CanvasBitmap); + canvasDrawingSession.DrawImage(CanvasBitmap); + } + catch { } + }); } + IsCanvasCurrentlyDrawing = false; } -#endif public void Dimm() { @@ -387,7 +349,7 @@ await Task.WhenAll( ); } - public void Show() + public void Show(bool isForceShow = false) { BackgroundMediaUtility.SharedActionBlockQueue?.Post(ShowInner()); } @@ -396,9 +358,10 @@ private async Task ShowInner() { if (CurrentMediaPlayerFrameParentGrid.Opacity > 0f) return; -#if !USEDYNAMICVIDEOPALETTE - App.ToggleBlurBackdrop(false); -#endif + if (!IsUseVideoBGDynamicColorUpdate) + { + App.ToggleBlurBackdrop(false); + } TimeSpan duration = TimeSpan.FromSeconds(BackgroundMediaUtility.TransitionDuration); await CurrentMediaPlayerFrameParentGrid @@ -416,9 +379,11 @@ public void Hide() private async Task HideInner() { bool isLastAcrylicEnabled = LauncherConfig.GetAppConfigValue("EnableAcrylicEffect").ToBool(); -#if !USEDYNAMICVIDEOPALETTE - App.ToggleBlurBackdrop(isLastAcrylicEnabled); -#endif + + if (!IsUseVideoBGDynamicColorUpdate) + { + App.ToggleBlurBackdrop(isLastAcrylicEnabled); + } if (CurrentMediaPlayerFrameParentGrid.Opacity < 1f) return; TimeSpan duration = TimeSpan.FromSeconds(BackgroundMediaUtility.TransitionDuration); @@ -431,24 +396,12 @@ await CurrentMediaPlayerFrameParentGrid .Opacity) ); - if (CurrentMediaStream != null) - { - await CurrentMediaStream.DisposeAsync(); - } - - if (CurrentMediaPlayer != null) - { - CurrentMediaPlayer.Dispose(); - } - -#if USEDYNAMICVIDEOPALETTE - CurrentStopwatch?.Stop(); -#endif + DisposeMediaModules(); } - public void WindowUnfocused() + public async void WindowUnfocused() { - BackgroundMediaUtility.SharedActionBlockQueue?.Post(WindowUnfocusedInner()); + await (BackgroundMediaUtility.SharedActionBlockQueue?.SendAsync(WindowUnfocusedInner()) ?? Task.CompletedTask); } private async Task WindowUnfocusedInner() @@ -458,9 +411,9 @@ private async Task WindowUnfocusedInner() Pause(); } - public void WindowFocused() + public async void WindowFocused() { - BackgroundMediaUtility.SharedActionBlockQueue?.Post(WindowFocusedInner()); + await (BackgroundMediaUtility.SharedActionBlockQueue?.SendAsync(WindowFocusedInner()) ?? Task.CompletedTask); } private async Task WindowFocusedInner() @@ -497,7 +450,7 @@ private async ValueTask InterpolateVolumeChange(float from, float to, bool isMut double inc = isMute ? -0.05 : 0.05; Loops: - current += inc; + current += inc; CurrentMediaPlayer.Volume = current; await Task.Delay(10); @@ -516,7 +469,8 @@ private async ValueTask InterpolateVolumeChange(float from, float to, bool isMut public void SetVolume(double value) { - CurrentMediaPlayer!.Volume = value; + if (CurrentMediaPlayer != null) + CurrentMediaPlayer.Volume = value; LauncherConfig.SetAndSaveConfigValue("BackgroundAudioVolume", value); } diff --git a/CollapseLauncher/Classes/Helper/Background/Loaders/StillImageLoader.cs b/CollapseLauncher/Classes/Helper/Background/Loaders/StillImageLoader.cs index 54ced9190..8854a0216 100644 --- a/CollapseLauncher/Classes/Helper/Background/Loaders/StillImageLoader.cs +++ b/CollapseLauncher/Classes/Helper/Background/Loaders/StillImageLoader.cs @@ -30,7 +30,12 @@ internal class StillImageLoader : IBackgroundMediaLoader private Grid AcrylicMask { get; } private Grid OverlayTitleBar { get; } private double AnimationDuration { get; } - public bool IsBackgroundDimm { get; set; } + + public bool IsBackgroundDimm + { + get; + set; + } internal StillImageLoader( FrameworkElement parentUI, @@ -39,6 +44,7 @@ internal StillImageLoader( ImageUI? imageBackCurrent, ImageUI? imageBackLast, double animationDuration = BackgroundMediaUtility.TransitionDuration) { + GC.SuppressFinalize(this); ParentUI = parentUI; CurrentCompositor = parentUI.GetElementCompositor(); @@ -56,7 +62,7 @@ internal StillImageLoader( public void Dispose() { - GC.SuppressFinalize(this); + GC.Collect(); } public async Task LoadAsync(string filePath, bool isImageLoadForFirstTime, @@ -85,11 +91,11 @@ await Task.WhenAll( isImageLoadForFirstTime, false), ApplyAndSwitchImage(AnimationDuration, bitmapImage) ); + } finally { GC.Collect(); - GC.WaitForPendingFinalizers(); } } @@ -107,9 +113,11 @@ private async Task ApplyAndSwitchImage(double duration, BitmapImage imageToApply await Task.WhenAll( ImageBackCurrent.StartAnimation(timeSpan, CurrentCompositor - .CreateScalarKeyFrameAnimation("Opacity", 1, 0)), + .CreateScalarKeyFrameAnimation("Opacity", + 1, 0)), ImageBackLast.StartAnimation(timeSpan, - CurrentCompositor.CreateScalarKeyFrameAnimation("Opacity", + CurrentCompositor + .CreateScalarKeyFrameAnimation("Opacity", 0, 1, timeSpan * 0.8)) ); } @@ -124,10 +132,18 @@ public void Undimm() BackgroundMediaUtility.SharedActionBlockQueue?.Post(ToggleImageVisibility(false)); } - private async Task ToggleImageVisibility(bool hideImage, bool completeInvisible = false) + private async Task ToggleImageVisibility(bool hideImage, bool completeInvisible = false, bool isForceShow = false) { - if (IsBackgroundDimm == hideImage) return; - IsBackgroundDimm = hideImage; + if (isForceShow) + { + hideImage = false; + completeInvisible = false; + } + else + { + if (IsBackgroundDimm == hideImage) return; + IsBackgroundDimm = hideImage; + } TimeSpan duration = TimeSpan.FromSeconds(hideImage ? BackgroundMediaUtility.TransitionDuration @@ -141,7 +157,26 @@ private async Task ToggleImageVisibility(bool hideImage, bool completeInvisible Vector3 toTranslate = new Vector3(-((float)(ImageBackParentGrid?.ActualWidth ?? 0) * (toScale - 1f) / 2), -((float)(ImageBackParentGrid?.ActualHeight ?? 0) * (toScale - 1f) / 2), 0); - if (completeInvisible) + if (isForceShow) + { + await Task.WhenAll( + AcrylicMask.StartAnimation( + duration, + CurrentCompositor.CreateScalarKeyFrameAnimation("Opacity", hideImage ? 1f : 0f, hideImage ? 0f : 1f) + ), + OverlayTitleBar.StartAnimation( + duration, + CurrentCompositor.CreateScalarKeyFrameAnimation("Opacity", hideImage ? 0f : 1f, hideImage ? 1f : 0f) + ), + ImageBackParentGrid.StartAnimation( + duration, + CurrentCompositor.CreateVector3KeyFrameAnimation("Scale", new Vector3(hideImage ? toScale : fromScale), new Vector3(!hideImage ? toScale : fromScale)), + CurrentCompositor.CreateVector3KeyFrameAnimation("Translation", hideImage ? toTranslate : fromTranslate, !hideImage ? toTranslate : fromTranslate), + CurrentCompositor.CreateScalarKeyFrameAnimation("Opacity", 1f, 0f) + ) + ); + } + else if (completeInvisible) { await Task.WhenAll( ImageBackParentGrid.StartAnimation( @@ -170,10 +205,10 @@ await Task.WhenAll( } } - public void Show() + public void Show(bool isForceShow = false) { if (ImageBackParentGrid?.Opacity > 0f) return; - BackgroundMediaUtility.SharedActionBlockQueue?.Post(ToggleImageVisibility(false, true)); + BackgroundMediaUtility.SharedActionBlockQueue?.Post(ToggleImageVisibility(false, true, isForceShow)); } public void Hide() diff --git a/CollapseLauncher/Classes/Helper/Database/DBConfig.cs b/CollapseLauncher/Classes/Helper/Database/DBConfig.cs new file mode 100644 index 000000000..ecdb9dd9f --- /dev/null +++ b/CollapseLauncher/Classes/Helper/Database/DBConfig.cs @@ -0,0 +1,118 @@ +using Hi3Helper.Data; +using Hi3Helper.Shared.Region; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace CollapseLauncher.Helper.Database +{ + public static class DbConfig + { + #region Properties + + private static readonly string _configFolder = LauncherConfig.AppDataFolder; + + private const string _configFileName = "dbConfig.ini"; + private const string DbSectionName = "database"; + + private static readonly string _configPath = Path.Combine(_configFolder, _configFileName); + + // ReSharper disable once FieldCanBeMadeReadOnly.Local + private static IniFile _config = new(); + #endregion + + public static void Init() + { + EnsureConfigExist(); + Load(); + if (!_config.ContainsSection(DbSectionName)) + _config.Add(DbSectionName, DbSettingsTemplate); + + DefaultChecker(); + } + + private static void EnsureConfigExist() + { + if (File.Exists(_configPath)) return; + + var f = File.Create(_configPath); + f.Close(); + } + + private static void DefaultChecker() + { + foreach (KeyValuePair Entry in DbSettingsTemplate) + { + if (!_config[DbSectionName].ContainsKey(Entry.Key) || + string.IsNullOrEmpty(_config[DbSectionName][Entry.Key].Value)) + { + SetValue(Entry.Key, Entry.Value); + } + } + } + + private static void Load() => _config.Load(_configPath); + private static void Save() => _config.Save(_configPath); + + public static IniValue GetConfig(string key) => _config[DbSectionName][key]; + + private static void SetValue(string key, IniValue value) => _config[DbSectionName]![key] = value; + + public static void SetAndSaveValue(string key, IniValue value) + { + SetValue(key, value); + Save(); + } + + #region Template + private static readonly Dictionary DbSettingsTemplate = new() + { + { "enabled", false }, + { "url", "" }, + { "token", "" }, + { "userGuid", "" } + }; + #endregion + + public static bool DbEnabled + { + get => GetConfig("enabled").ToBool(); + set => SetAndSaveValue("enabled", value); + } + + [DebuggerHidden] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public static string DbUrl + { + get => GetConfig("url").ToString(); + set => SetAndSaveValue("url", value); + } + + [DebuggerHidden] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public static string DbToken + { + get => GetConfig("token").ToString(); + set => SetAndSaveValue("token", value); + } + + [DebuggerHidden] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public static string UserGuid + { + get + { + var c = GetConfig("userGuid").ToString(); + + if (!string.IsNullOrEmpty(c)) return c; // Return early if config is set + + c = Guid.CreateVersion7().ToString(); + SetAndSaveValue("userGuid", c); + + return c; + } + set => SetAndSaveValue("userGuid", value); + } + } +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs new file mode 100644 index 000000000..11698e328 --- /dev/null +++ b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs @@ -0,0 +1,329 @@ +using Hi3Helper; +using Libsql.Client; +using System; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static Hi3Helper.Locale; +using static Hi3Helper.Logger; + +#nullable enable +namespace CollapseLauncher.Helper.Database +{ + internal static class DbHandler + { + #region Config Properties + + private static bool? _enabled; + public static bool IsEnabled + { + get + { + if (_enabled != null) return (bool)_enabled; + var c = DbConfig.DbEnabled; + _enabled = c; + return c; + } + set + { + _enabled = value; + DbConfig.DbEnabled = value; + + _isFirstInit = true; // Force first init + if (value) _ = Init(); + else Dispose(); // Dispose instance if user disabled database function globally + } + } + + + private static string? _uri; + public static string Uri + { + get + { + if (!string.IsNullOrEmpty(_uri)) return _uri; + var c = DbConfig.DbUrl; + _uri = c; + return c; + } + set + { + if (value != _uri) _isFirstInit = true; // Force first init if value changed + + _uri = value; + DbConfig.DbUrl = value; + _isFirstInit = true; + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static string? _token; + + [DebuggerHidden] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public static string Token + { + get + { + if (!string.IsNullOrEmpty(_token)) return _token; + var c = DbConfig.DbToken; + _token = c; + return c; + } + set + { + if (value != _token) _isFirstInit = true; // Force first init if value changed + + _token = value; + DbConfig.DbToken = value; + _isFirstInit = true; + } + } + + private static string? _userId; + private static string? _userIdHash; + public static string UserId + { + get + { + if (_userId != null) return _userId; + var c = DbConfig.UserGuid; // Get or create (if not yet has one) GUIDv7 + _userId = c; + _userIdHash = Convert.ToHexStringLower(System.IO.Hashing.XxHash64.Hash(Encoding.ASCII.GetBytes(c))); + // Get hash for the UserID to be used as SQL table name + // I know that this is overkill, but I want it to be totally non-identifiable if for some reason someone + // has access to their database. It also lowers the amount of query command length to be sent, hopefully + // reducing access latency. + // p.s. oh yeah, this is also why user won't be able to get their data back if they lost the GUID, + // good luck reversing Xxhash64 back to string. Technically possible, but good luck! + return c; + } + set + { + if (value != _userId) _isFirstInit = true; // Force first init if value changed + + _userId = value; + DbConfig.UserGuid = value; + + var byteUidH = System.IO.Hashing.XxHash64.Hash(Encoding.ASCII.GetBytes(value)); + _userIdHash = Convert.ToHexStringLower(byteUidH); + _isFirstInit = true; + } + } + + private static bool _isFirstInit = true; + #endregion + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static IDatabaseClient? _database; + + public static async Task Init(bool redirectThrow = false, bool bypassEnableFlag = false) + { + DbConfig.Init(); + + if (!bypassEnableFlag && !IsEnabled) + { + LogWriteLine("[DbHandler::Init] Database functionality is disabled!"); + return; + } + + try + { + // Init props + _ = Token; + _ = Uri; + _ = UserId; + + if (string.IsNullOrEmpty(Uri)) + throw new NullReferenceException(Lang._SettingsPage.Database_Error_EmptyUri); + if (string.IsNullOrEmpty(Token)) + throw new NullReferenceException(Lang._SettingsPage.Database_Error_EmptyToken); + + // Connect to database + // Libsql-client-dotnet technically support file based SQLite by pushing `file://` proto in the URL. + // But what's the point? + _database = await DatabaseClient.Create(opts => + { + opts.Url = Uri; + opts.AuthToken = Token; + }); + + if (_isFirstInit) + { + LogWriteLine("[DbHandler::Init] Initializing database system..."); + // Ensure table exist at first initialization + await + _database + .Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' TEXT)"); + _isFirstInit = false; + } + else LogWriteLine("[DbHandler::Init] Reinitializing database system..."); + } + catch (LibsqlException e) when (e.Message.Contains("`api error: `{\"error\":\"Unauthorized: `The JWT is invalid`\"}``", + StringComparison.InvariantCultureIgnoreCase) && !redirectThrow) + { + LogWriteLine($"[DBHandler::Init] Error when connecting to database system! Token invalid!\r\n{e}", + LogType.Error, true); + } + catch (Exception e) when (!redirectThrow) + { + LogWriteLine($"[DBHandler::Init] Error when (re)initializing database system!\r\n{e}", LogType.Error, true); + } + catch (LibsqlException e) when (e.Message.Contains("`api error: `{\"error\":\"Unauthorized: `The JWT is invalid`\"}``", + StringComparison.InvariantCultureIgnoreCase)) + { + LogWriteLine($"[DBHandler::Init] Error when connecting to database system! Token invalid!\r\n{e}", + LogType.Error, true); + var ex = new AggregateException("Unauthorized: wrong token inserted", e); + throw ex; + } + catch (Exception e) + { + LogWriteLine($"[DBHandler::Init] Error when (re)initializing database system!\r\n{e}", LogType.Error, true); + throw; + } + } + + private static void Dispose() + { + _database = null; + _token = null; + _uri = null; + _userId = null; + _userIdHash = null; + } + + public static async Task QueryKey(string key, bool redirectThrow = false) + { + if (!IsEnabled) return null; + #if DEBUG + var r = new Random(); + var sId = Math.Abs(r.Next(0, 1000).ToString().GetHashCode()); + LogWriteLine($"[DBHandler::QueryKey][{sId}] Invoked!\r\n\tKey: {key}", LogType.Debug, true); + var t = Stopwatch.StartNew(); + #endif + const int retryCount = 3; + for (var i = 0; i < retryCount; i++) + { + try + { + if (_database == null) await Init(true); + // Get table row for exact key + var rs = + await + _database! + .Execute($"SELECT value FROM \"uid-{_userIdHash}\" WHERE key = ?", key); + if (rs != null) + { + // freaking black magic to convert the column row to the value + var str = + string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString())))); + #if DEBUG + LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug, + true); + #endif + return str; + } + } + catch (LibsqlException ex) when ((ex.Message.Contains("STREAM_EXPIRED") || + ex.Message.Contains("Received an invalid baton")) && + i < retryCount - 1) + { + if (i > 0) + LogWriteLine("[DBHandler::QueryKey] Database stream expired, retrying...", LogType.Error, true); + + await Init(); + } + catch (Exception ex) when (i < retryCount - 1) + { + LogWriteLine($"[DBHandler::QueryKey] Failed when getting value for key {key}! Retrying...\r\n{ex}", + LogType.Error, true); + break; + } + catch (Exception ex) when (!redirectThrow) + { + LogWriteLine($"[DBHandler::QueryKey] Failed when getting value for key {key} after {retryCount} retries! Returning null...\r\n{ex}", + LogType.Error, true); + return null; + } + catch (Exception ex) + { + LogWriteLine($"[DBHandler::QueryKey] Failed when getting value for key {key} after {retryCount} retries! Returning null...\r\n{ex}", + LogType.Error, true); + throw; + } + #if DEBUG + finally + { + t.Stop(); + LogWriteLine($"[DBHandler::QueryKey][{sId}] Operation took {t.ElapsedMilliseconds} ms!", LogType.Debug, true); + } + #endif + } + + return null; + } + + public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false) + { + if (!IsEnabled) return; + #if DEBUG + var t = Stopwatch.StartNew(); + var r = new Random(); + var sId = Math.Abs(r.Next(0, 1000).ToString().GetHashCode()); + LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug, + true); + #endif + const int retryCount = 5; + for (var i = 0; i < retryCount; i++) + { + try + { + if (_database == null) await Init(true); + + // Create key for storing value, if key already exist, just update the value (key column is set to UNIQUE) + var command = $"INSERT INTO \"uid-{_userIdHash}\" (key, value) VALUES (?, ?) " + + $"ON CONFLICT(key) DO UPDATE SET value = ?"; + var parameters = new object[] { key, value, value }; + await _database!.Execute(command, parameters); + break; + } + catch (LibsqlException ex) when ((ex.Message.Contains("STREAM_EXPIRED") || + ex.Message.Contains("Received an invalid baton")) && + i < retryCount - 1) + { + if (i > 0) + LogWriteLine("[DBHandler::StoreKeyValue] Database stream expired, retrying...", LogType.Error, + true); + + await Init(); + } + catch (Exception ex) when (i < retryCount - 1) + { + LogWriteLine($"[DBHandler::StoreKeyValue] Failed when saving value for key {key}! Retrying...\r\n{ex}", + LogType.Error, true); + } + catch (Exception ex) when (!redirectThrow) + { + LogWriteLine($"[DBHandler::StoreKeyValue] Failed when saving value for key {key} after {retryCount} tries!\r\n{ex}", + LogType.Error, true); + } + catch (Exception ex) + { + LogWriteLine($"[DBHandler::StoreKeyValue] Failed when saving value for key {key} after {retryCount} tries!\r\n{ex}", + LogType.Error, true); + throw; + } + #if DEBUG + finally + { + t.Stop(); + LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Operation took {t.ElapsedMilliseconds} ms!", + LogType.Debug, true); + } + #endif + } + } + } +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/FileUtility.cs b/CollapseLauncher/Classes/Helper/FileUtility.cs new file mode 100644 index 000000000..c1283277e --- /dev/null +++ b/CollapseLauncher/Classes/Helper/FileUtility.cs @@ -0,0 +1,113 @@ +using Hi3Helper; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using static Hi3Helper.Logger; + +namespace CollapseLauncher.Helper +{ + public static class FileUtility + { + #nullable enable + /// + /// Get latest file from a directory given a pattern. + /// + /// Path to directory you want to get the file from + /// Pattern of the file you are looking for (e.g. "*.txt" or "file_*.txt). Default: *.* + /// Full path of the new file + public static string? GetLatestFile(string directoryPath, string searchPattern = "*.*") + { + var directoryInfo = new DirectoryInfo(directoryPath); + var latestFile = directoryInfo.GetFiles(searchPattern) + .OrderByDescending(f => f.LastWriteTime) + .FirstOrDefault(); + return latestFile?.FullName; + } + #nullable restore + + /// + /// Wait for a directory to spawn a new file. + /// + /// Directory of a path you want to monitor + /// Time you want to keep waiting for a file to spawn + /// True if a new file is found, and false if it timed out + public static async Task WaitForNewFileAsync(string directoryPath, int timeoutMilliseconds) + { + var tcs = new TaskCompletionSource(); + var cts = new CancellationTokenSource(timeoutMilliseconds); + var watcher = new FileSystemWatcher(directoryPath); + + watcher.Filter = "*.*"; + watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime; + watcher.Created += (_, e) => tcs.TrySetResult(e.FullPath); + watcher.EnableRaisingEvents = true; + + await using (cts.Token.Register(() => tcs.TrySetCanceled())) + { + try + { + await tcs.Task; + return true; + } + catch (TaskCanceledException) + { + return false; + } + finally + { + watcher.Dispose(); + } + } + } + + /// + /// Add a prefix to a filename + /// + /// File you want to rename + /// Prefix being added to the file + /// Overwrite if the target filename+prefix already exist + /// True if success, false if fail or target exist while overwrite is false + public static bool RenameFileWithPrefix(string filePath, string prefix = "-old", bool overwrite = false) + { + try + { + if (File.Exists(filePath)) + { + var filename = Path.GetFileNameWithoutExtension(filePath); + var extension = Path.GetExtension(filePath); + + var directory = Path.GetDirectoryName(filePath); + if (directory == null) + { + throw new NullReferenceException("[FileUtility::RenameFileWithPrefix] Directory is null!"); + } + + var newFilePath = Path.Combine(directory, $"{filename}{prefix}{extension}"); + + if (File.Exists(newFilePath)) + { + if (overwrite) File.Delete(newFilePath); + else + { + LogWriteLine($"[FileUtility::RenameFileWithPrefix] Target file {newFilePath} exist " + + $"while overwrite is disabled!", LogType.Warning, true); + return false; + } + } + + File.Move(filePath, newFilePath); + + return true; + } + } + catch (Exception ex) + { + LogWriteLine($"[FileUtility::RenameFileWithPrefix] Failed to rename file {filePath}!\r\n{ex}", + LogType.Error, true); + } + return false; + } + } +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs b/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs index 70450fd30..20bd248df 100644 --- a/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs +++ b/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs @@ -4,33 +4,39 @@ using System.Diagnostics; using System.Net; using System.Net.Http; +using System.Net.Security; using System.Runtime.InteropServices; #nullable enable namespace CollapseLauncher.Helper { - public class HttpClientBuilder + public class HttpClientBuilder : HttpClientBuilder { - private const int _maxConnectionsDefault = 16; + public HttpClientBuilder() : base() { } + } + + public class HttpClientBuilder where THandler : HttpMessageHandler, new() + { + private const int _maxConnectionsDefault = 32; private const double _httpTimeoutDefault = 90; // in Seconds private bool IsUseProxy { get; set; } = true; private bool IsUseSystemProxy { get; set; } = true; - private bool IsAllowHttpRedirections { get; set; } = false; - private bool IsAllowHttpCookies { get; set; } = false; - private bool IsAllowUntrustedCert { get; set; } = false; + private bool IsAllowHttpRedirections { get; set; } + private bool IsAllowHttpCookies { get; set; } + private bool IsAllowUntrustedCert { get; set; } - private int MaxConnections { get; set; } = _maxConnectionsDefault; + private int MaxConnections { get; set; } = _maxConnectionsDefault; private DecompressionMethods DecompressionMethod { get; set; } = DecompressionMethods.All; - private WebProxy? ExternalProxy { get; set; } - private Version HttpProtocolVersion { get; set; } = HttpVersion.Version30; - private string? HttpUserAgent { get; set; } = GetDefaultUserAgent(); + private WebProxy? ExternalProxy { get; set; } + private Version HttpProtocolVersion { get; set; } = HttpVersion.Version30; + private string? HttpUserAgent { get; set; } = GetDefaultUserAgent(); + private string? HttpAuthHeader { get; set; } private HttpVersionPolicy HttpProtocolVersionPolicy { get; set; } = HttpVersionPolicy.RequestVersionOrLower; - private TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(_httpTimeoutDefault); + private TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(_httpTimeoutDefault); + private Uri? HttpBaseUri { get; set; } - public HttpClientBuilder() { } - - public HttpClientBuilder UseProxy(bool isUseSystemProxy = true) + public HttpClientBuilder UseProxy(bool isUseSystemProxy = true) { IsUseProxy = true; IsUseSystemProxy = isUseSystemProxy; @@ -39,19 +45,18 @@ public HttpClientBuilder UseProxy(bool isUseSystemProxy = true) private static string GetDefaultUserAgent() { - bool isWindows10 = InnerLauncherConfig.m_isWindows11; Version operatingSystemVer = Environment.OSVersion.Version; FileVersionInfo winAppSDKVer = FileVersionInfo.GetVersionInfo("Microsoft.ui.xaml.dll"); return $"Mozilla/5.0 (Windows NT {operatingSystemVer}; Win64; x64) " - + $"{RuntimeInformation.FrameworkDescription.ToString().Replace(' ', '/')} (KHTML, like Gecko) " + + $"{RuntimeInformation.FrameworkDescription.Replace(' ', '/')} (KHTML, like Gecko) " + $"Collapse/{LauncherUpdateHelper.LauncherCurrentVersionString}-{(LauncherConfig.IsPreview ? "Preview" : "Stable")} " + $"WinAppSDK/{winAppSDKVer.ProductVersion}"; } - public HttpClientBuilder UseExternalProxy(string host, string? username = null, string? password = null) + public HttpClientBuilder UseExternalProxy(string host, string? username = null, string? password = null) { - // Try create the Uri + // Try to create the Uri if (!Uri.TryCreate(host, UriKind.Absolute, out Uri? hostUri)) { IsUseProxy = false; @@ -63,7 +68,7 @@ public HttpClientBuilder UseExternalProxy(string host, string? username = null, return UseExternalProxy(hostUri, username, password); } - public HttpClientBuilder UseExternalProxy(Uri hostUri, string? username = null, string? password = null) + public HttpClientBuilder UseExternalProxy(Uri hostUri, string? username = null, string? password = null) { IsUseSystemProxy = false; @@ -77,37 +82,77 @@ public HttpClientBuilder UseExternalProxy(Uri hostUri, string? username = null, return this; } - public HttpClientBuilder SetMaxConnection(int maxConnections = _maxConnectionsDefault) + public HttpClientBuilder UseLauncherConfig(int maxConnections = _maxConnectionsDefault) { + bool lIsUseProxy = LauncherConfig.GetAppConfigValue("IsUseProxy").ToBool(); + bool lIsAllowHttpRedirections = LauncherConfig.GetAppConfigValue("IsAllowHttpRedirections").ToBool(); + bool lIsAllowHttpCookies = LauncherConfig.GetAppConfigValue("IsAllowHttpCookies").ToBool(); + bool lIsAllowUntrustedCert = LauncherConfig.GetAppConfigValue("IsAllowUntrustedCert").ToBool(); + + string? lHttpProxyUrl = LauncherConfig.GetAppConfigValue("HttpProxyUrl").ToString(); + string? lHttpProxyUsername = LauncherConfig.GetAppConfigValue("HttpProxyUsername").ToString(); + string? lHttpProxyPassword = LauncherConfig.GetAppConfigValue("HttpProxyPassword").ToString(); + int lHttpClientConnections = maxConnections; + + double lHttpClientTimeout = LauncherConfig.GetAppConfigValue("HttpClientTimeout").ToDouble(); + + bool isHttpProxyUrlValid = Uri.TryCreate(lHttpProxyUrl, UriKind.Absolute, out Uri? lProxyUri); + + this.UseProxy(); + + if (lIsUseProxy && isHttpProxyUrlValid && lProxyUri != null) + this.UseExternalProxy(lProxyUri, lHttpProxyUsername, lHttpProxyPassword); + + this.AllowUntrustedCert(lIsAllowUntrustedCert); + this.AllowCookies(lIsAllowHttpCookies); + this.AllowRedirections(lIsAllowHttpRedirections); + + this.SetTimeout(lHttpClientTimeout); + this.SetMaxConnection(lHttpClientConnections); + + return this; + } + + public HttpClientBuilder SetMaxConnection(int maxConnections = _maxConnectionsDefault) + { + if (maxConnections < 2) + maxConnections = 2; + MaxConnections = maxConnections; return this; } - public HttpClientBuilder SetAllowedDecompression(DecompressionMethods decompressionMethods = DecompressionMethods.All) + public HttpClientBuilder SetAllowedDecompression(DecompressionMethods decompressionMethods = DecompressionMethods.All) { DecompressionMethod = decompressionMethods; return this; } - public HttpClientBuilder AllowRedirections(bool allowRedirections = true) + public HttpClientBuilder AllowRedirections(bool allowRedirections = true) { IsAllowHttpRedirections = allowRedirections; return this; } - public HttpClientBuilder AllowCookies(bool allowCookies = true) + public HttpClientBuilder SetAuthHeader(string authHeader) + { + if (!string.IsNullOrEmpty(authHeader)) HttpAuthHeader = authHeader; + return this; + } + + public HttpClientBuilder AllowCookies(bool allowCookies = true) { IsAllowHttpCookies = allowCookies; return this; } - public HttpClientBuilder AllowUntrustedCert(bool allowUntrustedCert = false) + public HttpClientBuilder AllowUntrustedCert(bool allowUntrustedCert = false) { IsAllowUntrustedCert = allowUntrustedCert; return this; } - public HttpClientBuilder SetHttpVersion(Version? version = null, HttpVersionPolicy versionPolicy = HttpVersionPolicy.RequestVersionOrLower) + public HttpClientBuilder SetHttpVersion(Version? version = null, HttpVersionPolicy versionPolicy = HttpVersionPolicy.RequestVersionOrLower) { if (version != null) HttpProtocolVersion = version; @@ -116,90 +161,122 @@ public HttpClientBuilder SetHttpVersion(Version? version = null, HttpVersionPoli return this; } - public HttpClientBuilder SetTimeout(double fromSeconds = _httpTimeoutDefault) + public HttpClientBuilder SetTimeout(double fromSeconds = _httpTimeoutDefault) { + if (double.IsNaN(fromSeconds) || double.IsInfinity(fromSeconds)) + fromSeconds = _httpTimeoutDefault; + return SetTimeout(TimeSpan.FromSeconds(fromSeconds)); } - public HttpClientBuilder SetTimeout(TimeSpan? timeout = null) + public HttpClientBuilder SetTimeout(TimeSpan? timeout = null) { timeout ??= TimeSpan.FromSeconds(_httpTimeoutDefault); HttpTimeout = timeout.Value; return this; } - public HttpClientBuilder SetUserAgent(string? userAgent = null) + public HttpClientBuilder SetUserAgent(string? userAgent = null) { HttpUserAgent = userAgent; return this; } - public HttpClient Create() + public HttpClientBuilder SetBaseUrl(string baseUrl) { - // Set the HttpClientHandler - HttpClientHandler handler = new HttpClientHandler() - { - UseProxy = IsUseProxy || IsUseSystemProxy, - MaxConnectionsPerServer = MaxConnections, - AllowAutoRedirect = IsAllowHttpRedirections, - UseCookies = IsAllowHttpCookies, - AutomaticDecompression = DecompressionMethod, - ClientCertificateOptions = ClientCertificateOption.Manual - }; + Uri baseUri = new Uri(baseUrl); + return SetBaseUrl(baseUri); + } - // Toggle for allowing untrusted cert - if (IsAllowUntrustedCert) - handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + public HttpClientBuilder SetBaseUrl(Uri baseUrl) + { + HttpBaseUri = baseUrl; + return this; + } - // Set if the external proxy is set - if (!IsUseSystemProxy && ExternalProxy != null) - handler.Proxy = ExternalProxy; + public HttpClient Create() + { + // Create the instance of the handler + THandler handler = new(); + + // Set the features of each handlers + if (typeof(THandler) == typeof(HttpClientHandler)) + { + // Cast as HttpClientHandler + HttpClientHandler? httpClientHandler = handler as HttpClientHandler; + if (httpClientHandler == null) + throw new InvalidCastException("Cannot cast handler as HttpClientHandler"); + + // Set the properties + httpClientHandler.UseProxy = IsUseProxy || IsUseSystemProxy; + httpClientHandler.MaxConnectionsPerServer = MaxConnections; + httpClientHandler.AllowAutoRedirect = IsAllowHttpRedirections; + httpClientHandler.UseCookies = IsAllowHttpCookies; + httpClientHandler.AutomaticDecompression = DecompressionMethod; + httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual; + + // Toggle for allowing untrusted cert + if (IsAllowUntrustedCert) + httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; + + // Set if the external proxy is set + if (!IsUseSystemProxy && ExternalProxy != null) + httpClientHandler.Proxy = ExternalProxy; + } + else if (typeof(THandler) == typeof(SocketsHttpHandler)) + { + // Cast as SocketsHttpHandler + SocketsHttpHandler? socketsHttpHandler = handler as SocketsHttpHandler; + if (socketsHttpHandler == null) + throw new InvalidCastException("Cannot cast handler as SocketsHttpHandler"); + + // Set the properties + socketsHttpHandler.UseProxy = IsUseProxy || IsUseSystemProxy; + socketsHttpHandler.MaxConnectionsPerServer = MaxConnections; + socketsHttpHandler.AllowAutoRedirect = IsAllowHttpRedirections; + socketsHttpHandler.UseCookies = IsAllowHttpCookies; + socketsHttpHandler.AutomaticDecompression = DecompressionMethod; + socketsHttpHandler.EnableMultipleHttp2Connections = true; + socketsHttpHandler.EnableMultipleHttp3Connections = true; + + // Toggle for allowing untrusted cert + if (IsAllowUntrustedCert) + { + SslClientAuthenticationOptions sslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = delegate { return true; } + }; + socketsHttpHandler.SslOptions = sslOptions; + } + + // Set if the external proxy is set + if (!IsUseSystemProxy && ExternalProxy != null) + socketsHttpHandler.Proxy = ExternalProxy; + } + else + { + throw new InvalidOperationException("Generic must be a member of HttpMessageHandler!"); + } // Create the HttpClient instance - HttpClient client = new HttpClient(handler) + HttpClient client = new HttpClient(handler, false) { Timeout = HttpTimeout, DefaultRequestVersion = HttpProtocolVersion, - DefaultVersionPolicy = HttpProtocolVersionPolicy + DefaultVersionPolicy = HttpProtocolVersionPolicy, + BaseAddress = HttpBaseUri, + MaxResponseContentBufferSize = int.MaxValue }; // Set User-agent if (!string.IsNullOrEmpty(HttpUserAgent)) client.DefaultRequestHeaders.Add("User-Agent", HttpUserAgent); + + // Add Http Auth Header + if (!string.IsNullOrEmpty(HttpAuthHeader)) + client.DefaultRequestHeaders.Add("Authorization", HttpAuthHeader); return client; } - - public HttpClientBuilder UseLauncherConfig(int maxConnections = 16) - { - bool lIsUseProxy = LauncherConfig.GetAppConfigValue("IsUseProxy").ToBool(); - bool lIsAllowHttpRedirections = LauncherConfig.GetAppConfigValue("IsAllowHttpRedirections").ToBool(); - bool lIsAllowHttpCookies = LauncherConfig.GetAppConfigValue("IsAllowHttpCookies").ToBool(); - bool lIsAllowUntrustedCert = LauncherConfig.GetAppConfigValue("IsAllowUntrustedCert").ToBool(); - - string? lHttpProxyUrl = LauncherConfig.GetAppConfigValue("HttpProxyUrl").ToString(); - string? lHttpProxyUsername = LauncherConfig.GetAppConfigValue("HttpProxyUsername").ToString(); - string? lHttpProxyPassword = LauncherConfig.GetAppConfigValue("HttpProxyPassword").ToString(); - int lHttpClientConnections = maxConnections; - - double lHttpClientTimeout = LauncherConfig.GetAppConfigValue("HttpClientTimeout").ToDouble(); - - bool isHttpProxyUrlValid = Uri.TryCreate(lHttpProxyUrl, UriKind.Absolute, out Uri? lProxyUri); - - this.UseProxy(); - - if (lIsUseProxy && isHttpProxyUrlValid && lProxyUri != null) - this.UseExternalProxy(lProxyUri, lHttpProxyUsername, lHttpProxyPassword); - - this.AllowUntrustedCert(lIsAllowUntrustedCert); - this.AllowCookies(lIsAllowHttpCookies); - this.AllowRedirections(lIsAllowHttpRedirections); - - this.SetTimeout(lHttpClientTimeout); - this.SetMaxConnection(lHttpClientConnections); - - return this; - } } -} -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/ILoggerHelper.cs b/CollapseLauncher/Classes/Helper/ILoggerHelper.cs new file mode 100644 index 000000000..68081b990 --- /dev/null +++ b/CollapseLauncher/Classes/Helper/ILoggerHelper.cs @@ -0,0 +1,51 @@ +#if USEVELOPACK +using Hi3Helper; +using Microsoft.Extensions.Logging; +using System; + +namespace CollapseLauncher.Helper +{ +#nullable enable + internal static class ILoggerHelper + { + private static ILogger? Logger; + internal static ILogger CreateCollapseILogger() => Logger ??= new CollapseILoggerWrapper(); + } + + public class CollapseILoggerWrapper : ILogger + { + internal CollapseILoggerWrapper() { } + + public IDisposable BeginScope(TState state) + where TState : notnull => default!; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + LogType logType = logLevel switch + { + LogLevel.Trace => LogType.Debug, + LogLevel.Debug => LogType.Debug, + LogLevel.Information => LogType.Default, + LogLevel.Warning => LogType.Warning, + LogLevel.Error => LogType.Error, + LogLevel.Critical => LogType.Error, + LogLevel.None => LogType.NoTag, + _ => LogType.Default + }; + + bool isWriteToLog = logType switch + { + LogType.Error => true, + LogType.Warning => true, + LogType.Debug => true, + _ => false + }; + + string message = formatter(state, exception); + Logger.LogWriteLine(message, logType, isWriteToLog); + } + } +} +#endif \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs index 365f1204b..1f8b01f2c 100644 --- a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs +++ b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs @@ -3,7 +3,6 @@ using CollapseLauncher.Extension; using CollapseLauncher.Helper.Background; using CommunityToolkit.WinUI.Animations; -using CommunityToolkit.WinUI.Controls; using CommunityToolkit.WinUI.Media; using Hi3Helper; using Hi3Helper.Data; @@ -17,9 +16,11 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Windows.Storage; @@ -28,17 +29,28 @@ using static Hi3Helper.Shared.Region.LauncherConfig; using Orientation = Microsoft.UI.Xaml.Controls.Orientation; +using ImageBlendBrush = Hi3Helper.CommunityToolkit.WinUI.Media.ImageBlendBrush; +using ImageCropper = Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper; +using CropShape = Hi3Helper.CommunityToolkit.WinUI.Controls.CropShape; +using ThumbPlacement = Hi3Helper.CommunityToolkit.WinUI.Controls.ThumbPlacement; +using BitmapFileFormat = Hi3Helper.CommunityToolkit.WinUI.Controls.BitmapFileFormat; + namespace CollapseLauncher.Helper.Image { internal static class ImageLoaderHelper { - internal static Dictionary SupportedImageFormats = + internal static readonly Dictionary SupportedImageFormats = new() { { "All supported formats", string.Join(';', BackgroundMediaUtility.SupportedImageExt.Select(x => $"*{x}")) + ';' + string.Join(';', BackgroundMediaUtility.SupportedMediaPlayerExt.Select(x => $"*{x}")) }, { "Image formats", string.Join(';', BackgroundMediaUtility.SupportedImageExt.Select(x => $"*{x}")) }, { "Video formats", string.Join(';', BackgroundMediaUtility.SupportedMediaPlayerExt.Select(x => $"*{x}")) } }; + internal static readonly Dictionary SupportedStaticImageFormats = new() + { + { "Image formats", string.Join(';', BackgroundMediaUtility.SupportedImageExt.Select(x => $"*{x}")) } + }; + #region Waifu2X private static Waifu2X _waifu2X; private static Waifu2XStatus _cachedStatus = Waifu2XStatus.NotInitialized; @@ -147,7 +159,7 @@ internal static async Task LoadImage(string path, bool isUseImageCro } private static async Task SpawnImageCropperDialog(string filePath, string cachedFilePath, - uint ToWidth, uint ToHeight) + uint toWidth, uint toHeight) { Grid parentGrid = new() { @@ -164,15 +176,19 @@ private static async Task SpawnImageCropperDialog(string filePath, s imageCropper.HorizontalAlignment = HorizontalAlignment.Stretch; imageCropper.VerticalAlignment = VerticalAlignment.Stretch; imageCropper.Opacity = 0; + + // Path of image + Uri overlayImageUri = new Uri(Path.Combine(AppFolder!, @"Assets\Images\ImageCropperOverlay", + GetAppConfigValue("WindowSizeProfile").ToString() == "Small" ? "small.png" : "normal.png")); + // Why not use ImageBrush? // https://github.com/microsoft/microsoft-ui-xaml/issues/7809 imageCropper.Overlay = new ImageBlendBrush() { - Source = new BitmapImage(new Uri(Path.Combine(AppFolder!, @"Assets\Images\ImageCropperOverlay", - GetAppConfigValue("WindowSizeProfile").ToString() == "Small" ? "small.png" : "normal.png"))), Opacity = 0.5, Stretch = Stretch.Fill, Mode = ImageBlendMode.Multiply, + SourceUri = overlayImageUri }; ContentDialogOverlay dialogOverlay = new ContentDialogOverlay(ContentDialogTheme.Informational) @@ -191,18 +207,26 @@ private static async Task SpawnImageCropperDialog(string filePath, s ContentDialogResult dialogResult = await dialogOverlay.QueueAndSpawnDialog(); if (dialogResult == ContentDialogResult.Secondary) return null; - await using (FileStream cachedFileStream = new FileStream(cachedFilePath!, StreamUtility.FileStreamCreateReadWriteOpt)) + try { - dialogOverlay.IsPrimaryButtonEnabled = false; - dialogOverlay.IsSecondaryButtonEnabled = false; - await imageCropper.SaveAsync(cachedFileStream.AsRandomAccessStream()!, BitmapFileFormat.Png); - } + await using (FileStream cachedFileStream = + new FileStream(cachedFilePath!, StreamUtility.FileStreamCreateReadWriteOpt)) + { + dialogOverlay.IsPrimaryButtonEnabled = false; + dialogOverlay.IsSecondaryButtonEnabled = false; + await imageCropper.SaveAsync(cachedFileStream.AsRandomAccessStream()!, BitmapFileFormat.Png); + } - GC.WaitForPendingFinalizers(); - GC.WaitForFullGCComplete(); + GC.WaitForPendingFinalizers(); + GC.WaitForFullGCComplete(); + } + catch (Exception ex) + { + Logger.LogWriteLine($"Exception caught at [ImageLoaderHelper::SpawnImageCropperDialog]\r\n{ex}", LogType.Error, true); + } FileInfo cachedFileInfo = new FileInfo(cachedFilePath); - return await GenerateCachedStream(cachedFileInfo, ToWidth, ToHeight, true); + return await GenerateCachedStream(cachedFileInfo, toWidth, toHeight, true); } private static async void LoadImageCropperDetached(string filePath, ImageCropper imageCropper, @@ -258,15 +282,23 @@ private static async Task GenerateCachedStream(FileInfo InputFileInf if (isFromCropProcess) { string InputFileName = InputFileInfo!.FullName; - InputFileInfo.MoveTo(InputFileInfo.FullName + "_old", true); - FileInfo newCachedFileInfo = new FileInfo(InputFileName); + try + { + InputFileInfo.MoveTo(InputFileInfo.FullName + "_old", true); + FileInfo newCachedFileInfo = new FileInfo(InputFileName); + await using (FileStream newCachedFileStream = newCachedFileInfo.Open(StreamUtility.FileStreamCreateWriteOpt)) + await using (FileStream oldInputFileStream = InputFileInfo.Open(StreamUtility.FileStreamOpenReadOpt)) + await ResizeImageStream(oldInputFileStream, newCachedFileStream, ToWidth, ToHeight); - await using (FileStream newCachedFileStream = newCachedFileInfo.Open(StreamUtility.FileStreamCreateWriteOpt)) - await using (FileStream oldInputFileStream = InputFileInfo.Open(StreamUtility.FileStreamOpenReadOpt)) - await ResizeImageStream(oldInputFileStream, newCachedFileStream, ToWidth, ToHeight); + InputFileInfo.Delete(); - InputFileInfo.Delete(); - return newCachedFileInfo.Open(StreamUtility.FileStreamOpenReadOpt); + return newCachedFileInfo.Open(StreamUtility.FileStreamOpenReadOpt); + } + catch (IOException ex) + { + Logger.LogWriteLine($"[ImageLoaderHelper::GenerateCachedStream] IOException Caught! Opening InputFile instead...\r\n{ex}", LogType.Error, true); + return InputFileInfo.Open(StreamUtility.FileStreamOpenReadOpt); + } } FileInfo cachedFileInfo = GetCacheFileInfo(InputFileInfo!.FullName + InputFileInfo.Length); @@ -274,8 +306,8 @@ private static async Task GenerateCachedStream(FileInfo InputFileInf if (isCachedFileExist) return cachedFileInfo.Open(StreamUtility.FileStreamOpenReadOpt); await using (FileStream cachedFileStream = cachedFileInfo.Create()) - await using (FileStream inputFileStream = InputFileInfo.Open(StreamUtility.FileStreamOpenReadOpt)) - await ResizeImageStream(inputFileStream, cachedFileStream, ToWidth, ToHeight); + await using (FileStream inputFileStream = InputFileInfo.Open(StreamUtility.FileStreamOpenReadOpt)) + await ResizeImageStream(inputFileStream, cachedFileStream, ToWidth, ToHeight); return cachedFileInfo.Open(StreamUtility.FileStreamOpenReadOpt); } @@ -291,32 +323,29 @@ internal static FileInfo GetCacheFileInfo(string filePath) public static async Task ResizeImageStream(Stream input, Stream output, uint ToWidth, uint ToHeight) { - ProcessImageSettings settings = new() - { - Width = (int)ToWidth, - Height = (int)ToHeight, - HybridMode = HybridScaleMode.Off, - Interpolation = InterpolationSettings.CubicSmoother, - Anchor = CropAnchor.Bottom | CropAnchor.Center - }; - settings.TrySetEncoderFormat(ImageMimeTypes.Png); - await Task.Run(() => { + ProcessImageSettings settings = new() + { + Width = (int)ToWidth, + Height = (int)ToHeight, + HybridMode = HybridScaleMode.Off, + Interpolation = InterpolationSettings.CubicSmoother, + Anchor = CropAnchor.Bottom | CropAnchor.Center + }; + settings.TrySetEncoderFormat(ImageMimeTypes.Png); + var imageFileInfo = ImageFileInfo.Load(input!); var frame = imageFileInfo.Frames[0]; input.Position = 0; - if (IsWaifu2XEnabled && (frame.Width < ToWidth || frame.Height < ToHeight)) - { - var pipeline = MagicImageProcessor.BuildPipeline(input, ProcessImageSettings.Default) - .AddTransform(new Waifu2XTransform(_waifu2X)); - MagicImageProcessor.ProcessImage(pipeline.PixelSource!, output!, settings); - pipeline.Dispose(); - } - else - { - MagicImageProcessor.ProcessImage(input!, output!, settings); - } + + bool isUseWaifu2x = IsWaifu2XEnabled && (frame.Width < ToWidth || frame.Height < ToHeight); + using var pipeline = MagicImageProcessor.BuildPipeline(input, isUseWaifu2x ? ProcessImageSettings.Default : settings); + + if (isUseWaifu2x) + pipeline.AddTransform(new Waifu2XTransform(_waifu2X)); + + pipeline.WriteOutput(output); }); } @@ -350,43 +379,102 @@ public static Bitmap Stream2Bitmap(IRandomAccessStream image) return new Bitmap(image.AsStream()!); } - public static async ValueTask DownloadAndEnsureCompleteness(string url, string outputPath, CancellationToken token) + public static async ValueTask DownloadAndEnsureCompleteness(string url, string outputPath, bool checkIsHashable, CancellationToken token) { // Initialize the FileInfo and check if the file exist - FileInfo fI = new FileInfo(outputPath); - bool isFileExist = IsFileCompletelyDownloaded(fI); + FileInfo fileInfo = new FileInfo(outputPath); + bool isFileExist = IsFileCompletelyDownloaded(fileInfo, checkIsHashable); // If the file and the file assumed to exist, then return if (isFileExist) return; // If not, then try download the file - await TryDownloadToCompleteness(url, fI, token); + await TryDownloadToCompleteness(url, fileInfo, token); } - public static bool IsFileCompletelyDownloaded(FileInfo fileInfo) + public static bool IsFileCompletelyDownloaded(FileInfo fileInfo, bool checkIsHashable) { // Get the parent path and file name string outputParentPath = Path.GetDirectoryName(fileInfo.FullName); string outputFileName = Path.GetFileName(fileInfo.FullName); +#nullable enable + // Try get a hash from filename if the checkIsHashable set to true and the file does exist + if (checkIsHashable && fileInfo.Exists && TryGetMd5HashFromFilename(outputFileName, out byte[]? hashFromFilename)) + { + // Open the file and check for the hash + using FileStream fileStream = fileInfo.OpenRead(); + ReadOnlySpan hashByte = MD5.HashData(fileStream); + + // Check if the hash matches then return the completeness + bool isMatch = hashByte.SequenceEqual(hashFromFilename); + return isMatch; + } +#nullable restore + // Try to get the prop file which includes the filename + the suggested size provided // by the network stream if it has been downloaded before - string propFilePath = Directory.EnumerateFiles(outputParentPath, $"{outputFileName}#*", SearchOption.TopDirectoryOnly).FirstOrDefault(); - // Check if the file is found (not null), then try parse the information - if (string.IsNullOrEmpty(propFilePath)) + if (outputParentPath != null) { - return false; - } + string propFilePath = Directory.EnumerateFiles(outputParentPath, $"{outputFileName}#*", SearchOption.TopDirectoryOnly).FirstOrDefault(); + // Check if the file is found (not null), then try parse the information + if (string.IsNullOrEmpty(propFilePath)) + { + return false; + } - // Try split the filename into a segment by # char - string[] propSegment = Path.GetFileName(propFilePath).Split('#'); - // Assign the check if the condition met and set the file existence status - return propSegment.Length >= 2 - && long.TryParse(propSegment[1], null, out long suggestedSize) - && fileInfo.Exists && fileInfo.Length == suggestedSize; + // Try split the filename into a segment by # char + string[] propSegment = Path.GetFileName(propFilePath).Split('#'); + // Assign the check if the condition met and set the file existence status + return propSegment.Length >= 2 + && long.TryParse(propSegment[1], null, out long suggestedSize) + && fileInfo.Exists && fileInfo.Length == suggestedSize; + } // If the prop doesn't exist, then return false to assume that the file doesn't exist + return false; + } + +#nullable enable + private static bool TryGetMd5HashFromFilename([NotNull] string fileName, out byte[]? hash) + { + // Set default value for out + hash = null; + + // If the filename is null, then return false + if (string.IsNullOrEmpty(fileName)) + return false; + + // Assign range and try get the split + Span range = stackalloc Range[4]; + ReadOnlySpan fileNameSpan = fileName.AsSpan(); + int len = fileNameSpan.Split(range, '_', StringSplitOptions.RemoveEmptyEntries); + + // As per format should be "hash_number.ext", check that the range should have + // expected to return 2. If not, then return false as non hashable. + if (len != 2) + return false; + + // Try get the span of the hash + ReadOnlySpan hashSpan = fileNameSpan[range[0]]; + + // If the hashSpan is empty or the length is not even or it's not a MD5 Hex (32 chars), then return false + if (hashSpan.IsEmpty || hashSpan.Length % 2 != 0 || hashSpan.Length != 32) + return false; + + // Try decode hash hex to find out if the string is actually a hex + Span dummy = stackalloc byte[16]; + if (!HexTool.TryHexToBytesUnsafe(hashSpan, dummy)) + return false; + + // Copy hash from stackalloc to output array + hash = new byte[16]; + dummy.CopyTo(hash); + + // Return true as it's a valid MD5 hash + return true; } +#nullable restore public static async void TryDownloadToCompletenessAsync(string url, FileInfo fileInfo, CancellationToken token) => await TryDownloadToCompleteness(url, fileInfo, token); @@ -396,35 +484,49 @@ public static async ValueTask TryDownloadToCompleteness(string url, FileInfo fil byte[] buffer = ArrayPool.Shared.Rent(4 << 10); try { + // Initialize file temporary name + FileInfo fileInfoTemp = new FileInfo(fileInfo.FullName + "_temp"); + long fileLength = 0; + Logger.LogWriteLine($"Start downloading resource from: {url}", LogType.Default, true); + if (fileInfo.Exists) + fileInfo.Delete(); + // Try to get the remote stream and download the file - await using Stream netStream = await FallbackCDNUtil.GetHttpStreamFromResponse(url, token); - await using Stream outStream = fileInfo.Open(new FileStreamOptions() + await using (Stream netStream = await FallbackCDNUtil.GetHttpStreamFromResponse(url, token)) { - Access = FileAccess.Write, - Mode = FileMode.Create, - Share = FileShare.ReadWrite, - Options = FileOptions.Asynchronous - }); - - // Get the file length - long fileLength = netStream.Length; - - // Create the prop file for download completeness checking - string outputParentPath = Path.GetDirectoryName(fileInfo.FullName); - string outputFilename = Path.GetFileName(fileInfo.FullName); - string propFilePath = Path.Combine(outputParentPath, $"{outputFilename}#{netStream.Length}"); - await File.Create(propFilePath).DisposeAsync(); - - // Copy (and download) the remote streams to local - int read; - while ((read = await netStream.ReadAsync(buffer, token)) > 0) - await outStream.WriteAsync(buffer, 0, read, token); + await using (Stream outStream = fileInfoTemp.Create()) + { + // Get the file length + fileLength = netStream.Length; + + // Create the prop file for download completeness checking + string outputParentPath = Path.GetDirectoryName(fileInfoTemp.FullName); + string outputFilename = Path.GetFileName(fileInfoTemp.FullName); + if (outputParentPath != null) + { + string propFilePath = Path.Combine(outputParentPath, $"{outputFilename}#{netStream.Length}"); + await File.Create(propFilePath).DisposeAsync(); + } + + // Copy (and download) the remote streams to local + int read; + while ((read = await netStream.ReadAsync(buffer, token)) > 0) + await outStream.WriteAsync(buffer, 0, read, token); + } + } + + // Move to its original filename + fileInfoTemp.Refresh(); + fileInfoTemp.MoveTo(fileInfo.FullName, true); Logger.LogWriteLine($"Resource download from: {url} has been completed and stored locally into:" + $"\"{fileInfo.FullName}\" with size: {ConverterTool.SummarizeSizeSimple(fileLength)} ({fileLength} bytes)", LogType.Default, true); } + // Ignore cancellation exceptions + catch (TaskCanceledException) { } + catch (OperationCanceledException) { } #if !DEBUG catch (Exception ex) { @@ -448,7 +550,7 @@ public static string GetCachedSprites(string URL, CancellationToken token) Directory.CreateDirectory(AppGameImgCachedFolder); FileInfo fInfo = new FileInfo(cachePath); - if (IsFileCompletelyDownloaded(fInfo)) + if (IsFileCompletelyDownloaded(fInfo, true)) { return cachePath; } @@ -467,7 +569,7 @@ public static async ValueTask GetCachedSpritesAsync(string URL, Cancella Directory.CreateDirectory(AppGameImgCachedFolder); FileInfo fInfo = new FileInfo(cachePath); - if (!IsFileCompletelyDownloaded(fInfo)) + if (!IsFileCompletelyDownloaded(fInfo, true)) { await TryDownloadToCompleteness(URL, fInfo, token); } diff --git a/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs b/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs index 4580dcb15..b32dabfbb 100644 --- a/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs +++ b/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs @@ -1,64 +1,115 @@ using Hi3Helper; +using Hi3Helper.Shared.Region; using System; using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using static CollapseLauncher.Helper.Image.Waifu2X; namespace CollapseLauncher.Helper.Image { - #region ncnn Defines - public static class Ncnn + #region PInvokes + internal static partial class Waifu2XPInvoke { - public const string DllName = "Lib\\waifu2x-ncnn-vulkan"; + private const string DllName = "Lib\\waifu2x-ncnn-vulkan.dll"; + +#nullable enable + private static string? appDirPath; + private static string? waifu2xLibPath; +#nullable restore + + static Waifu2XPInvoke() + { + // Use custom Dll import resolver + NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); + } + + private static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + appDirPath ??= LauncherConfig.AppFolder; - [DllImport(DllName, ExactSpelling = true)] - private static extern int ncnn_get_default_gpu_index(); + if (DllImportSearchPath.AssemblyDirectory != searchPath + && DllImportSearchPath.ApplicationDirectory != searchPath) + { + return LoadInternal(libraryName, assembly, searchPath); + } - [DllImport(DllName, ExactSpelling = true)] - private static extern IntPtr ncnn_get_gpu_name(int gpuId); + waifu2xLibPath ??= Path.Combine(appDirPath, DllName); + return LoadInternal(waifu2xLibPath, assembly, null); - public static int DefaultGpuIndex => ncnn_get_default_gpu_index(); + } - public static string GetGpuName(int gpuId) + private static IntPtr LoadInternal(string path, Assembly assembly, DllImportSearchPath? searchPath) { - return Marshal.PtrToStringUTF8(ncnn_get_gpu_name(gpuId)); + bool isLoadSuccessful = NativeLibrary.TryLoad(path, assembly, null, out IntPtr pResult); + if (!isLoadSuccessful || pResult == IntPtr.Zero) + throw new FileLoadException($"Failed while loading library from this path: {path} with Search Path: {searchPath}\r\nMake sure that the library/.dll is a valid Win32 library and not corrupted!"); + + return pResult; } - } - #endregion - public class Waifu2X : IDisposable - { - public const string DllName = "Lib\\waifu2x-ncnn-vulkan"; + #region ncnn PInvokes + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial int ncnn_get_default_gpu_index(); + + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial IntPtr ncnn_get_gpu_name(int gpuId); + #endregion - #region DllImports - [DllImport(DllName, ExactSpelling = true)] - private static extern IntPtr waifu2x_create(int gpuId = 0, bool ttaMode = false, int numThreads = 0); + #region Waifu2X PInvokes + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial IntPtr waifu2x_create(int gpuId, [MarshalAs(UnmanagedType.Bool)] bool ttaMode, int numThreads); - [DllImport(DllName, ExactSpelling = true)] - private static extern void waifu2x_destroy(IntPtr context); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial void waifu2x_destroy(IntPtr context); - [DllImport(DllName, ExactSpelling = true)] - private static extern unsafe int waifu2x_load(IntPtr context, byte* param, byte* model); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static unsafe partial int waifu2x_load(IntPtr context, byte* param, byte* model); - [DllImport(DllName, ExactSpelling = true)] - private static extern unsafe int waifu2x_process(IntPtr context, int w, int h, int c, byte* inData, byte* outData); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static unsafe partial int waifu2x_process(IntPtr context, int w, int h, int c, byte* inData, byte* outData); - [DllImport(DllName, ExactSpelling = true)] - private static extern unsafe int waifu2x_process_cpu(IntPtr context, int w, int h, int c, byte* inData, byte* outData); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static unsafe partial int waifu2x_process_cpu(IntPtr context, int w, int h, int c, byte* inData, byte* outData); - [DllImport(DllName, ExactSpelling = true)] - private static extern void waifu2x_set_param(IntPtr context, Param param, int value); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial void waifu2x_set_param(IntPtr context, Param param, int value); - [DllImport(DllName, ExactSpelling = true)] - private static extern int waifu2x_get_param(IntPtr context, Param param); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial int waifu2x_get_param(IntPtr context, Param param); - [DllImport(DllName, ExactSpelling = true)] - private static extern Waifu2XStatus waifu2x_self_test(IntPtr context); + [LibraryImport(DllName)] + [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] + internal static partial Waifu2XStatus waifu2x_self_test(IntPtr context); - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + [LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16)] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern long GetPackagesByPackageFamily(string packageFamilyName, ref uint count, [Out] IntPtr packageFullNames, ref uint bufferLength, [Out] IntPtr buffer); + internal static unsafe partial long GetPackagesByPackageFamily(string packageFamilyName, ref uint count, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 1)] out string[] packageFullNames, ref uint bufferLength, void* buffer); #endregion + } + #endregion + #region ncnn Defines + public static class Ncnn + { + public static int DefaultGpuIndex => Waifu2XPInvoke.ncnn_get_default_gpu_index(); + + public static string GetGpuName(int gpuId) => Marshal.PtrToStringUTF8(Waifu2XPInvoke.ncnn_get_gpu_name(gpuId)); + } + #endregion + + public partial class Waifu2X : IDisposable + { #region Enums public enum Param { @@ -84,8 +135,6 @@ public enum Waifu2XStatus #region Properties private IntPtr _context; - private byte[] _paramBuffer; - private byte[] _modelBuffer; private Waifu2XStatus _status; #endregion @@ -100,7 +149,7 @@ public Waifu2X() _status = VulkanTest(); if (_status == Waifu2XStatus.Ok) gpuId = Ncnn.DefaultGpuIndex; - _context = waifu2x_create(gpuId); + _context = Waifu2XPInvoke.waifu2x_create(gpuId, false, 0); Logger.LogWriteLine($"Waifu2X initialized successfully with device: {Ncnn.GetGpuName(gpuId)}", LogType.Default, true); } catch ( DllNotFoundException ) @@ -119,19 +168,20 @@ public void Dispose() { if (_context != 0) { - waifu2x_destroy(_context); + Waifu2XPInvoke.waifu2x_destroy(_context); _context = 0; _status = Waifu2XStatus.NotInitialized; Logger.LogWriteLine("Waifu2X is destroyed!"); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool Load(ReadOnlySpan param, ReadOnlySpan model) { if (_context == 0) throw new NotSupportedException(); - fixed (byte* pParam = param, pModel = model) + fixed (byte* pParam = &MemoryMarshal.GetReference(param), pModel = &MemoryMarshal.GetReference(model)) { - return waifu2x_load(_context, pParam, pModel) == 0 && ProcessTest(); + return Waifu2XPInvoke.waifu2x_load(_context, pParam, pModel) == 0 && ProcessTest(); } } @@ -140,19 +190,10 @@ public bool Load(string paramPath, string modelPath) if (_context == 0) throw new NotSupportedException(); try { - using (var ms = new MemoryStream()) - using (var fs = new FileStream(paramPath, FileMode.Open)) - { - fs.CopyTo(ms); - _paramBuffer = ms.ToArray(); - } - - using (var ms = new MemoryStream()) - using (var fs = new FileStream(modelPath, FileMode.Open)) - { - fs.CopyTo(ms); - _modelBuffer = ms.ToArray(); - } + byte[] paramBuffer = File.ReadAllBytes(paramPath); + byte[] modelBuffer = File.ReadAllBytes(modelPath); + + return Load(paramBuffer, modelBuffer); } catch (IOException) { @@ -160,79 +201,91 @@ public bool Load(string paramPath, string modelPath) Logger.LogWriteLine("Waifu2X model file can not be found. Waifu2X feature will be disabled.", LogType.Error, true); return false; } - - return Load(_paramBuffer, _modelBuffer); } #endregion #region Process Methods - public unsafe int Process(int w, int h, int c, ReadOnlySpan inData, Span outData) - { - if (_context == 0) throw new NotSupportedException(); - fixed (byte* pInData = inData, pOutData = outData) - { - return waifu2x_process(_context, w, h, c, pInData, pOutData); - } - } - - public unsafe int ProcessCpu(int w, int h, int c, ReadOnlySpan inData, Span outData) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe int Process(int w, int h, int c, ReadOnlySpan inData, Span outData) { if (_context == 0) throw new NotSupportedException(); - fixed (byte* pInData = inData, pOutData = outData) + fixed (byte* pInData = &MemoryMarshal.GetReference(inData), pOutData = &MemoryMarshal.GetReference(outData)) { - return waifu2x_process_cpu(_context, w, h, c, pInData, pOutData); + return Waifu2XStatus.CpuMode == Status ? + Waifu2XPInvoke.waifu2x_process_cpu(_context, w, h, c, pInData, pOutData) : + Waifu2XPInvoke.waifu2x_process(_context, w, h, c, pInData, pOutData); } } #endregion #region Parameters + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetParam(Param param, int value) { if (_context == 0) throw new NotSupportedException(); - waifu2x_set_param(_context, param, value); + Waifu2XPInvoke.waifu2x_set_param(_context, param, value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetParam(Param param) { if (_context == 0) throw new NotSupportedException(); - return waifu2x_get_param(_context, param); + return Waifu2XPInvoke.waifu2x_get_param(_context, param); } #endregion #region Misc public static Waifu2XStatus VulkanTest() { - if (CheckD3DMappingLayersPackageInstalled()) + try { - Logger.LogWriteLine("D3DMappingLayers package detected. Fallback to CPU mode.", LogType.Warning, true); - return Waifu2XStatus.D3DMappingLayers; + if (CheckD3DMappingLayersPackageInstalled()) + { + Logger.LogWriteLine("D3DMappingLayers package detected. Fallback to CPU mode.", LogType.Warning, true); + return Waifu2XStatus.D3DMappingLayers; + } + var status = Waifu2XPInvoke.waifu2x_self_test(0); + switch (status) + { + case Waifu2XStatus.CpuMode: + Logger.LogWriteLine("No available Vulkan GPU device was found and CPU mode will be used. This will greatly increase image processing time.", LogType.Warning, true); + break; + case Waifu2XStatus.NotAvailable: + Logger.LogWriteLine("An error occurred while initializing Vulkan. Fallback to CPU mode.", LogType.Warning, true); + status = Waifu2XStatus.CpuMode; + break; + case Waifu2XStatus.Ok: + Logger.LogWriteLine("Vulkan test passes and GPU mode can be used.", LogType.Default, true); + break; + default: + Logger.LogWriteLine("Waifu2X: Unknown return value from waifu2x_self_test.", LogType.Error, true); + status = Waifu2XStatus.NotAvailable; + break; + } + return status; } - var status = waifu2x_self_test(0); - switch (status) + catch (FileLoadException ex) { - case Waifu2XStatus.CpuMode: - Logger.LogWriteLine("No available Vulkan GPU device was found and CPU mode will be used. This will greatly increase image processing time.", LogType.Warning, true); - break; - case Waifu2XStatus.NotAvailable: - Logger.LogWriteLine("An error occurred while initializing Vulkan. Fallback to CPU mode.", LogType.Warning, true); - status = Waifu2XStatus.CpuMode; - break; - case Waifu2XStatus.Ok: - Logger.LogWriteLine("Vulkan test passes and GPU mode can be used.", LogType.Default, true); - break; - default: - Logger.LogWriteLine("Waifu2X: Unknown return value from waifu2x_self_test.", LogType.Error, true); - status = Waifu2XStatus.NotAvailable; - break; + return ReturnAsFailedDllInit(ex); + } + catch (DllNotFoundException ex) + { + return ReturnAsFailedDllInit(ex); + } + + Waifu2XStatus ReturnAsFailedDllInit(T ex) + where T : Exception + { + Logger.LogWriteLine($"Cannot load Waifu2X as the library failed to load!\r\n{ex}", LogType.Error, true); + return Waifu2XStatus.Error; } - return status; } private bool ProcessTest() { if (Status < Waifu2XStatus.Error) { - _status = waifu2x_self_test(_context); + _status = Waifu2XPInvoke.waifu2x_self_test(_context); switch (_status) { case Waifu2XStatus.TestNotPassed: @@ -252,12 +305,13 @@ private bool ProcessTest() } return _status == Waifu2XStatus.Ok; } - - private static bool CheckD3DMappingLayersPackageInstalled() + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe bool CheckD3DMappingLayersPackageInstalled() { const string FAMILY_NAME = "Microsoft.D3DMappingLayers_8wekyb3d8bbwe"; uint count = 0, bufferLength = 0; - GetPackagesByPackageFamily(FAMILY_NAME, ref count, 0, ref bufferLength, 0); + Waifu2XPInvoke.GetPackagesByPackageFamily(FAMILY_NAME, ref count, out _, ref bufferLength, null); return count != 0; } #endregion diff --git a/CollapseLauncher/Classes/Helper/JsonConverter/RegionResourcePluginValidateConverter.cs b/CollapseLauncher/Classes/Helper/JsonConverter/RegionResourcePluginValidateConverter.cs index a1f48a9c2..334ff9b04 100644 --- a/CollapseLauncher/Classes/Helper/JsonConverter/RegionResourcePluginValidateConverter.cs +++ b/CollapseLauncher/Classes/Helper/JsonConverter/RegionResourcePluginValidateConverter.cs @@ -19,7 +19,7 @@ public override List Read( JsonSerializerOptions options) { string valueString = EmptiedBackslash(reader.ValueSpan); - List returnList = valueString.Deserialize>(InternalAppJSONContext.Default); + List returnList = valueString.Deserialize(InternalAppJSONContext.Default.ListRegionResourcePluginValidate); return returnList; } diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs index ae917ff1e..a2f22b1c9 100644 --- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs +++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs @@ -28,31 +28,41 @@ protected override async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onT ActionTimeoutValueTaskCallback hypResourceResponseCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken); - HoYoPlayLauncherResources? hypResourceResponse = await hypResourceResponseCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); + // Assign as 3 Task array + Task[] tasks = [ + Task.CompletedTask, + Task.CompletedTask, + Task.CompletedTask + ]; + // Init as null first before being assigned when the backing task is called + HoYoPlayLauncherResources? hypResourceResponse = null; HoYoPlayLauncherResources? hypPluginResource = null; HoYoPlayLauncherResources? hypSdkResource = null; + + tasks[0] = hypResourceResponseCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypResourceResponse = result); + if (!string.IsNullOrEmpty(PresetConfig?.LauncherPluginURL) && (PresetConfig.IsPluginUpdateEnabled ?? false)) { ActionTimeoutValueTaskCallback hypPluginResourceCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherPluginURL, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherPluginURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken); - hypPluginResource = await hypPluginResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); + tasks[1] = hypPluginResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypPluginResource = result); } if (!string.IsNullOrEmpty(PresetConfig?.LauncherGameChannelSDKURL)) { ActionTimeoutValueTaskCallback hypSdkResourceCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherGameChannelSDKURL, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherGameChannelSDKURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken); - hypSdkResource = await hypSdkResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); + tasks[2] = hypSdkResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypSdkResource = result); } RegionResourceLatest sophonResourceCurrentPackage = new RegionResourceLatest(); @@ -67,6 +77,9 @@ protected override async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onT data = sophonResourceData }; + // Await all callbacks + await Task.WhenAll(tasks).ConfigureAwait(false); + ConvertPluginResources(ref sophonResourceData, hypPluginResource); ConvertSdkResources(ref sophonResourceData, hypSdkResource); ConvertPackageResources(sophonResourceData, hypResourceResponse?.Data?.LauncherPackages); @@ -309,7 +322,7 @@ private void ConvertMultiPackageResource(ref RegionResourceVersion sophonPackage } #endregion - protected override async ValueTask LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRoutine, CancellationToken token) + protected override async Task LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRoutine, CancellationToken token) { bool isUseMultiLang = PresetConfig?.LauncherSpriteURLMultiLang ?? false; @@ -317,18 +330,28 @@ protected override async ValueTask LoadLauncherNews(ActionOnTimeOutRetry? onTime string launcherSpriteUrl = string.Format(PresetConfig?.LauncherSpriteURL!, localeCode); string launcherNewsUrl = string.Format(PresetConfig?.LauncherNewsURL!, localeCode); + ActionTimeoutValueTaskCallback hypLauncherBackgroundCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(launcherSpriteUrl, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(launcherSpriteUrl, InternalAppJSONContext.Default.HoYoPlayLauncherNews, innerToken); ActionTimeoutValueTaskCallback hypLauncherNewsCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(launcherNewsUrl, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(launcherNewsUrl, InternalAppJSONContext.Default.HoYoPlayLauncherNews, innerToken); - HoYoPlayLauncherNews? hypLauncherBackground = await hypLauncherBackgroundCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); - HoYoPlayLauncherNews? hypLauncherNews = await hypLauncherNewsCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); + HoYoPlayLauncherNews? hypLauncherBackground = null; + HoYoPlayLauncherNews? hypLauncherNews = null; + + // Load both in parallel + Task[] tasks = [ + hypLauncherBackgroundCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypLauncherBackground = result), + hypLauncherNewsCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypLauncherNews = result) + ]; + + // Await the result + await Task.WhenAll(tasks); // Merge background image if (hypLauncherBackground?.Data?.GameInfoList != null && hypLauncherNews?.Data != null) @@ -447,7 +470,7 @@ private void ConvertLauncherSocialMedia(ref LauncherGameNews? sophonLauncherNews } #endregion - protected override async ValueTask LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeoutRoutine, CancellationToken token) + protected override async Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeoutRoutine, CancellationToken token) { if (PresetConfig?.LauncherGameInfoDisplayURL == null) { @@ -462,7 +485,7 @@ protected override async ValueTask LoadLauncherGameInfo(ActionOnTimeOutRetry? on ActionTimeoutValueTaskCallback hypLauncherGameInfoCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(launcherGameInfoUrl, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(launcherGameInfoUrl, InternalAppJSONContext.Default.HoYoPlayLauncherGameInfo, innerToken); HoYoPlayLauncherGameInfo? hypLauncherGameInfo = await hypLauncherGameInfoCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, ExecutionTimeoutAttempt, onTimeoutRoutine, token); diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs index 8ef1d36f5..1514262a5 100644 --- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs +++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs @@ -73,12 +73,14 @@ public async Task LoadAsync(OnLoadAction? beforeLoadRoutine, OnLoa } } - protected virtual async ValueTask LoadAsyncInner(ActionOnTimeOutRetry? onTimeoutRoutine, + protected virtual async Task LoadAsyncInner(ActionOnTimeOutRetry? onTimeoutRoutine, CancellationToken token) { - await LoadLauncherGameResource(onTimeoutRoutine, token); - await LoadLauncherNews(onTimeoutRoutine, token); - await LoadLauncherGameInfo(onTimeoutRoutine, token); + await Task.WhenAll([ + LoadLauncherGameResource(onTimeoutRoutine, token), + LoadLauncherNews(onTimeoutRoutine, token), + LoadLauncherGameInfo(onTimeoutRoutine, token) + ]).ConfigureAwait(false); } protected virtual async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onTimeoutRoutine, @@ -89,33 +91,41 @@ protected virtual async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onTi ActionTimeoutValueTaskCallback launcherGameResourceCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default.RegionResourceProp, innerToken); - LauncherGameResource = await launcherGameResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); - - if (LauncherGameResource == null) - { - throw new NullReferenceException("Launcher game resource returns a null!"); - } + Task[] tasks = [ + launcherGameResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token) + .AsTaskAndDoAction((result) => LauncherGameResource = result), + Task.CompletedTask + ]; + RegionResourceProp? pluginProp = null; if (string.IsNullOrEmpty(PresetConfig?.LauncherPluginURL)) { ActionTimeoutValueTaskCallback launcherPluginPropCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(string.Format(PresetConfig?.LauncherPluginURL!, GetDeviceId(PresetConfig!)), InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(string.Format(PresetConfig?.LauncherPluginURL!, GetDeviceId(PresetConfig!)), InternalAppJSONContext.Default.RegionResourceProp, innerToken); - RegionResourceProp? pluginProp = await launcherPluginPropCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, - ExecutionTimeoutAttempt, onTimeoutRoutine, token); + tasks[1] = launcherPluginPropCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep, + ExecutionTimeoutAttempt, onTimeoutRoutine, token) + .AsTaskAndDoAction((result) => pluginProp = result); + } - if (pluginProp != null && LauncherGameResource.data != null) - { - LauncherGameResource.data.plugins = pluginProp.data?.plugins; - #if DEBUG - Logger.LogWriteLine("[LauncherApiBase::LoadLauncherGameResource] Loading plugin handle!", - LogType.Debug, true); - #endif - } + await Task.WhenAll(tasks).ConfigureAwait(false); + + if (LauncherGameResource == null) + { + throw new NullReferenceException("Launcher game resource returns a null!"); + } + + if (pluginProp != null && LauncherGameResource.data != null) + { + LauncherGameResource.data.plugins = pluginProp.data?.plugins; +#if DEBUG + Logger.LogWriteLine("[LauncherApiBase::LoadLauncherGameResource] Loading plugin handle!", + LogType.Debug, true); +#endif } PerformDebugRoutines(); @@ -207,8 +217,8 @@ protected void EnsureResourceUrlNotNull() } } - protected virtual async ValueTask LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRoutine, - CancellationToken token) + protected virtual async Task LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRoutine, + CancellationToken token) { EnsurePresetConfigNotNull(); @@ -252,9 +262,9 @@ protected virtual async ValueTask LoadLauncherNews(ActionOnTimeOutRetry? onTimeo #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected virtual async ValueTask LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeoutRoutine, + protected virtual async Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeoutRoutine, #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - CancellationToken token) + CancellationToken token) { LauncherGameInfoField = new HoYoPlayGameInfoField(); } @@ -282,8 +292,7 @@ protected virtual async ValueTask LoadLauncherGameInfo(ActionOnTimeOutRetry? onT private static async Task LoadSingleLangLauncherNews( string launcherSpriteUrl, CancellationToken token) { - return await FallbackCDNUtil.DownloadAsJSONType(launcherSpriteUrl, - InternalAppJSONContext.Default, token); + return await FallbackCDNUtil.DownloadAsJSONType(launcherSpriteUrl, InternalAppJSONContext.Default.LauncherGameNews, token); } private static async Task LoadMultiLangLauncherNews(string launcherSpriteUrl, string lang, @@ -291,8 +300,7 @@ protected virtual async ValueTask LoadLauncherGameInfo(ActionOnTimeOutRetry? onT { return await FallbackCDNUtil - .DownloadAsJSONType(string.Format(launcherSpriteUrl, lang), - InternalAppJSONContext.Default, token); + .DownloadAsJSONType(string.Format(launcherSpriteUrl, lang), InternalAppJSONContext.Default.LauncherGameNews, token); } protected virtual string GetDeviceId(PresetConfig preset) diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs index 00496cc1c..59ac9600b 100644 --- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs +++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; +using WinRT; +// ReSharper disable PartialTypeWithSinglePart namespace CollapseLauncher.Helper.LauncherApiLoader.Sophon { @@ -83,7 +85,7 @@ public List? NewsPostTypeInfo return _newsPostTypeInfo; } - _newsPostTypeInfo = NewsPost.Where(x => x.PostType == LauncherGameNewsPostType.POST_TYPE_INFO)? + _newsPostTypeInfo = NewsPost.Where(x => x.PostType == LauncherGameNewsPostType.POST_TYPE_INFO) .OrderBy(x => x.PostOrder) .ToList(); return _newsPostTypeInfo; @@ -105,7 +107,7 @@ public List? NewsPostTypeActivity return _newsPostTypeActivity; } - _newsPostTypeActivity = NewsPost.Where(x => x.PostType == LauncherGameNewsPostType.POST_TYPE_ACTIVITY)? + _newsPostTypeActivity = NewsPost.Where(x => x.PostType == LauncherGameNewsPostType.POST_TYPE_ACTIVITY) .OrderBy(x => x.PostOrder) .ToList(); return _newsPostTypeActivity; @@ -128,7 +130,7 @@ public List? NewsPostTypeAnnouncement } _newsPostTypeAnnouncement = NewsPost - .Where(x => x.PostType == LauncherGameNewsPostType.POST_TYPE_ANNOUNCE)? + .Where(x => x.PostType == LauncherGameNewsPostType.POST_TYPE_ANNOUNCE) .OrderBy(x => x.PostOrder) .ToList(); return _newsPostTypeAnnouncement; @@ -220,7 +222,8 @@ public string? CarouselImg public int? CarouselOrder { get; init; } } - public class LauncherGameNewsSocialMedia : ILauncherGameNewsDataTokenized + [GeneratedBindableCustomProperty] + public partial class LauncherGameNewsSocialMedia : ILauncherGameNewsDataTokenized { private readonly string? _qrImg; private readonly List? _qrLinks; @@ -303,7 +306,8 @@ public List? QrLinks [JsonIgnore] public bool IsHasQrDescription => !string.IsNullOrEmpty(QrTitle); } - public class LauncherGameNewsSocialMediaQrLinks + [GeneratedBindableCustomProperty] + public partial class LauncherGameNewsSocialMediaQrLinks { [JsonPropertyName("title")] [JsonConverter(typeof(EmptyStringAsNullConverter))] diff --git a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs index 6a9c49256..e97fdb341 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs @@ -2,17 +2,21 @@ using System; using System.Buffers; using System.Buffers.Text; +using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; +using ZstdDecompressStream = ZstdNet.DecompressionStream; + namespace CollapseLauncher.Helper.Metadata { internal enum CompressionType : byte { None, - Brotli + Brotli, + Zstd } internal static class DataCooker @@ -150,27 +154,10 @@ internal static void ServeV3Data(ReadOnlySpan data, Span dataWritten = decompressedSize; break; case CompressionType.Brotli: - { - Span dataDecompressed = outData; - BrotliDecoder decoder = new BrotliDecoder(); - - int offset = 0; - int decompressedWritten = 0; - while (offset < compressedSize) - { - decoder.Decompress(dataRawBuffer.Slice(offset), dataDecompressed.Slice(decompressedWritten), - out int dataConsumedWritten, out int dataDecodedWritten); - decompressedWritten += dataDecodedWritten; - offset += dataConsumedWritten; - } - - if (decompressedSize != decompressedWritten) - { - throw new DataMisalignedException("Decompressed data is misaligned!"); - } - - dataWritten = decompressedWritten; - } + dataWritten = DecompressDataFromBrotli(outData, compressedSize, decompressedSize, dataRawBuffer); + break; + case CompressionType.Zstd: + dataWritten = DecompressDataFromZstd(outData, compressedSize, decompressedSize, dataRawBuffer); break; default: throw new FormatException($"Decompression format is not supported! ({compressionType})"); @@ -189,5 +176,56 @@ internal static void ServeV3Data(ReadOnlySpan data, Span } } } + + private static int DecompressDataFromBrotli(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) + { + BrotliDecoder decoder = new BrotliDecoder(); + + int offset = 0; + int decompressedWritten = 0; + while (offset < compressedSize) + { + decoder.Decompress(dataRawBuffer.Slice(offset), outData.Slice(decompressedWritten), + out int dataConsumedWritten, out int dataDecodedWritten); + decompressedWritten += dataDecodedWritten; + offset += dataConsumedWritten; + } + + if (decompressedSize != decompressedWritten) + { + throw new DataMisalignedException("Decompressed data is misaligned!"); + } + + return decompressedWritten; + } + + private static unsafe int DecompressDataFromZstd(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) + { + fixed (byte* inputBuffer = &dataRawBuffer[0]) + fixed (byte* outputBuffer = &outData[0]) + { + int decompressedWritten = 0; + + byte[] buffer = new byte[4 << 10]; + + using UnmanagedMemoryStream inputStream = new UnmanagedMemoryStream(inputBuffer, dataRawBuffer.Length); + using UnmanagedMemoryStream outputStream = new UnmanagedMemoryStream(outputBuffer, outData.Length); + using ZstdDecompressStream decompStream = new ZstdDecompressStream(inputStream); + + int read; + while ((read = decompStream.Read(buffer)) > 0) + { + outputStream.Write(buffer, 0, read); + decompressedWritten += read; + } + + if (decompressedSize != decompressedWritten) + { + throw new DataMisalignedException("Decompressed data is misaligned!"); + } + + return decompressedWritten; + } + } } } \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/Metadata/LauncherMetadataHelper.cs b/CollapseLauncher/Classes/Helper/Metadata/LauncherMetadataHelper.cs index d3432a779..1096aaa53 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/LauncherMetadataHelper.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/LauncherMetadataHelper.cs @@ -1,5 +1,4 @@ #nullable enable -using CollapseLauncher.Helper.LauncherApiLoader; using CollapseLauncher.Helper.LauncherApiLoader.HoYoPlay; using CollapseLauncher.Helper.LauncherApiLoader.Sophon; using CollapseLauncher.Helper.Loading; @@ -33,8 +32,8 @@ internal static class LauncherMetadataHelper #region Metadata Stamp List and Config Dictionary - internal static List? LauncherMetadataStamp { get; private set; } - internal static List? NewUpdateMetadataStamp { get; private set; } + internal static List? LauncherMetadataStamp { get; private set; } + internal static List? NewUpdateMetadataStamp { get; private set; } internal static List? LauncherGameNameCollection => LauncherGameNameRegionCollection?.Keys.ToList(); private static Dictionary? LauncherMetadataStampDictionary { get; set; } @@ -140,6 +139,18 @@ internal static PresetConfig? CurrentMetadataConfig throw new AccessViolationException($"Config is not exist or null inside of the metadata! This should not be happening!\r\nGame: ({gameName} - {gameRegion})"); } + internal static string GetTranslatedCurrentGameTitleRegionString() + { + string? curGameName = CurrentMetadataConfigGameName; + string? curGameRegion = CurrentMetadataConfigGameRegion; + + string? curGameNameTranslate = + InnerLauncherConfig.GetGameTitleRegionTranslationString(curGameName, Locale.Lang._GameClientTitles); + string? curGameRegionTranslate = + InnerLauncherConfig.GetGameTitleRegionTranslationString(curGameRegion, Locale.Lang._GameClientRegions); + + return $"{curGameNameTranslate} - {curGameRegionTranslate}"; + } /// /// Checks for the local modification of the metadata config @@ -237,7 +248,7 @@ internal static async ValueTask InitializeStamp(string currentChannel, bool thro // Deserialize the stream LauncherMetadataStamp = - await stampLocalStream.DeserializeAsync>(InternalAppJSONContext.Default); + await stampLocalStream.DeserializeAsListAsync(InternalAppJSONContext.Default.Stamp); // SANITIZE: Check if the stamp is empty, then throw if (LauncherMetadataStamp == null || LauncherMetadataStamp.Count == 0) @@ -246,6 +257,9 @@ internal static async ValueTask InitializeStamp(string currentChannel, bool thro // Load and add stamp into stamp dictionary foreach (Stamp? stamp in LauncherMetadataStamp) { + if (stamp == null) + continue; + string? gameName = string.IsNullOrEmpty(stamp.GameName) ? stamp.MetadataType.ToString() : stamp.GameName; string? gameRegion = string.IsNullOrEmpty(stamp.GameRegion) ? stamp.MetadataPath : stamp.GameRegion; LauncherMetadataStampDictionary?.Add($"{gameName} - {gameRegion}", stamp); @@ -312,7 +326,7 @@ internal static async ValueTask InitializeConfig(string currentChannel, bool isC // Find and iterate the master key first Stamp? masterKeyStamp = - LauncherMetadataStamp.FirstOrDefault(x => x.MetadataType == MetadataType.MasterKey); + LauncherMetadataStamp.FirstOrDefault(x => x?.MetadataType == MetadataType.MasterKey); if (masterKeyStamp == null) { throw new KeyNotFoundException("Master key information is not found in the stamp!"); @@ -321,7 +335,7 @@ internal static async ValueTask InitializeConfig(string currentChannel, bool isC // Iterate the CommunityTools configs Stamp? stampCommunityToolkit = LauncherMetadataStamp - .FirstOrDefault(x => x.MetadataType == MetadataType.CommunityTools); + .FirstOrDefault(x => x?.MetadataType == MetadataType.CommunityTools); if (stampCommunityToolkit != null) { await LoadConfigInner(stampCommunityToolkit, currentChannel, false, false); @@ -329,11 +343,14 @@ internal static async ValueTask InitializeConfig(string currentChannel, bool isC // Iterate the stamp and try to load the configs int index = 1; - List stampList = LauncherMetadataStamp - .Where(x => x.MetadataType == MetadataType.PresetConfigV2) + List stampList = LauncherMetadataStamp + .Where(x => x?.MetadataType == MetadataType.PresetConfigV2) .ToList(); - foreach (Stamp stamp in stampList) + foreach (Stamp? stamp in stampList) { + if (stamp == null) + continue; + if (isShowLoadingMessage) { LoadingMessageHelper.SetMessage(Locale.Lang._MainPage.Initializing, @@ -392,7 +409,7 @@ internal static async ValueTask LoadConfigInner(Stamp stamp, string currentChann { // Deserialize the key config MasterKeyConfig? keyConfig = - await configLocalStream.DeserializeAsync(InternalAppJSONContext.Default); + await configLocalStream.DeserializeAsync(InternalAppJSONContext.Default.MasterKeyConfig); // Assign the key to instance property CurrentMasterKey = keyConfig ?? throw new InvalidDataException("Master key config seems to be empty!"); @@ -409,7 +426,7 @@ internal static async ValueTask LoadConfigInner(Stamp stamp, string currentChann case MetadataType.PresetConfigV2: { PresetConfig? presetConfig = - await configLocalStream.DeserializeAsync(InternalAppJSONContext.Default); + await configLocalStream.DeserializeAsync(InternalAppJSONContext.Default.PresetConfig); if (presetConfig != null) { if (isCacheUpdateModeOnly && (!presetConfig.IsCacheUpdateEnabled ?? false)) return; @@ -514,8 +531,8 @@ internal static async ValueTask IsMetadataHasUpdate() // Check and throw if the stream returns null or empty if (stampRemoteStream != null) { - List? remoteMetadataStampList = - await stampRemoteStream.DeserializeAsync>(InternalAppJSONContext.Default); + List? remoteMetadataStampList = + await stampRemoteStream.DeserializeAsListAsync(InternalAppJSONContext.Default.Stamp); // Check and throw if the metadata stamp returns null or empty if (remoteMetadataStampList == null || remoteMetadataStampList.Count == 0) @@ -533,18 +550,21 @@ internal static async ValueTask IsMetadataHasUpdate() bool isOutdatedStampDetected = false; foreach (Stamp? remoteMetadataStamp in remoteMetadataStampList) { + if (remoteMetadataStamp == null) + continue; + // Check if the local stamp does not have one, then add it to new update stamp list Stamp? localStamp = LauncherMetadataStamp?.FirstOrDefault(x => remoteMetadataStamp.GameRegion == - x.GameRegion + x?.GameRegion && remoteMetadataStamp.GameName == - x.GameName + x?.GameName && remoteMetadataStamp.LastUpdated == - x.LastUpdated + x?.LastUpdated && remoteMetadataStamp.MetadataPath == - x.MetadataPath + x?.MetadataPath && remoteMetadataStamp.MetadataType == - x.MetadataType); + x?.MetadataType); if (localStamp != null) continue; @@ -596,8 +616,10 @@ internal static async ValueTask RunMetadataUpdate() } // Remove the old metadata config file first - foreach (Stamp newUpdateStamp in NewUpdateMetadataStamp) + foreach (Stamp? newUpdateStamp in NewUpdateMetadataStamp) { + if (newUpdateStamp == null) continue; + // Ensure if the MetadataPath is not empty if (string.IsNullOrEmpty(newUpdateStamp.MetadataPath)) { @@ -632,7 +654,7 @@ internal static async ValueTask RunMetadataUpdate() } } - private static async ValueTask UpdateStampContent(string stampPath, List newStampList) + private static async ValueTask UpdateStampContent(string stampPath, List newStampList) { try { @@ -641,26 +663,28 @@ private static async ValueTask UpdateStampContent(string stampPath, List throw new FileNotFoundException($"Unable to update the stamp file because it is not exist! It should have been located here: {stampPath}"); // Read the old stamp list stream - List? oldStampList = null; + List? oldStampList = null; using (FileStream stampStream = File.OpenRead(stampPath)) { // Deserialize and do sanitize if the old stamp list is empty - oldStampList = await stampStream.DeserializeAsync>(InternalAppJSONContext.Default); + oldStampList = await stampStream.DeserializeAsListAsync(InternalAppJSONContext.Default.Stamp); if (oldStampList == null || oldStampList?.Count == 0) throw new NullReferenceException($"The old stamp list contains an empty/null content!"); // Try iterate the new stamp list to replace the old ones or add a new entry - foreach (Stamp newStamp in newStampList) + foreach (Stamp? newStamp in newStampList) { + if (newStamp == null) continue; + // Find the old stamp reference from the old list Stamp? oldStampRef = oldStampList?.FirstOrDefault(x => newStamp.GameRegion == - x.GameRegion + x?.GameRegion && newStamp.GameName == - x.GameName + x?.GameName && newStamp.MetadataPath == - x.MetadataPath + x?.MetadataPath && newStamp.MetadataType == - x.MetadataType); + x?.MetadataType); // Check if the old stamp ref is null or index of old stamp reference returns < 0, then // add it as a new entry. int indexOfOldStamp = 0; @@ -675,7 +699,7 @@ private static async ValueTask UpdateStampContent(string stampPath, List // Now write the updated list to the stamp file using (FileStream updatedStampStream = File.Create(stampPath)) { - await oldStampList.SerializeAsync(updatedStampStream, InternalAppJSONContext.Default); + await oldStampList.SerializeAsync(updatedStampStream, InternalAppJSONContext.Default.ListStamp); return; } } diff --git a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs index 06e6e359c..486d81108 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs @@ -94,7 +94,7 @@ public async ValueTask EnsureReassociated(HttpClient client, string? branchUrl, // Fetch branch info ActionTimeoutValueTaskCallback hypLauncherBranchCallback = async innerToken => - await FallbackCDNUtil.DownloadAsJSONType(branchUrl, InternalAppJSONContext.Default, innerToken); + await FallbackCDNUtil.DownloadAsJSONType(branchUrl, InternalAppJSONContext.Default.HoYoPlayLauncherGameInfo, innerToken); HoYoPlayLauncherGameInfo? hypLauncherBranchInfo = await hypLauncherBranchCallback.WaitForRetryAsync( LauncherApiBase.ExecutionTimeout, @@ -135,7 +135,7 @@ public async ValueTask EnsureReassociated(HttpClient client, string? branchUrl, string packageIdValue = branch.GamePreloadField.PackageId; string passwordValue = branch.GamePreloadField.Password; - PreloadUrl = MainUrl.AssociateGameAndLauncherId( + PreloadUrl = PreloadUrl.AssociateGameAndLauncherId( QueryPasswordHead, QueryPackageIdHead, passwordValue, @@ -425,8 +425,8 @@ public SophonChunkUrls? LauncherResourceChunksURL public bool? IsHideSocMedDesc { get; init; } = true; #if !DEBUG - public bool? IsRepairEnabled { get; init; } - public bool? IsCacheUpdateEnabled { get; init; } + public bool? IsRepairEnabled { get; set; } + public bool? IsCacheUpdateEnabled { get; set; } #else public bool? IsRepairEnabled = true; public bool? IsCacheUpdateEnabled = true; diff --git a/CollapseLauncher/Classes/Helper/PatternMatcher.cs b/CollapseLauncher/Classes/Helper/PatternMatcher.cs index 354432479..5078c0935 100644 --- a/CollapseLauncher/Classes/Helper/PatternMatcher.cs +++ b/CollapseLauncher/Classes/Helper/PatternMatcher.cs @@ -19,7 +19,7 @@ public static bool MatchSimpleExpression(string input, string pattern) return Regex.IsMatch(input, regexPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.NonBacktracking); } - + /// /// Determines whether the specified input string matches any pattern in the given list of patterns. /// @@ -35,6 +35,7 @@ public static bool MatchesAnyPattern(string input, List patterns) return true; } } + return false; } } diff --git a/CollapseLauncher/Classes/Helper/SimpleProtectData.cs b/CollapseLauncher/Classes/Helper/SimpleProtectData.cs index 739c29cee..923f68a08 100644 --- a/CollapseLauncher/Classes/Helper/SimpleProtectData.cs +++ b/CollapseLauncher/Classes/Helper/SimpleProtectData.cs @@ -1,7 +1,6 @@ using Hi3Helper; using System; using System.Buffers.Text; -using System.Security; using System.Security.Cryptography; using System.Text; diff --git a/CollapseLauncher/Classes/Helper/StreamUtility.cs b/CollapseLauncher/Classes/Helper/StreamUtility.cs index 91c943d04..add6a9cc0 100644 --- a/CollapseLauncher/Classes/Helper/StreamUtility.cs +++ b/CollapseLauncher/Classes/Helper/StreamUtility.cs @@ -17,27 +17,21 @@ internal static class StreamUtility { Mode = FileMode.Open, Access = FileAccess.Read, - Share = FileShare.Read, - BufferSize = DefaultBufferSize, - Options = FileOptions.Asynchronous | FileOptions.RandomAccess + Share = FileShare.Read }; internal static readonly FileStreamOptions FileStreamCreateWriteOpt = new FileStreamOptions { Mode = FileMode.Create, Access = FileAccess.Write, - Share = FileShare.Write, - BufferSize = DefaultBufferSize, - Options = FileOptions.Asynchronous | FileOptions.RandomAccess + Share = FileShare.Write }; internal static readonly FileStreamOptions FileStreamCreateReadWriteOpt = new FileStreamOptions { Mode = FileMode.Create, Access = FileAccess.ReadWrite, - Share = FileShare.ReadWrite, - BufferSize = DefaultBufferSize, - Options = FileOptions.Asynchronous | FileOptions.RandomAccess + Share = FileShare.ReadWrite }; /* diff --git a/CollapseLauncher/Classes/Helper/TaskSchedulerHelper.cs b/CollapseLauncher/Classes/Helper/TaskSchedulerHelper.cs new file mode 100644 index 000000000..1f8d9fef8 --- /dev/null +++ b/CollapseLauncher/Classes/Helper/TaskSchedulerHelper.cs @@ -0,0 +1,276 @@ +using CollapseLauncher.ShellLinkCOM; +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.Shared.Region; +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace CollapseLauncher.Helper +{ + internal static class TaskSchedulerHelper + { + private const string CollapseStartupTaskName = "CollapseLauncherStartupTask"; + + internal static bool IsInitialized; + internal static bool CachedIsOnTrayEnabled; + internal static bool CachedIsEnabled; + + internal static bool IsOnTrayEnabled() + { + if (!IsInitialized) + InvokeGetStatusCommand().GetAwaiter().GetResult(); + + return CachedIsOnTrayEnabled; + } + + internal static bool IsEnabled() + { + if (!IsInitialized) + InvokeGetStatusCommand().GetAwaiter().GetResult(); + + return CachedIsEnabled; + } + + private static async Task InvokeGetStatusCommand() + { + // Build the argument and mode to set + var argumentBuilder = new StringBuilder(); + argumentBuilder.Append("IsEnabled"); + + // Append task name and stub path + AppendTaskNameAndPathArgument(argumentBuilder); + + // Store argument builder as string + var argumentString = argumentBuilder.ToString(); + + // Invoke command and get return code + var returnCode = await GetInvokeCommandReturnCode(argumentString); + + (CachedIsEnabled, CachedIsOnTrayEnabled) = returnCode switch + { + // -1 means task is disabled with tray enabled + -1 => (false, true), + // 0 means task is disabled with tray disabled + 0 => (false, false), + // 1 means task is enabled with tray disabled + 1 => (true, false), + // 2 means task is enabled with tray enabled + 2 => (true, true), + // Otherwise, return both disabled (due to failure) + _ => (false, false) + }; + + // Print init determination + CheckInitDetermination(returnCode); + } + + private static void CheckInitDetermination(int returnCode) + { + // If the return code is within range, then set as initialized + if (returnCode is > -2 and < 3) + { + // Set as initialized + IsInitialized = true; + } + // Otherwise, log the return code + else + { + string reason = returnCode switch + { + int.MaxValue => "ARGUMENT_INVALID", + int.MinValue => "UNHANDLED_ERROR", + short.MaxValue => "INTERNALINVOKE_ERROR", + short.MinValue => "APPLET_NOTFOUND", + _ => $"UNKNOWN_{returnCode}" + }; + Logger.LogWriteLine($"Error while getting task status from applet with reason: {reason}", LogType.Error, true); + } + } + + internal static void ToggleTrayEnabled(bool isEnabled) + { + CachedIsOnTrayEnabled = isEnabled; + InvokeToggleCommand().GetAwaiter().GetResult(); + } + + internal static void ToggleEnabled(bool isEnabled) + { + CachedIsEnabled = isEnabled; + InvokeToggleCommand().GetAwaiter().GetResult(); + } + + private static async Task InvokeToggleCommand() + { + // Build the argument and mode to set + StringBuilder argumentBuilder = new StringBuilder(); + argumentBuilder.Append(CachedIsEnabled ? "Enable" : "Disable"); + + // Append argument whether to toggle the tray or not + if (CachedIsOnTrayEnabled) + argumentBuilder.Append("ToTray"); + + // Append task name and stub path + AppendTaskNameAndPathArgument(argumentBuilder); + + // Store argument builder as string + string argumentString = argumentBuilder.ToString(); + + // Invoke applet + int returnCode = await GetInvokeCommandReturnCode(argumentString); + + // Print init determination + CheckInitDetermination(returnCode); + } + + private static void AppendTaskNameAndPathArgument(StringBuilder argumentBuilder) + { + // Get current stub or main executable path + string currentExecPath = MainEntryPoint.FindCollapseStubPath(); + + // Build argument to the task name + argumentBuilder.Append(" \""); + argumentBuilder.Append(CollapseStartupTaskName); + argumentBuilder.Append('"'); + + // Build argument to the executable path + argumentBuilder.Append(" \""); + argumentBuilder.Append(currentExecPath); + argumentBuilder.Append('"'); + } + + internal static void RecreateIconShortcuts() + { + /* Invocation from Hi3Helper.TaskScheduler is no longer being user. + * Moving to Main App's ShellLink implementation instead! + + // Build the argument and get the current executable path + StringBuilder argumentBuilder = new StringBuilder(); + argumentBuilder.Append("RecreateIcons"); + string currentExecPath = LauncherConfig.AppExecutablePath; + + // Build argument to the executable path + argumentBuilder.Append(" \""); + argumentBuilder.Append(currentExecPath); + argumentBuilder.Append('"'); + + // Store argument builder as string + string argumentString = argumentBuilder.ToString(); + + // Invoke applet + int returnCode = await GetInvokeCommandReturnCode(argumentString); + + // Print init determination + CheckInitDetermination(returnCode); + */ + + // Get current executable path as its target. + string currentExecPath = LauncherConfig.AppExecutablePath; + string workingDirPath = Path.GetDirectoryName(currentExecPath); + + // Get exe's description + FileVersionInfo currentExecVersionInfo = FileVersionInfo.GetVersionInfo(currentExecPath); + string currentExecDescription = currentExecVersionInfo.FileDescription ?? ""; + + // Create shell link instance and save the shortcut under Desktop and User's Start menu + using ShellLink shellLink = new ShellLink() + { + IconIndex = 0, + IconPath = currentExecPath, + DisplayMode = LinkDisplayMode.edmNormal, + WorkingDirectory = workingDirPath, + Target = currentExecPath, + Description = currentExecDescription + }; + + // Get paths + string shortcutFilename = currentExecVersionInfo.ProductName + ".lnk"; + string startMenuLocation = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu); + string desktopLocation = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory); + string iconLocationStartMenu = Path.Combine( + startMenuLocation, + "Programs", + currentExecVersionInfo.CompanyName, + shortcutFilename); + string iconLocationDesktop = Path.Combine( + desktopLocation, + shortcutFilename); + + // Get icon location directory + string iconLocationStartMenuDir = Path.GetDirectoryName(iconLocationStartMenu); + string iconLocationDesktopDir = Path.GetDirectoryName(iconLocationDesktop); + + // Try create directory + Directory.CreateDirectory(iconLocationStartMenuDir); + Directory.CreateDirectory(iconLocationDesktopDir); + + // Save the icons + shellLink.Save(iconLocationStartMenu); + shellLink.Save(iconLocationDesktop); + } + + private static async Task GetInvokeCommandReturnCode(string argument) + { + const string retValMark = "RETURNVAL_"; + + // Get the applet path and check if the file exist + string appletPath = Path.Combine(LauncherConfig.AppFolder, "Lib", "win-x64", "Hi3Helper.TaskScheduler.exe"); + if (!File.Exists(appletPath)) + { + Logger.LogWriteLine($"Task Scheduler Applet does not exist in this path: {appletPath}", LogType.Error, true); + return short.MinValue; + } + + // Try to make process instance for the applet + using Process process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = appletPath, + Arguments = argument, + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + +#if DEBUG + Logger.LogWriteLine("[TaskSchedulerHelper] Running TaskSchedulerHelper with command:\r\n" + appletPath + " " + argument, LogType.Debug, true); +#endif + + int lastErrCode = short.MaxValue; + try + { + // Start the applet and wait until it exit. + process.Start(); + while (!process.StandardOutput.EndOfStream) + { + string consoleStdOut = await process.StandardOutput.ReadLineAsync(); + Logger.LogWriteLine("[TaskScheduler] " + consoleStdOut, LogType.Debug, true); + + // Parse if it has RETURNVAL_ + if (consoleStdOut == null || !consoleStdOut.StartsWith(retValMark)) + { + continue; + } + + ReadOnlySpan span = consoleStdOut.AsSpan(retValMark.Length); + if (int.TryParse(span, null, out int resultReturnCode)) + { + lastErrCode = resultReturnCode; + } + } + await process.WaitForExitAsync(); + } + catch (Exception ex) + { + // If error happened, then return. + Logger.LogWriteLine($"An error has occurred while invoking Task Scheduler applet!\r\n{ex}", LogType.Error, true); + return short.MaxValue; + } + + // Get return code + return lastErrCode; + } + } +} diff --git a/CollapseLauncher/Classes/Helper/Update/LauncherUpdateHelper.cs b/CollapseLauncher/Classes/Helper/Update/LauncherUpdateHelper.cs index 44ab9b118..4bb3619c4 100644 --- a/CollapseLauncher/Classes/Helper/Update/LauncherUpdateHelper.cs +++ b/CollapseLauncher/Classes/Helper/Update/LauncherUpdateHelper.cs @@ -1,14 +1,21 @@ #nullable enable - using Hi3Helper; - using Hi3Helper.Data; - using Hi3Helper.Shared.Region; - using Squirrel; - using Squirrel.Sources; - using System; - using System.Threading.Tasks; +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.Shared.Region; +using System; +using System.Threading.Tasks; +#if !USEVELOPACK +using Squirrel; +using Squirrel.Sources; +#else +using Microsoft.Extensions.Logging; +using Velopack; +using Velopack.Locators; +using Velopack.Sources; +#endif // ReSharper disable CheckNamespace - namespace CollapseLauncher.Helper.Update +namespace CollapseLauncher.Helper.Update { internal static class LauncherUpdateHelper { @@ -42,7 +49,7 @@ internal static async Task RunUpdateCheckDetached() } catch (Exception ex) { - Logger.LogWriteLine($"The squirrel check throws an error, Skipping update check!\r\n{ex}", LogType.Warning, true); + Logger.LogWriteLine($"The update manager check throws an error, Skipping update check!\r\n{ex}", LogType.Warning, true); } } @@ -51,12 +58,65 @@ internal static async Task IsUpdateAvailable(bool isForceCheckUpdate = fal string updateChannel = LauncherConfig.IsPreview ? "preview" : "stable"; CDNURLProperty launcherUpdatePreferredCdn = FallbackCDNUtil.GetPreferredCDN(); - string? launcherUpdateSquirrelBaseUrl = ConverterTool.CombineURLFromString(launcherUpdatePreferredCdn.URLPrefix, "squirrel", updateChannel); + string? launcherUpdateManagerBaseUrl = ConverterTool.CombineURLFromString(launcherUpdatePreferredCdn.URLPrefix, +#if USEVELOPACK + "velopack", + updateChannel +#else + "squirrel", + updateChannel +#endif + ); - IFileDownloader squirrelUpdateManagerHttpAdapter = new UpdateManagerHttpAdapter(); - using (UpdateManager squirrelUpdateManager = new UpdateManager(launcherUpdateSquirrelBaseUrl, null, null, squirrelUpdateManagerHttpAdapter)) + // Register the update manager adapter + IFileDownloader updateManagerHttpAdapter = new UpdateManagerHttpAdapter(); +#if USEVELOPACK + // Initialize update manager logger, locator and options + ILogger velopackLogger = ILoggerHelper.CreateCollapseILogger(); + VelopackLocator updateManagerLocator = VelopackLocator.GetDefault(velopackLogger); + UpdateOptions updateManagerOptions = new UpdateOptions { - UpdateInfo info = await squirrelUpdateManager.CheckForUpdate(); + AllowVersionDowngrade = true, + ExplicitChannel = updateChannel + }; + + // Initialize update manager source + IUpdateSource updateSource = new SimpleWebSource(launcherUpdateManagerBaseUrl, updateManagerHttpAdapter); + + // Initialize the update manager + UpdateManager updateManager = new UpdateManager( + updateSource, + updateManagerOptions, + velopackLogger, + updateManagerLocator); + + // Get the update info. If it's null, then return false (no update) + UpdateInfo? updateInfo = await updateManager.CheckForUpdatesAsync(); + if (updateInfo == null) + { + return false; + } + + // If there's an update, then get the update metadata + GameVersion updateVersion = new GameVersion(updateInfo.TargetFullRelease.Version.ToString()); + AppUpdateVersionProp = await GetUpdateMetadata(updateChannel); + if (AppUpdateVersionProp == null) + { + return false; + } + + // Compare the version + IsLauncherUpdateAvailable = LauncherCurrentVersion.Compare(updateVersion); + + // Get the status if the update is ignorable or forced update. + bool isUserIgnoreUpdate = (LauncherConfig.GetAppConfigValue("DontAskUpdate").ToBoolNullable() ?? false) && !isForceCheckUpdate; + bool isUpdateRoutineSkipped = isUserIgnoreUpdate && !AppUpdateVersionProp.IsForceUpdate; + + return IsLauncherUpdateAvailable && !isUpdateRoutineSkipped; +#else + using (UpdateManager updateManager = new UpdateManager(launcherUpdateManagerBaseUrl, null, null, updateManagerHttpAdapter)) + { + UpdateInfo info = await updateManager.CheckForUpdate(); if (info == null) return false; GameVersion remoteVersion = new GameVersion(info.FutureReleaseEntry.Version.Version); @@ -71,13 +131,14 @@ internal static async Task IsUpdateAvailable(bool isForceCheckUpdate = fal return IsLauncherUpdateAvailable && !isUpdateRoutineSkipped; } +#endif } private static async ValueTask GetUpdateMetadata(string updateChannel) { string relativePath = ConverterTool.CombineURLFromString(updateChannel, "fileindex.json"); await using BridgedNetworkStream ms = await FallbackCDNUtil.TryGetCDNFallbackStream(relativePath); - return await ms.DeserializeAsync(InternalAppJSONContext.Default); + return await ms.DeserializeAsync(InternalAppJSONContext.Default.AppUpdateVersionProp); } } } diff --git a/CollapseLauncher/Classes/Helper/WindowUtility.cs b/CollapseLauncher/Classes/Helper/WindowUtility.cs index 5ca94baeb..664b9ccdf 100644 --- a/CollapseLauncher/Classes/Helper/WindowUtility.cs +++ b/CollapseLauncher/Classes/Helper/WindowUtility.cs @@ -1,12 +1,14 @@ #nullable enable using CollapseLauncher.Extension; using CollapseLauncher.FileDialogCOM; + using H.NotifyIcon.Core; using Hi3Helper; using Hi3Helper.Screen; using Hi3Helper.Shared.Region; using Microsoft.Graphics.Display; using Microsoft.UI; using Microsoft.UI.Composition.SystemBackdrops; + using Microsoft.UI.Dispatching; using Microsoft.UI.Input; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; @@ -58,9 +60,38 @@ internal static DisplayArea? CurrentWindowDisplayArea } } - internal static DisplayInformation? CurrentWindowDisplayInformation => CurrentWindowId.HasValue - ? DisplayInformation.CreateForWindowId(CurrentWindowId.Value) - : null; + internal static DisplayInformation? CurrentWindowDisplayInformation + { + get + { + try + { + DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + if (dispatcherQueue.HasThreadAccess) + { + return CurrentWindowId.HasValue + ? DisplayInformation.CreateForWindowId(CurrentWindowId.Value) + : null; + } + else + { + DisplayInformation? displayInfoInit = null; + dispatcherQueue.TryEnqueue(() => + { + displayInfoInit = CurrentWindowId.HasValue + ? DisplayInformation.CreateForWindowId(CurrentWindowId.Value) + : null; + }); + return displayInfoInit; + } + } + catch (Exception ex) + { + Logger.LogWriteLine($"An error has occured while getting display information\r\n{ex}", LogType.Error, true); + } + return null; + } + } internal static DisplayAdvancedColorInfo? CurrentWindowDisplayColorInfo { @@ -143,6 +174,8 @@ internal static uint CurrentWindowMonitorDpi } internal static double CurrentWindowMonitorScaleFactor + // Deliberate loss of precision + // ReSharper disable once PossibleLossOfFraction => (CurrentWindowMonitorDpi * 100 + (96 >> 1)) / 96 / 100.0; internal static Rect CurrentWindowPosition @@ -361,6 +394,7 @@ private static IntPtr MainWndProc(IntPtr hwnd, uint msg, UIntPtr wParam, IntPtr { mainWindow._TrayIcon.ToggleAllVisibility(); } + else TrayNullHandler("WindowUtility.MainWndProc"); return 0; } @@ -536,11 +570,11 @@ private static void ApplyWindowBorderFix() | InvokeProp.SetWindowPosFlags.SWP_NOZORDER | InvokeProp.SetWindowPosFlags.SWP_FRAMECHANGED; InvokeProp.SetWindowPos(CurrentWindowPtr, 0, 0, 0, 0, 0, flags); - - var desktopSiteBridgeHwnd = InvokeProp.FindWindowEx(CurrentWindowPtr, 0, "Microsoft.UI.Content.DesktopChildSiteBridge", ""); - OldDesktopSiteBridgeWndProcPtr = InstallWndProcCallback(desktopSiteBridgeHwnd, DesktopSiteBridgeWndProc); } - } + + var desktopSiteBridgeHwnd = InvokeProp.FindWindowEx(CurrentWindowPtr, 0, "Microsoft.UI.Content.DesktopChildSiteBridge", ""); + OldDesktopSiteBridgeWndProcPtr = InstallWndProcCallback(desktopSiteBridgeHwnd, DesktopSiteBridgeWndProc); + } private static IntPtr DesktopSiteBridgeWndProc(IntPtr hwnd, uint msg, UIntPtr wParam, IntPtr lParam) { @@ -551,17 +585,14 @@ private static IntPtr DesktopSiteBridgeWndProc(IntPtr hwnd, uint msg, UIntPtr wP case WM_WINDOWPOSCHANGING: { // Fix the weird 1px offset - if (!InnerLauncherConfig.m_isWindows11) + var windowPos = Marshal.PtrToStructure(lParam); + if (windowPos.x == 0 && windowPos.y == 1 && + windowPos.cx == (int)(WindowSize.WindowSize.CurrentWindowSize.WindowBounds.Width * CurrentWindowMonitorScaleFactor) && + windowPos.cy == (int)(WindowSize.WindowSize.CurrentWindowSize.WindowBounds.Height * CurrentWindowMonitorScaleFactor) - 1) { - var windowPos = Marshal.PtrToStructure(lParam); - if (windowPos.x == 0 && windowPos.y == 1 && - windowPos.cx == WindowSize.WindowSize.CurrentWindowSize.WindowBounds.Width && - windowPos.cy == WindowSize.WindowSize.CurrentWindowSize.WindowBounds.Height - 1) - { - windowPos.y = 0; - windowPos.cy += 1; - Marshal.StructureToPtr(windowPos, lParam, false); - } + windowPos.y = 0; + windowPos.cy += 1; + Marshal.StructureToPtr(windowPos, lParam, false); } break; @@ -655,16 +686,15 @@ internal static bool IsCurrentWindowInFocus() #endregion #region Tray Icon Invoker - /// /// /// public static void ToggleToTray_MainWindow() { - if (CurrentWindow is MainWindow window) - { - window._TrayIcon.ToggleMainVisibility(); - } + if (CurrentWindow is not MainWindow window) return; + + if (window._TrayIcon != null) window._TrayIcon.ToggleMainVisibility(); + else TrayNullHandler(nameof(Tray_ShowNotification)); } /// @@ -672,10 +702,67 @@ public static void ToggleToTray_MainWindow() /// public static void ToggleToTray_AllWindow() { - if (CurrentWindow is MainWindow window) - { - window._TrayIcon.ToggleAllVisibility(); - } + if (CurrentWindow is not MainWindow window) return; + + if (window._TrayIcon != null) window._TrayIcon.ToggleAllVisibility(); + else TrayNullHandler(nameof(Tray_ShowNotification)); + } + + /// + /// + /// + /// The title to display on the balloon tip. + /// The text to display on the balloon tip. + /// A symbol that indicates the severity. + /// A custom icon. + /// True to allow large icons (Windows Vista and later). + /// If false do not play the associated sound. + /// + /// Do not display the balloon notification if the current user is in "quiet time", + /// which is the first hour after a new user logs into his or her account for the first time. + /// During this time, most notifications should not be sent or shown. + /// This lets a user become accustomed to a new computer system without those distractions. + /// Quiet time also occurs for each user after an operating system upgrade or clean installation. + /// A notification sent with this flag during quiet time is not queued; + /// it is simply dismissed unshown. The application can resend the notification later + /// if it is still valid at that time.
+ /// Because an application cannot predict when it might encounter quiet time, + /// we recommended that this flag always be set on all appropriate notifications + /// by any application that means to honor quiet time.
+ /// During quiet time, certain notifications should still be sent because + /// they are expected by the user as feedback in response to a user action, + /// for instance when he or she plugs in a USB device or prints a document.
+ /// If the current user is not in quiet time, this flag has no effect. + /// + /// + /// Windows Vista and later.
+ /// If the balloon notification cannot be displayed immediately, discard it. + /// Use this flag for notifications that represent real-time information + /// which would be meaningless or misleading if displayed at a later time.
+ /// For example, a message that states "Your telephone is ringing." + /// + // Taken from H.NotifyIcon.TrayIcon.ShowNotification docs + // https://github.com/HavenDV/H.NotifyIcon/blob/89356c52bedae45b1fd451531e8ac8cfe8b13086/src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs#L14 + public static void Tray_ShowNotification(string title, + string message, + NotificationIcon icon = NotificationIcon.None, + IntPtr? customIconHandle = null, + bool largeIcon = false, + bool sound = true, + bool respectQuietTime = true, + bool realtime = false) + { + if (CurrentWindow is not MainWindow window) return; + + if (window._TrayIcon != null) + window._TrayIcon.ShowNotification(title, message, icon, customIconHandle, largeIcon, sound, + respectQuietTime, realtime); + else TrayNullHandler(nameof(Tray_ShowNotification)); + } + + private static void TrayNullHandler(string caller) + { + Logger.LogWriteLine($"TrayIcon is null/not initialized!\r\n\tCalled by: {caller}"); } #endregion diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs index 8fde47789..8e6546f62 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs @@ -1,7 +1,7 @@ using Hi3Helper; using Hi3Helper.Data; using Hi3Helper.EncTool; -using Hi3Helper.Http; +using Hi3Helper.Http.Legacy; using Hi3Helper.Preset; using System; using System.Collections.Generic; @@ -9,27 +9,29 @@ using System.IO; using System.Linq; +#pragma warning disable CS0618 // Type or member is obsolete namespace CollapseLauncher.InstallManager { [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] internal class GameInstallPackage : IAssetIndexSummary { #region Properties - public string URL { get; set; } - public string DecompressedURL { get; set; } - public string Name { get; set; } - public string PathOutput { get; set; } - public GameInstallPackageType PackageType { get; set; } - public long Size { get; set; } - public long SizeRequired { get; set; } - public long SizeDownloaded { get; set; } - public GameVersion Version { get; set; } - public byte[] Hash { get; set; } - public string HashString { get => HexTool.BytesToHexUnsafe(Hash); } - public string LanguageID { get; set; } - public List Segments { get; set; } - public string RunCommand { get; set; } - public string PluginId { get; set; } + public string URL { get; set; } + public string DecompressedURL { get; set; } + public string Name { get; set; } + public string PathOutput { get; set; } + public GameInstallPackageType PackageType { get; set; } + public long Size { get; set; } + public long SizeRequired { get; set; } + public long SizeDownloaded { get; set; } + public GameVersion Version { get; set; } + public byte[] Hash { get; set; } + public string HashString { get => HexTool.BytesToHexUnsafe(Hash); } + public string LanguageID { get; set; } + public List Segments { get; set; } + public string RunCommand { get; set; } + public string PluginId { get; set; } + public bool IsUseLegacyDownloader { get; set; } #endregion public GameInstallPackage(RegionResourcePlugin packageProperty, string pathOutput) @@ -106,11 +108,13 @@ public bool IsReadStreamExist(int count) // Get the hash number long ID = Http.GetHashNumber(count, chunkID); // Append the hash number to the path - string path = $"{PathOutput}.{ID}"; + string pathLegacy = $"{PathOutput}.{ID}"; + string path = PathOutput + string.Format(".{0:000}", chunkID + 1); // Get the file info + FileInfo _fileInfoLegacy = new FileInfo(pathLegacy); FileInfo _fileInfo = new FileInfo(path); // Check if the file exist - return _fileInfo.Exists; + return _fileInfoLegacy.Exists || _fileInfo.Exists; }); } @@ -161,20 +165,27 @@ private CombinedStream GetCombinedStreamFromPackageAsset(int count) // Get the hash ID long ID = Http.GetHashNumber(count, i); // Append hash ID to the path - string path = $"{PathOutput}.{ID}"; + string path = PathOutput + string.Format(".{0:000}", i + 1); + string pathLegacy = $"{PathOutput}.{ID}"; // Get the file info and check if the file exist FileInfo fileInfo = new FileInfo(path); - if (fileInfo.Exists) + FileInfo fileInfoLegacy = new FileInfo(pathLegacy); + if (fileInfo.Exists || fileInfoLegacy.Exists) { // Allocate to the array and open the stream - streamList[i] = fileInfo.Open(new FileStreamOptions + FileStreamOptions opt = new FileStreamOptions { Access = FileAccess.Read, BufferSize = 4 << 10, Mode = FileMode.Open, Options = FileOptions.None, Share = FileShare.Read - }); + }; + if (fileInfo.Exists) + streamList[i] = fileInfo.Open(opt); + else if (fileInfoLegacy.Exists) + streamList[i] = fileInfoLegacy.Open(opt); + // Then go back to the loop routine continue; } @@ -197,13 +208,18 @@ private long GetCombinedLengthFromPackageAsset(int count) // Get the hash ID long ID = Http.GetHashNumber(count, i); // Append hash ID to the path - string path = $"{PathOutput}.{ID}"; + string path = PathOutput + string.Format(".{0:000}", i + 1); + string pathLegacy = $"{PathOutput}.{ID}"; // Get the file info and check if the file exist FileInfo fileInfo = new FileInfo(path); - if (fileInfo.Exists) + FileInfo fileInfoLegacy = new FileInfo(pathLegacy); + if (fileInfo.Exists || fileInfoLegacy.Exists) { // Add length to the existing one - length += fileInfo.Length; + if (fileInfo.Exists) + length += fileInfo.Length; + else if (fileInfoLegacy.Exists) + length += fileInfoLegacy.Length; // Then go back to the loop routine // ReSharper disable once RedundantJumpStatement continue; @@ -228,13 +244,22 @@ public void DeleteFile(int count) for (int i = 0; i < count; i++) { long ID = Http.GetHashNumber(count, i); - string path = $"{PathOutput}.{ID}"; - lastFile = path; + string path = PathOutput + string.Format(".{0:000}", i + 1); + string pathLegacy = $"{PathOutput}.{ID}"; + bool isUseLegacy = File.Exists(pathLegacy); + + lastFile = isUseLegacy ? pathLegacy : path; fileInfo = new FileInfo(path); + FileInfo fileInfoLegacy = new FileInfo(pathLegacy); if (fileInfo.Exists) { fileInfo.Delete(); } + + if (fileInfoLegacy.Exists) + { + fileInfoLegacy.Delete(); + } } } catch (Exception ex) diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs index 0146c87f8..a89b54451 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs @@ -169,7 +169,7 @@ protected virtual async Task> GetUnusedFileInfoList(bool inc { // Initialize new proxy-aware HttpClient using HttpClient httpClient = new HttpClientBuilder() - .UseLauncherConfig() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) .SetAllowedDecompression(DecompressionMethods.None) .Create(); @@ -178,7 +178,7 @@ protected virtual async Task> GetUnusedFileInfoList(bool inc Locale.Lang._FileCleanupPage.LoadingTitle, Locale.Lang._FileCleanupPage.LoadingSubtitle2); - using Http client = new Http(httpClient); + DownloadClient downloadClient = DownloadClient.CreateInstance(httpClient); RegionResourceVersion? packageLatestBase = _gameVersionManager .GetGameLatestZip(gameStateEnum).FirstOrDefault(); string? packageExtractBasePath = packageLatestBase?.decompressed_path; @@ -190,8 +190,7 @@ protected virtual async Task> GetUnusedFileInfoList(bool inc // Check Fail-safe: Download main pkg_version file string mainPkgVersionUrl = ConverterTool.CombineURLFromString(packageExtractBasePath, "pkg_version"); - await client.Download(mainPkgVersionUrl, pkgVersionPath, _downloadThreadCount, true); - await client.Merge(default); + await downloadClient.DownloadAsync(mainPkgVersionUrl, pkgVersionPath, true); // Check Fail-safe: Download audio pkg_version files if (!string.IsNullOrEmpty(_gameAudioLangListPathStatic) && @@ -206,7 +205,7 @@ protected virtual async Task> GetUnusedFileInfoList(bool inc await DownloadOtherAudioPkgVersion(_gameAudioLangListPathStatic, packageExtractBasePath, - client); + downloadClient); } } @@ -239,7 +238,7 @@ await DownloadOtherAudioPkgVersion(_gameAudioLangListPathStatic, } // Add pre-download zips into the ignored list - var packagePreDownloadList = _gameVersionManager.GetGamePreloadZip().FirstOrDefault(); + RegionResourceVersion? packagePreDownloadList = _gameVersionManager.GetGamePreloadZip()?.FirstOrDefault(); if (packagePreDownloadList != null) { var preDownloadZips = new List(); @@ -301,7 +300,7 @@ await Task.Run(() => } protected virtual async ValueTask DownloadOtherAudioPkgVersion(string audioListFilePath, string baseExtractUrl, - Http client) + DownloadClient downloadClient) { // Initialize reader using StreamReader reader = new StreamReader(audioListFilePath); @@ -327,8 +326,7 @@ protected virtual async ValueTask DownloadOtherAudioPkgVersion(string audioListF } // Download the file - await client.Download(pkgUrl, pkgPath, _downloadThreadCount, true); - await client.Merge(default); + await downloadClient.DownloadAsync(pkgUrl, pkgPath, true); } } @@ -359,7 +357,7 @@ protected virtual async ValueTask InnerParsePkgVersion2FileInfo(string { // Read line and deserialize string? line = await reader.ReadLineAsync(token); - LocalFileInfo? localFileInfo = line?.Deserialize(InternalAppJSONContext.Default); + LocalFileInfo? localFileInfo = line?.Deserialize(InternalAppJSONContext.Default.LocalFileInfo); // Assign the values if (localFileInfo == null) diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs index a4ca220ec..a48de6f4f 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs @@ -14,8 +14,8 @@ // ReSharper disable ConvertToPrimaryConstructor using CollapseLauncher.CustomControls; +using CollapseLauncher.Dialogs; using CollapseLauncher.Extension; -using CollapseLauncher.FileDialogCOM; using CollapseLauncher.Helper; using CollapseLauncher.Helper.Metadata; using CollapseLauncher.Interfaces; @@ -24,6 +24,7 @@ using Hi3Helper.Data; using Hi3Helper.EncTool.Parser.AssetIndex; using Hi3Helper.Http; +using Hi3Helper.Http.Legacy; using Hi3Helper.Shared.ClassStruct; using Hi3Helper.Shared.Region; using Hi3Helper.Sophon; @@ -42,6 +43,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -59,6 +61,8 @@ using SophonLogger = Hi3Helper.Sophon.Helper.Logger; using SophonManifest = Hi3Helper.Sophon.SophonManifest; +using CollapseLauncher.Classes.FileDialogCOM; +using CollapseLauncher.DiscordPresence; // ReSharper disable ForCanBeConvertedToForeach // ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault @@ -77,6 +81,14 @@ public enum CompletenessStatus Idle } + public enum MigrateFromLauncherType + { + Official, + BetterHi3Launcher, + Steam, + Unknown + } + // ReSharper disable once UnusedTypeParameter internal partial class InstallManagerBase : ProgressBase, IGameInstallManager { @@ -90,14 +102,6 @@ protected struct UninstallGameProperty public string[] foldersToKeepInData; } - protected enum MigrateFromLauncherType - { - Official, - BetterHi3Launcher, - Steam, - Unknown - } - protected delegate Task InstallPackageExtractorDelegate(GameInstallPackage asset); #endregion @@ -134,7 +138,7 @@ protected enum MigrateFromLauncherType protected virtual bool _canDeltaPatch => false; protected virtual DeltaPatchProperty _gameDeltaPatchProperty => null; - protected List _gameDeltaPatchPreReqList = []; + protected List _gameDeltaPatchPreReqList { get; } = []; protected bool _forceIgnoreDeltaPatch; private long _totalLastSizeCurrent; @@ -395,7 +399,7 @@ await Task.Run(() => HDiffPatch patch = new HDiffPatch(); patch.Initialize(patchProperty!.PatchPath); patch.Patch(ingredientPath, previousPath, true, _token!.Token, false, true); - }); + }).ConfigureAwait(false); // Remove ingredient folder Directory.Delete(ingredientPath, true); @@ -414,12 +418,6 @@ await Task.Run(() => // Delete the delta patch file File.Delete(patchProperty!.PatchPath!); - // Delete the pre-req delta patch file if there's one - foreach (GameInstallPackage package in _gameDeltaPatchPreReqList!) - { - DeleteSingleOrSegmentedDownloadStream(package); - } - // Then return return true; } @@ -507,9 +505,6 @@ protected virtual async ValueTask GetAndDownloadDeltaPatchPreReq( // Start the download routine await StartDeltaPatchPreReqDownload(gamePackage); - // Start the install routine - await StartPackageInstallationInner(gamePackage, true, true)!; - return true; } @@ -521,6 +516,15 @@ protected virtual async ValueTask StartDeltaPatchPreReqDownload(List x!.Segments != null ? x.Segments.Count : 1) > 1; _status.IsProgressPerFileIndetermined = true; @@ -532,7 +536,7 @@ protected virtual async ValueTask StartDeltaPatchPreReqDownload(List x!.Size); - _progressAllSizeCurrent = GetExistingDownloadPackageSize(gamePackage); + _progressAllSizeCurrent = await GetExistingDownloadPackageSize(downloadClient, gamePackage, _token!.Token); // Sanitize Check: Check for the free space of the drive and show the dialog if necessary await CheckDriveFreeSpace(_parentUI, gamePackage, _progressAllSizeCurrent); @@ -546,7 +550,7 @@ protected virtual async ValueTask StartDeltaPatchPreReqDownload(List null; +#nullable restore + // Bool: 0 -> Indicates that the action is completed and no need to step further // 1 -> Continue to the next step // -1 -> Cancel the operation @@ -652,6 +660,15 @@ public virtual async Task StartPackageDownload(bool skipDialog) break; } + // Initialize new proxy-aware HttpClient + using HttpClient httpClientNew = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) + .SetAllowedDecompression(DecompressionMethods.None) + .Create(); + + // Use the new DownloadClient (if available) + DownloadClient downloadClient = DownloadClient.CreateInstance(httpClientNew); + // Set the progress bar to indetermined _status!.IsIncludePerFileIndicator = _assetIndex!.Sum(x => x!.Segments != null ? x.Segments.Count : 1) > 1; @@ -664,7 +681,7 @@ public virtual async Task StartPackageDownload(bool skipDialog) // Get the remote total size and current total size _progressAllSizeTotal = _assetIndex!.Sum(x => x!.Size); - _progressAllSizeCurrent = GetExistingDownloadPackageSize(_assetIndex); + _progressAllSizeCurrent = await GetExistingDownloadPackageSize(downloadClient, _assetIndex, _token!.Token); // Sanitize Check: Check for the free space of the drive and show the dialog if necessary await CheckDriveFreeSpace(_parentUI, _assetIndex, _progressAllSizeCurrent); @@ -676,7 +693,7 @@ public virtual async Task StartPackageDownload(bool skipDialog) } // Start downloading process - await InvokePackageDownloadRoutine(_assetIndex, _token!.Token); + await InvokePackageDownloadRoutine(downloadClient, _assetIndex, _token!.Token); } // Bool: 0 -> Indicates that one of the package is failing and need to redownload @@ -925,11 +942,7 @@ await _gameVersionManager.GamePreset async ValueTask DelegateAssetDownload(SophonAsset asset, CancellationToken _) { // ReSharper disable once AccessToDisposedClosure - if (httpClient != null) - { - // ReSharper disable once AccessToDisposedClosure - await RunSophonAssetDownloadThread(httpClient, asset, parallelChunksOptions); - } + await RunSophonAssetDownloadThread(httpClient, asset, parallelChunksOptions); } // Declare the rename temp file delegate @@ -982,12 +995,24 @@ async Task RunTaskAction(HttpClient client, List so ParallelOptions parallelOptions, Func actionDelegate) { - foreach (SophonChunkManifestInfoPair sophonDownloadInfoPair in sophonInfoPairList) + // Create a sophon download speed limiter instance + SophonDownloadSpeedLimiter downloadSpeedLimiter = SophonDownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached); + + try { - // Enumerate in parallel and process the assets - await Parallel.ForEachAsync(SophonManifest.EnumerateAsync(client, sophonDownloadInfoPair), - parallelOptions, - actionDelegate); + LauncherConfig.DownloadSpeedLimitChanged += downloadSpeedLimiter.GetListener(); + foreach (SophonChunkManifestInfoPair sophonDownloadInfoPair in sophonInfoPairList) + { + // Enumerate in parallel and process the assets + await Parallel.ForEachAsync(SophonManifest.EnumerateAsync(client, sophonDownloadInfoPair, + downloadSpeedLimiter), + parallelOptions, + actionDelegate); + } + } + finally + { + LauncherConfig.DownloadSpeedLimitChanged -= downloadSpeedLimiter.GetListener(); } } } @@ -1057,21 +1082,25 @@ await _gameVersionManager.GamePreset // Add the tag query to the previous version's Url requestedBaseUrlFrom += $"&tag={requestedVersionFrom.ToString()}"; + // Create a sophon download speed limiter instance + SophonDownloadSpeedLimiter downloadSpeedLimiter = SophonDownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached); + // Add base game diff data await AddSophonDiffAssetsToList(httpClient, requestedBaseUrlFrom, requestedBaseUrlTo, - sophonUpdateAssetList, "game"); + sophonUpdateAssetList, "game", downloadSpeedLimiter); // If the game has lang list path, then add it if (_gameAudioLangListPath != null) { // Add existing voice-over diff data await AddSophonAdditionalVODiffAssetsToList(httpClient, requestedBaseUrlFrom, - requestedBaseUrlTo, sophonUpdateAssetList); + requestedBaseUrlTo, sophonUpdateAssetList, + downloadSpeedLimiter); } } // Get the remote chunk size - _progressPerFileSizeTotal = sophonUpdateAssetList.GetCalculatedDiffSize(); + _progressPerFileSizeTotal = sophonUpdateAssetList.GetCalculatedDiffSize(!isPreloadMode); _progressPerFileSizeCurrent = 0; // Get the remote total size and current total size @@ -1120,6 +1149,24 @@ await AddSophonAdditionalVODiffAssetsToList(httpClient, requestedBaseUrl bool canDeleteChunks = _canDeleteZip; bool isSophonPreloadCompleted = _isSophonPreloadCompleted; + // If preload completed and perf mode is on, use all CPU cores + if (isSophonPreloadCompleted && LauncherConfig.GetAppConfigValue("SophonPreloadApplyPerfMode").ToBool()) + { + maxThread = Environment.ProcessorCount; + maxChunksThread = Math.Clamp(maxThread / 2, 2, 32); + + parallelOptions = new ParallelOptions + { + MaxDegreeOfParallelism = maxThread, + CancellationToken = _token.Token + }; + parallelChunksOptions = new ParallelOptions + { + MaxDegreeOfParallelism = maxChunksThread, + CancellationToken = _token.Token + }; + } + // Set the delegate function for the download action async ValueTask Action(SophonAsset asset, CancellationToken _) { @@ -1187,10 +1234,12 @@ protected virtual void CleanupTempSophonVerifiedFiles() } } - private async Task AddSophonDiffAssetsToList(HttpClient httpClient, - string requestedUrlFrom, string requestedUrlTo, - List sophonPreloadAssetList, - string matchingField) + private async Task AddSophonDiffAssetsToList(HttpClient httpClient, + string requestedUrlFrom, + string requestedUrlTo, + List sophonPreloadAssetList, + string matchingField, + SophonDownloadSpeedLimiter downloadSpeedLimiter) { // Get the manifest pair for both previous (from) and next (to) version SophonChunkManifestInfoPair requestPairFrom = await SophonManifest @@ -1201,22 +1250,24 @@ private async Task AddSophonDiffAssetsToList(HttpClient httpClient, // Add asset to the list await foreach (SophonAsset sophonAsset in SophonUpdate .EnumerateUpdateAsync(httpClient, requestPairFrom, requestPairTo, - false) + false, downloadSpeedLimiter) .WithCancellation(_token.Token)) { sophonPreloadAssetList.Add(sophonAsset); } } - private async Task AddSophonAdditionalVODiffAssetsToList(HttpClient httpClient, - string requestedUrlFrom, string requestedUrlTo, - List sophonPreloadAssetList) + private async Task AddSophonAdditionalVODiffAssetsToList(HttpClient httpClient, + string requestedUrlFrom, + string requestedUrlTo, + List sophonPreloadAssetList, + SophonDownloadSpeedLimiter downloadSpeedLimiter) { // Get the main VO language name from Id string mainLangId = GetLanguageLocaleCodeByID(_gameVoiceLanguageID); // Get the manifest pair for both previous (from) and next (to) version for the main VO - await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo, sophonPreloadAssetList, - mainLangId); + await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo, + sophonPreloadAssetList, mainLangId, downloadSpeedLimiter); // Check if the audio lang list file is exist, then try add others if (File.Exists(_gameAudioLangListPath)) @@ -1237,7 +1288,7 @@ await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo, so string otherLangId = GetLanguageLocaleCodeByLanguageString(line); // Get the manifest pair for both previous (from) and next (to) version for other VOs await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo, - sophonPreloadAssetList, otherLangId); + sophonPreloadAssetList, otherLangId, downloadSpeedLimiter); } } } @@ -1350,19 +1401,19 @@ private async ValueTask RunPackageVerificationRoutine(GameInstallPackage as private long GetAssetIndexTotalUncompressSize(List assetIndex) { - long returnSize = 0; ArgumentNullException.ThrowIfNull(assetIndex); - foreach (GameInstallPackage asset in assetIndex) - { - using Stream stream = GetSingleOrSegmentedDownloadStream(asset); - using ArchiveFile archiveFile = new ArchiveFile(stream!); - returnSize += archiveFile.Entries.Sum(x => (long)x!.Size); - } - + long returnSize = assetIndex.Sum(GetSingleOrSegmentedUncompressedSize); return returnSize; } + private long GetSingleOrSegmentedUncompressedSize(GameInstallPackage asset) + { + using Stream stream = GetSingleOrSegmentedDownloadStream(asset); + using ArchiveFile archiveFile = new ArchiveFile(stream!); + return archiveFile.Entries.Sum(x => (long)x!.Size); + } + private Stream GetSingleOrSegmentedDownloadStream(GameInstallPackage asset) { if (asset == null) @@ -1449,12 +1500,19 @@ protected virtual async Task StartPackageInstallationInner(List= 0) + // int indexOfArgument = asset.RunCommand.IndexOf(".exe ", StringComparison.OrdinalIgnoreCase) + 5; + // if (indexOfArgument < 5 && !asset.RunCommand.EndsWith(".exe")) + // { + // indexOfArgument = asset.RunCommand.IndexOf(' '); + // } + // else + // { + // indexOfArgument = -1; + // } + // + // if (indexOfArgument >= 0) + // { + // argument = asset.RunCommand.Substring(indexOfArgument); + // executableName = + // ConverterTool.NormalizePath(asset.RunCommand.Substring(0, indexOfArgument) + // .TrimEnd(' ')); + // } + // else + // { + // executableName = asset.RunCommand; + // } + + var firstSpaceIndex = asset.RunCommand.IndexOf(' '); + if (firstSpaceIndex != -1) { - argument = asset.RunCommand.Substring(indexOfArgument); - executableName = - ConverterTool.NormalizePath(asset.RunCommand.Substring(0, indexOfArgument) - .TrimEnd(' ')); + // Split into executable and arguments + executableName = asset.RunCommand.Substring(0, firstSpaceIndex); + arguments = asset.RunCommand.Substring(firstSpaceIndex + 1); } else { + // No arguments, only executable executableName = asset.RunCommand; + arguments = string.Empty; } - - string executablePath = Path.Combine(_gamePath, executableName); + + + string executablePath = ConverterTool.NormalizePath(Path.Combine(_gamePath, executableName)); Process commandProcess = new Process { StartInfo = new ProcessStartInfo { FileName = executablePath, - Arguments = argument, - UseShellExecute = true + Arguments = arguments, + UseShellExecute = true, + WorkingDirectory = Path.GetDirectoryName(executablePath) } }; @@ -1540,6 +1618,7 @@ protected virtual async Task StartPackageInstallationInner(List entriesIndex, List entries, CancellationToken cancellationToken) { - // 16 MB of buffer (hope it's not too big) - byte[] buffer = new byte[16 << 20]; + // 4 MB of buffer + byte[] buffer = GC.AllocateUninitializedArray(4 << 20); foreach (int entryIndex in entriesIndex) { @@ -1627,13 +1706,7 @@ private async Task ExtractUsingNativeZipWorker(IEnumerable entriesIndex, L continue; } - string outputPath = Path.Combine(_gamePath, zipEntry.Key); - string dirPath = Path.GetDirectoryName(outputPath); - - if (!Directory.Exists(dirPath) && dirPath != null) - { - Directory.CreateDirectory(dirPath); - } + string outputPath = EnsureCreationOfDirectory(Path.Combine(_gamePath, zipEntry.Key)); int read; await using FileStream outputStream = @@ -2055,39 +2128,53 @@ public virtual async ValueTask TryShowFailedGameConversionState() return await Task.FromResult(false); } - public virtual void ApplyDeleteFileAction() + public virtual async Task ApplyDeleteFileActionAsync(CancellationToken token = default) { - foreach (string path in Directory.EnumerateFiles(_gamePath, "deletefiles_*", SearchOption.TopDirectoryOnly)) + async IAsyncEnumerable EnumerateFileInfoAsync([EnumeratorCancellation] CancellationToken innerToken) + { + foreach (string path in Directory.EnumerateFiles(_gamePath, "deletefiles_*", SearchOption.TopDirectoryOnly)) + { + using StreamReader sw = new StreamReader(path, + new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Options = _canDeleteHdiffReference + ? FileOptions.DeleteOnClose + : FileOptions.None + }); + while (!sw.EndOfStream) + { + string deleteFile = GetBasePersistentDirectory(_gamePath, await sw.ReadLineAsync(innerToken)); + FileInfo fileInfo = new FileInfo(deleteFile); + yield return fileInfo; + } + } + } + + await Parallel.ForEachAsync(EnumerateFileInfoAsync(token), token, (fileInfo, innerToken) => { - using StreamReader sw = new StreamReader(path, - new FileStreamOptions - { - Mode = FileMode.Open, - Access = FileAccess.Read, - Options = _canDeleteHdiffReference - ? FileOptions.DeleteOnClose - : FileOptions.None - }); - while (!sw.EndOfStream) + return new ValueTask(Task.Run(() => { - string deleteFile = GetBasePersistentDirectory(_gamePath, sw.ReadLine()); - FileInfo fileInfo = new FileInfo(deleteFile); + innerToken.ThrowIfCancellationRequested(); try { - if (fileInfo.Exists) + if (!fileInfo.Exists) { - fileInfo.IsReadOnly = false; - fileInfo.Delete(); - LogWriteLine($"Deleting old file: {deleteFile}"); + return; } + + fileInfo.IsReadOnly = false; + fileInfo.Delete(); + LogWriteLine($"Deleting old file: {fileInfo.FullName}"); } catch (Exception ex) { - LogWriteLine($"Failed deleting old file: {deleteFile}\r\n{ex}", LogType.Warning, true); + LogWriteLine($"Failed deleting old file: {fileInfo.FullName}\r\n{ex}", LogType.Warning, true); } - } - } + }, innerToken)); + }); } private string GetBasePersistentDirectory(string basePath, string input) @@ -2250,10 +2337,6 @@ public virtual async ValueTask ApplyHdiffListPatch() { throw ex.Flatten().InnerExceptions.First(); } - catch (Exception) - { - throw; - } finally { EventListener.LoggerEvent -= EventListener_PatchLogEvent; @@ -2328,7 +2411,7 @@ public virtual List TryGetHDiffList() while (!listReader.EndOfStream) { string currentLine = listReader.ReadLine(); - var prop = currentLine?.Deserialize(CoreLibraryJSONContext.Default); + var prop = currentLine?.Deserialize(CoreLibraryJSONContext.Default.PkgVersionProperties); if (prop == null) { @@ -2532,12 +2615,7 @@ protected virtual bool TryGetVoiceOverResourceByLocaleCode(List localeCode) { - // If it's empty or null, return false - if (localeCode == null) - { - return false; - } - + // If it's empty, return false if (localeCode.IsEmpty) { return false; @@ -2674,34 +2752,142 @@ private async ValueTask CheckExistingSteamInstallation() { // If the "Use current directory" option is chosen (migrationOptionReturn == 1), then proceed to another routine. // If not, then return the migrationOptionReturn value. - int migrationOptionReturn = - await PerformMigrationOption(_gameVersionManager.GamePreset.ActualGameDataLocation, - MigrateFromLauncherType.Official); - if (migrationOptionReturn != 1) - { - return migrationOptionReturn; - } + int migrationOptionReturn = await PerformMigrationOption(pathOnSteam, MigrateFromLauncherType.Steam); - switch (await Dialog_ExistingInstallationSteam(_parentUI)) + // If the option is applying to the current directory + if (migrationOptionReturn == 0) { - // If action to migrate was taken, then update the game path (but don't save it to the config file) - // After that, return 0 - case ContentDialogResult.Primary: - _gameVersionManager.UpdateGamePath(pathOnSteam, false); - return 0; - // If action to fresh install was taken, then return 2 (selecting path) - case ContentDialogResult.Secondary: - return 2; - // If action to cancel was taken, then return -1 (go back) - case ContentDialogResult.None: - return -1; + _gameVersionManager.UpdateGamePath(pathOnSteam, false); + await StartSteamMigration(pathOnSteam); + _gameVersionManager.UpdateGameVersionToLatest(false); + return 0; } + + if (migrationOptionReturn != 0) + return migrationOptionReturn; } // Return 1 to continue to another check return 1; } +#nullable enable + private async Task StartSteamMigration(string gamePath) + { + // Get game repair instance and if it's null, then return; + string? latestGameVersionString = _gameVersionManager.GetGameVersionAPI()?.VersionString; + if (string.IsNullOrEmpty(latestGameVersionString)) + return; + + using IRepair? gameRepairInstance = GetGameRepairInstance(latestGameVersionString); + if (gameRepairInstance == null) + return; + + // Build the UI + Grid mainGrid = UIElementExtensions.CreateGrid() + .WithWidth(590) + .WithColumns([ + new GridLength(1, GridUnitType.Star), + new GridLength(1, GridUnitType.Auto) + ]) + .WithRows([ + new GridLength(1, GridUnitType.Star), + new GridLength(1, GridUnitType.Star), + new GridLength(1, GridUnitType.Star), + new GridLength(1, GridUnitType.Star), + new GridLength(1, GridUnitType.Star) + ]) + .WithColumnSpacing(16); + + TextBlock statusActivity = mainGrid.AddElementToGridRowColumn( + new TextBlock() { + FontWeight = FontWeights.Medium, + FontSize = 18, + Text = Lang._InstallMigrateSteam.Step3Title, + TextWrapping = TextWrapping.Wrap } + .WithHorizontalAlignment(HorizontalAlignment.Left), + 0, 0, 0, 2) + .WithMargin(0, 0, 0, 8); + TextBlock fileActivityStatus = mainGrid.AddElementToGridRowColumn( + new TextBlock() { Text = "-", TextTrimming = TextTrimming.CharacterEllipsis } + .WithHorizontalAlignment(HorizontalAlignment.Left), + 1, 0, 0, 2); + TextBlock speedStatus = mainGrid.AddElementToGridRowColumn( + new TextBlock() { FontWeight = FontWeights.Bold, Text = Lang._Misc.SpeedPlaceholder, TextTrimming = TextTrimming.CharacterEllipsis } + .WithHorizontalAlignment(HorizontalAlignment.Left), + 2, 0, 0, 2); + TextBlock timeRemainStatus = mainGrid.AddElementToGridRowColumn( + new TextBlock() { FontWeight = FontWeights.Bold, Text = Lang._Misc.TimeRemainHMSFormatPlaceholder, TextTrimming = TextTrimming.CharacterEllipsis } + .WithHorizontalAlignment(HorizontalAlignment.Left), + 3, 0, 0, 2); + TextBlock percentageStatus = mainGrid.AddElementToGridRowColumn( + new TextBlock() { FontWeight = FontWeights.Bold, Text = "0.00%" } + .WithHorizontalAlignment(HorizontalAlignment.Right), + 3, 1, 0, 0); + ProgressBar progressBar = mainGrid.AddElementToGridRowColumn( + new ProgressBar { IsIndeterminate = true }, + 4, 0, 0, 2) + .WithMargin(0, 16, 0, 0); + + gameRepairInstance.ProgressChanged += StartSteamMigration_ProgressChanged; + gameRepairInstance.StatusChanged += StartSteamMigration_StatusChanged; + ContentDialogCollapse contentDialog = new ContentDialogCollapse(ContentDialogTheme.Informational) + { + Title = Lang._InstallMigrateSteam.PageTitle, + Content = mainGrid, + XamlRoot = _parentUI.XamlRoot, + CloseButtonText = Lang._Misc.Cancel + }; + + contentDialog.CloseButtonClick += (_, _) => + { + gameRepairInstance.CancelRoutine(); + }; + + try + { + #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + // This is intentional as the dialog is only to cancel the routine, not waiting for user input. + SimpleDialogs.QueueAndSpawnDialog(contentDialog); + #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + await gameRepairInstance.StartCheckRoutine(); + statusActivity.Text = Lang._InstallMigrateSteam.Step4Title; + await gameRepairInstance.StartRepairRoutine(false); + contentDialog.Hide(); + } + catch (Exception) + { + contentDialog.Hide(); + throw; + } + finally + { + gameRepairInstance.ProgressChanged -= StartSteamMigration_ProgressChanged; + gameRepairInstance.StatusChanged -= StartSteamMigration_StatusChanged; + } + + void StartSteamMigration_ProgressChanged(object? sender, TotalPerfileProgress e) + { + Dispatch(() => + { + progressBar.IsIndeterminate = false; + progressBar.Value = e.ProgressAllPercentage; + percentageStatus.Text = string.Format("{0}%", Math.Round(e.ProgressAllPercentage, 2)); + }); + } + + void StartSteamMigration_StatusChanged(object? sender, TotalPerfileStatus e) + { + Dispatch(() => + { + fileActivityStatus.Text = e.ActivityStatus; + speedStatus.Text = e.ActivityPerFile; + timeRemainStatus.Text = e.ActivityAll; + }); + } + } +#nullable restore + private async ValueTask CheckExistingBHI3LInstallation() { string pathOnBHi3L = ""; @@ -2709,27 +2895,17 @@ private async ValueTask CheckExistingBHI3LInstallation() { // If the "Use current directory" option is chosen (migrationOptionReturn == 1), then proceed to another routine. // If not, then return the migrationOptionReturn value. - int migrationOptionReturn = - await PerformMigrationOption(_gameVersionManager.GamePreset.ActualGameDataLocation, - MigrateFromLauncherType.Official); - if (migrationOptionReturn != 1) - { - return migrationOptionReturn; - } + int migrationOptionReturn = await PerformMigrationOption(pathOnBHi3L, MigrateFromLauncherType.BetterHi3Launcher); - switch (await Dialog_ExistingInstallationBetterLauncher(_parentUI, pathOnBHi3L)) + // If the option is applying to the current directory + if (migrationOptionReturn == 0) { - // If action to migrate was taken, then update the game path (but don't save it to the config file) - case ContentDialogResult.Primary: - _gameVersionManager.UpdateGamePath(pathOnBHi3L, false); - return 0; - // If action to fresh install was taken, then return 2 (selecting path) - case ContentDialogResult.Secondary: - return 2; - // If action to cancel was taken, then return -1 (go back) - case ContentDialogResult.None: - return -1; + _gameVersionManager.UpdateGamePath(pathOnBHi3L, false); + return 0; } + + if (migrationOptionReturn != 0) + return migrationOptionReturn; } // Return 1 to continue to another check @@ -2790,13 +2966,29 @@ private async ValueTask PerformMigrationOption(string path pathIfUseExistingSelected, _gameVersionManager.GamePreset.GameName, _gameVersionManager.GamePreset.ZoneName, - launcherName); + launcherName, + launcherType); if (dialogResult == ContentDialogResult.None) { return -1; // Cancel the installation } + // This will continue to other routines if non-official migration + // is detected. + if (launcherType != MigrateFromLauncherType.Official + && dialogResult == ContentDialogResult.Primary) + { + return 0; + } + + // This will use the "No, Keep Install It" option instead of migrating. + if (launcherType != MigrateFromLauncherType.Official + && dialogResult == ContentDialogResult.Secondary) + { + return 2; + } + if (dialogResult == ContentDialogResult.Primary) { return 1; // Use an existing path or continue to another routine @@ -2894,7 +3086,7 @@ private bool TryGetExistingBHI3LPath(ref string OutputPath) { // Try parsing the config value = Encoding.UTF8.GetString(keyValue); - config = value.Deserialize(InternalAppJSONContext.Default); + config = value.Deserialize(InternalAppJSONContext.Default.BHI3LInfo); } catch (Exception ex) { @@ -2919,7 +3111,8 @@ private bool TryGetExistingBHI3LPath(ref string OutputPath) #nullable disable } - private async Task AskGameFolderDialog() +#nullable enable + private async Task AskGameFolderDialog(Func? checkExistingGameDelegate = null) { // Set initial folder variable as empty string folder = ""; @@ -2945,22 +3138,8 @@ private async Task AskGameFolderDialog() break; // If secondary, then show folder picker dialog to choose the folder case ContentDialogResult.Secondary: - folder = await FileDialogNative.GetFolderPicker(); - - if (!string.IsNullOrEmpty(folder)) - { - // Check for the write permission on the folder - if (ConverterTool.IsUserHasPermission(folder)) - { - isChoosen = true; - } - else - { - // If not, then show the Insufficient access dialog - await Dialog_InsufficientWritePermission(_parentUI, folder); - } - } - + folder = await FileDialogHelper.GetRestrictedFolderPathDialog(Lang._Dialogs.FolderDialogTitle1, checkExistingGameDelegate); + isChoosen = !string.IsNullOrEmpty(folder); break; case ContentDialogResult.None: return null; @@ -2969,6 +3148,7 @@ private async Task AskGameFolderDialog() return folder; } +#nullable restore private async Task GetLatestPackageList(List packageList, GameInstallStateEnum gameState, bool usePreload) @@ -2980,9 +3160,9 @@ private async Task GetLatestPackageList(List packageList, Ga if (gameState != GameInstallStateEnum.InstalledHavePlugin) { // Iterate the package resource version and add it into packageList - foreach (RegionResourceVersion asset in usePreload + foreach (RegionResourceVersion asset in (usePreload ? _gameVersionManager.GetGamePreloadZip() - : _gameVersionManager.GetGameLatestZip(gameState)) + : _gameVersionManager.GetGameLatestZip(gameState))!) { if (asset == null) { @@ -3083,7 +3263,7 @@ protected virtual void TryAddPluginPackage(List assetList) private async ValueTask CheckExistingOrAskFolderDialog() { // Try run the result and if it's null, then return -1 (Cancel the operation) - string result = await AskGameFolderDialog(); + string result = await AskGameFolderDialog(_gameVersionManager.FindGameInstallationPath); if (result == null) { return -1; @@ -3256,9 +3436,9 @@ protected virtual async ValueTask TryAddOtherInstalledVoicePacks( if (TryGetVoiceOverResourceByLocaleCode(packs, localeCode, out RegionResourceVersion outRes)) { // Check if the existing package is already exist or not. - RegionResourceVersion outResDup = - packs.FirstOrDefault(x => x.language != null && - x.language.Equals(outRes.language, + GameInstallPackage outResDup = + packageList.FirstOrDefault(x => x.LanguageID != null && + x.LanguageID.Equals(outRes.language, StringComparison.OrdinalIgnoreCase)); if (outResDup != null) { @@ -3301,15 +3481,7 @@ private void MoveFileToIngredientList(List assetIndex, str { // Get the combined path from the asset name var inputPath = Path.Combine(sourcePath, index.N); - var outputPath = Path.Combine(targetPath, index.N); - var outputFolder = Path.GetDirectoryName(outputPath); - - // Create directory of the output path if not exist - if (!Directory.Exists(outputFolder) - && outputFolder != null) - { - Directory.CreateDirectory(outputFolder); - } + var outputPath = EnsureCreationOfDirectory(Path.Combine(targetPath, index.N)); // Sanity Check: If the file is still missing even after the process, then throw var fileInfo = new FileInfo(inputPath); @@ -3362,7 +3534,8 @@ private void TryRemoveRedundantHDiffList() #region Private Methods - StartPackageDownload - private async ValueTask InvokePackageDownloadRoutine(List packageList, + private async ValueTask InvokePackageDownloadRoutine(DownloadClient downloadClient, + List packageList, CancellationToken token) { // Get the package/segment count @@ -3373,17 +3546,12 @@ private async ValueTask InvokePackageDownloadRoutine(List pa _progressAllCountTotal = packageCount; RestartStopwatch(); - // Initialize new proxy-aware HttpClient - using HttpClient httpClientNew = new HttpClientBuilder() - .UseLauncherConfig() - .SetUserAgent(_userAgent) - .SetAllowedDecompression(DecompressionMethods.None) - .Create(); - - using Http _httpClient = new Http(true, customHttpClient: httpClientNew); + // Initialize a legacy Http as well + using Http _httpClient = new Http(true, customHttpClient: downloadClient.GetHttpClient()); // Subscribe the download progress to the event adapter _httpClient.DownloadProgress += HttpClientDownloadProgressAdapter; + try { // Iterate the package list @@ -3395,7 +3563,7 @@ private async ValueTask InvokePackageDownloadRoutine(List pa // Iterate the segment list for (int i = 0; i < package.Segments.Count; i++) { - await RunPackageDownloadRoutine(package.Segments[i], token, packageCount); + await RunPackageDownloadRoutine(_httpClient, downloadClient, package.Segments[i], token, packageCount); } // Skip action below and continue to the next segment @@ -3403,7 +3571,7 @@ private async ValueTask InvokePackageDownloadRoutine(List pa } // Else, run the routine as normal - await RunPackageDownloadRoutine(package, token, packageCount); + await RunPackageDownloadRoutine(_httpClient, downloadClient, package, token, packageCount); } } finally @@ -3413,8 +3581,11 @@ private async ValueTask InvokePackageDownloadRoutine(List pa } } - private async ValueTask RunPackageDownloadRoutine(GameInstallPackage package, CancellationToken token, - int packageCount) + private async ValueTask RunPackageDownloadRoutine(Http httpClient, + DownloadClient downloadClient, + GameInstallPackage package, + CancellationToken token, + int packageCount) { // Set the activity status _status.IsIncludePerFileIndicator = packageCount > 1; @@ -3423,42 +3594,46 @@ private async ValueTask RunPackageDownloadRoutine(GameInstallPackage package, Ca _progressAllCountTotal)}"; LogWriteLine($"Downloading package URL {_progressAllCountCurrent}/{_progressAllCountTotal} ({ConverterTool.SummarizeSizeSimple(package.Size)}): {package.URL}"); - // Get the directory path - string pathDir = Path.GetDirectoryName(package.PathOutput); - - // If the directory doesn't exist, then create one - if (!Directory.Exists(pathDir) && pathDir != null) - { - Directory.CreateDirectory(pathDir); - } - // If the file exist or package size is unmatched, // then start downloading - long existingPackageFileSize = package.GetStreamLength(_downloadThreadCount); - bool isExistingPackageFileExist = package.IsReadStreamExist(_downloadThreadCount); - - // Initialize new proxy-aware HttpClient - using HttpClient httpClientNew = new HttpClientBuilder() - .UseLauncherConfig() - .SetUserAgent(_userAgent) - .SetAllowedDecompression(DecompressionMethods.None) - .Create(); - - using Http _httpClient = new Http(true, customHttpClient: httpClientNew); + long legacyExistingPackageFileSize = package.GetStreamLength(_downloadThreadCount); + long existingPackageFileSize = package.SizeDownloaded > legacyExistingPackageFileSize ? package.SizeDownloaded : legacyExistingPackageFileSize; + bool isExistingPackageFileExist = package.IsReadStreamExist(_downloadThreadCount); if (!isExistingPackageFileExist || existingPackageFileSize != package.Size) { - // If the package size is more than or equal to 10 MB, then allow to use multi-session. - // Otherwise, forcefully use single-session. - bool isCanMultiSession = package.Size >= 10 << 20; - if (isCanMultiSession) + // Get the file path + string filePath = EnsureCreationOfDirectory(package.PathOutput); + + bool isCanMultiSession = false; + // If a legacy downloader is used, then use the legacy Http downloader + if (package.IsUseLegacyDownloader) { - await _httpClient.Download(package.URL, package.PathOutput, _downloadThreadCount, false, token); + // If the package size is more than or equal to 10 MB, then allow to use multi-session. + // Otherwise, forcefully use single-session. + isCanMultiSession = package.Size >= 10 << 20; + if (isCanMultiSession) + { + await httpClient.Download(package.URL, filePath, _downloadThreadCount, false, token); + } + else + { + await httpClient.Download(package.URL, filePath, false, null, null, token); + } } + // Otherwise, use the new downloder else { - await _httpClient.Download(package.URL, package.PathOutput, false, null, null, token); + // Run the new downloader + await RunDownloadTask( + package.Size, + filePath, + package.URL, + downloadClient, + HttpClientDownloadProgressAdapter, + token, + false); } // Update status to merging @@ -3469,10 +3644,10 @@ private async ValueTask RunPackageDownloadRoutine(GameInstallPackage package, Ca _stopwatch.Stop(); // Check if the merge chunk is enabled and the download could perform multisession, - // then do merge. - if (_canMergeDownloadChunks && isCanMultiSession) + // then do merge (also if legacy downloader is used). + if (_canMergeDownloadChunks && isCanMultiSession && package.IsUseLegacyDownloader) { - await _httpClient.Merge(token); + await httpClient.Merge(token); } _stopwatch.Start(); @@ -3538,11 +3713,21 @@ private void DeleteDownloadedFile(string FileOutput, byte Thread) fileInfo.Delete(); } + // Get the info of the new downloader metadata + FileInfo fileInfoMetadata = new FileInfo(FileOutput + ".collapseMeta"); + + // If metadata file existed, then delete + if (fileInfoMetadata.Exists) + { + fileInfoMetadata.IsReadOnly = false; + fileInfoMetadata.Delete(); + } + // Delete the file of the chunk file too Http.DeleteMultisessionFiles(FileOutput, Thread); } - private long GetExistingDownloadPackageSize(List packageList) + private async ValueTask GetExistingDownloadPackageSize(DownloadClient downloadClient, List packageList, CancellationToken token) { // Initialize total existing size and download thread count long totalSize = 0; @@ -3559,19 +3744,41 @@ private long GetExistingDownloadPackageSize(List packageList Http .CalculateExistingMultisessionFilesWithExpctdSize(packageList[i].Segments[j].PathOutput, _downloadThreadCount, packageList[i].Segments[j].Size); - totalSize += segmentDownloaded; - totalSegmentDownloaded += segmentDownloaded; - packageList[i].Segments[j].SizeDownloaded = segmentDownloaded; + + long newSegmentedDownloaded = await downloadClient.GetDownloadedFileSize( + packageList[i].Segments[j].URL, + packageList[i].Segments[j].PathOutput, + packageList[i].Segments[j].Size, + token + ); + bool isUseLegacySegmentedSize = !LauncherConfig.IsUsePreallocatedDownloader + || segmentDownloaded > newSegmentedDownloaded + || newSegmentedDownloaded > packageList[i].Segments[j].Size; + totalSize += isUseLegacySegmentedSize ? segmentDownloaded : newSegmentedDownloaded; + totalSegmentDownloaded += isUseLegacySegmentedSize ? segmentDownloaded : newSegmentedDownloaded; + packageList[i].Segments[j].SizeDownloaded = isUseLegacySegmentedSize ? segmentDownloaded : newSegmentedDownloaded; + packageList[i].Segments[j].IsUseLegacyDownloader = isUseLegacySegmentedSize; } packageList[i].SizeDownloaded = totalSegmentDownloaded; continue; } - packageList[i].SizeDownloaded = + long legacyDownloadedSize = Http.CalculateExistingMultisessionFilesWithExpctdSize(packageList[i].PathOutput, _downloadThreadCount, packageList[i].Size); - totalSize += packageList[i].SizeDownloaded; + long newDownloaderSize = await downloadClient.GetDownloadedFileSize( + packageList[i].URL, + packageList[i].PathOutput, + packageList[i].Size, + token + ); + bool isUseLegacySize = !LauncherConfig.IsUsePreallocatedDownloader + || legacyDownloadedSize > newDownloaderSize + || newDownloaderSize > packageList[i].Size; + packageList[i].IsUseLegacyDownloader = isUseLegacySize; + packageList[i].SizeDownloaded = isUseLegacySize ? legacyDownloadedSize : newDownloaderSize; + totalSize += packageList[i].SizeDownloaded; } // return totalSize @@ -3639,17 +3846,20 @@ await Dialog_InsufficientDriveSpace(Content, diskFreeSpace, remainedDownloadUnco private async Task GetPackagesRemoteSize(List packageList, CancellationToken token) { - // Iterate and assign the remote size to each package inside the list - for (int i = 0; i < packageList.Count; i++) + // Iterate and assign the remote size to each package inside the list in parallel + await Parallel.ForEachAsync(packageList, new ParallelOptions { - if (packageList[i].Segments != null) + CancellationToken = token + }, async (package, innerToken) => + { + if (package.Segments != null) { - await TryGetSegmentedPackageRemoteSize(packageList[i], token); - continue; + await TryGetSegmentedPackageRemoteSize(package, token); + return; } - await TryGetPackageRemoteSize(packageList[i], token); - } + await TryGetPackageRemoteSize(package, token); + }); } #endregion @@ -3665,12 +3875,18 @@ public void UpdateCompletenessStatus(CompletenessStatus status) _status.IsRunning = true; _status.IsCompleted = false; _status.IsCanceled = false; + #if !DISABLEDISCORD + InnerLauncherConfig.AppDiscordPresence?.SetActivity(ActivityType.Update); + #endif break; case CompletenessStatus.Completed: IsRunning = false; _status.IsRunning = false; _status.IsCompleted = true; _status.IsCanceled = false; + #if !DISABLEDISCORD + InnerLauncherConfig.AppDiscordPresence?.SetActivity(ActivityType.Idle); + #endif // HACK: Fix the progress not achieving 100% while completed _progress.ProgressAllPercentage = 100f; _progress.ProgressPerFilePercentage = 100f; @@ -3680,12 +3896,18 @@ public void UpdateCompletenessStatus(CompletenessStatus status) _status.IsRunning = false; _status.IsCompleted = false; _status.IsCanceled = true; + #if !DISABLEDISCORD + InnerLauncherConfig.AppDiscordPresence?.SetActivity(ActivityType.Idle); + #endif break; case CompletenessStatus.Idle: IsRunning = false; _status.IsRunning = false; _status.IsCompleted = false; _status.IsCanceled = false; + #if !DISABLEDISCORD + InnerLauncherConfig.AppDiscordPresence?.SetActivity(ActivityType.Idle); + #endif break; } @@ -3703,14 +3925,18 @@ protected async Task TryGetPackageRemoteSize(GameInstallPackage asset, Cancellat protected async Task TryGetSegmentedPackageRemoteSize(GameInstallPackage asset, CancellationToken token) { long totalSize = 0; - for (int i = 0; i < asset.Segments.Count; i++) + await Parallel.ForAsync(0, asset.Segments.Count, new ParallelOptions + { + CancellationToken = token + }, + async (i, innerToken) => { long segmentSize = await FallbackCDNUtil.GetContentLength(asset.Segments[i].URL, token); - totalSize += segmentSize; - asset.Segments[i].Size = segmentSize; + totalSize += segmentSize; + asset.Segments[i].Size = segmentSize; LogWriteLine($"Package Segment: [T: {asset.PackageType}] {asset.Segments[i].Name} has {ConverterTool.SummarizeSizeSimple(segmentSize)} in size", LogType.Default, true); - } + }); asset.Size = totalSize; LogWriteLine($"Package Segment (count: {asset.Segments.Count}) has {ConverterTool.SummarizeSizeSimple(asset.Size)} in total size with {ConverterTool.SummarizeSizeSimple(asset.SizeRequired)} of free space required", @@ -3846,6 +4072,63 @@ private long GetLastSize(long input) return a; } + private async void HttpClientDownloadProgressAdapter(int read, DownloadProgress downloadProgress) + { + // Set the progress bar not indetermined + _status.IsProgressPerFileIndetermined = false; + _status.IsProgressAllIndetermined = false; + + // Increment the total current size if status is not merging + Interlocked.Add(ref _progressAllSizeCurrent, read); + // Increment the total last read + Interlocked.Add(ref _progressAllIOReadCurrent, read); + + if (_refreshStopwatch!.ElapsedMilliseconds > _refreshInterval) + { + // Assign local sizes to progress + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + + // Calculate the speed + double speedPerFile = _progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds; + double speedAllFile = (_progressAllSizeCurrent / _stopwatch.Elapsed.TotalSeconds).ClampLimitedSpeedNumber(); + _progress.ProgressAllSpeed = speedPerFile.ClampLimitedSpeedNumber(); + + // Calculate percentage + _progress.ProgressPerFilePercentage = + Math.Round(_progressPerFileSizeCurrent / (double)_progressPerFileSizeTotal * 100, 2); + _progress.ProgressAllPercentage = + Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); + + // Calculate the timelapse + _progress.ProgressAllTimeLeft = + ((_progressAllSizeTotal - _progressAllSizeCurrent) / speedAllFile.Unzeroed()) + .ToTimeSpanNormalized(); + + // Update the status of per file size and current progress from Http client + _progressPerFileSizeCurrent = downloadProgress.BytesDownloaded; + _progressPerFileSizeTotal = downloadProgress.BytesTotal; + _progress.ProgressPerFilePercentage = ConverterTool.GetPercentageNumber(downloadProgress.BytesDownloaded, downloadProgress.BytesTotal); + + lock (_downloadSpeedRefreshStopwatch) + { + if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) + { + _progressAllIOReadCurrent = 0; + _downloadSpeedRefreshStopwatch.Restart(); + } + } + + // Update the status + UpdateAll(); + + _refreshStopwatch.Restart(); + await Task.Delay(_refreshInterval); + } + } + private async void HttpClientDownloadProgressAdapter(object sender, DownloadEvent e) { // Set the progress bar not indetermined @@ -3893,13 +4176,22 @@ private async void HttpClientDownloadProgressAdapter(object sender, DownloadEven // If status is merging, then use progress for speed and timelapse from Http client // and set the rest from the base class _progress.ProgressAllTimeLeft = e.TimeLeft; - _progress.ProgressAllSpeed = e.Speed; + + double speedWithReset = _progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds; + _progress.ProgressAllSpeed = speedWithReset; + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; _progress.ProgressAllSizeTotal = _progressAllSizeTotal; _progress.ProgressAllPercentage = Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); + + if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) + { + _progressAllIOReadCurrent = 0; + _downloadSpeedRefreshStopwatch.Restart(); + } } // Update the status of per file size and current progress from Http client diff --git a/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs b/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs index 2661ef168..a8ab0c68b 100644 --- a/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs +++ b/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs @@ -2,6 +2,7 @@ using CollapseLauncher.Helper.Metadata; using Hi3Helper; using Hi3Helper.Http; +using Hi3Helper.Http.Legacy; using Hi3Helper.Preset; using Hi3Helper.Shared.ClassStruct; using SharpHDiffPatch.Core; @@ -29,7 +30,6 @@ public class GameConversionManagement : IDisposable private PresetConfig SourceProfile, TargetProfile; private List SourceFileManifest; private List TargetFileManifest; - private Http _http; private HttpClient _client; string BaseURL; @@ -49,7 +49,6 @@ internal GameConversionManagement(PresetConfig SourceProfile, PresetConfig Targe .UseLauncherConfig() .SetAllowedDecompression(DecompressionMethods.None) .Create(); - this._http = new Http(this._client); this.SourceProfile = SourceProfile; this.TargetProfile = TargetProfile; this.BaseURL = BaseURL; @@ -64,7 +63,6 @@ internal GameConversionManagement(PresetConfig SourceProfile, PresetConfig Targe public void Dispose() { this._client?.Dispose(); - this._http?.Dispose(); } public async Task StartPreparation() @@ -77,6 +75,8 @@ public async Task StartPreparation() string IngredientsPath = TargetProfile.ActualGameDataLocation + "_Ingredients"; string URL = ""; + DownloadClient downloadClient = DownloadClient.CreateInstance(_client); + try { FallbackCDNUtil.DownloadProgress += FetchIngredientsAPI_Progress; @@ -85,18 +85,18 @@ public async Task StartPreparation() { URL = string.Format(AppGameRepairIndexURLPrefix, SourceProfile.ProfileName, this.GameVersion); ConvertDetail = Lang._InstallConvert.Step2Subtitle; - await FallbackCDNUtil.DownloadCDNFallbackContent(_http, buffer, URL, Token); + await FallbackCDNUtil.DownloadCDNFallbackContent(downloadClient, buffer, URL, Token); buffer.Position = 0; - SourceFileRemote = await buffer.DeserializeAsync>(CoreLibraryJSONContext.Default, Token); + SourceFileRemote = await buffer.DeserializeAsListAsync(CoreLibraryJSONContext.Default.FilePropertiesRemote, Token); } using (MemoryStream buffer = new MemoryStream()) { URL = string.Format(AppGameRepairIndexURLPrefix, TargetProfile.ProfileName, this.GameVersion); ConvertDetail = Lang._InstallConvert.Step2Subtitle; - await FallbackCDNUtil.DownloadCDNFallbackContent(_http, buffer, URL, Token); + await FallbackCDNUtil.DownloadCDNFallbackContent(downloadClient, buffer, URL, Token); buffer.Position = 0; - TargetFileRemote = await buffer.DeserializeAsync>(CoreLibraryJSONContext.Default, Token); + TargetFileRemote = await buffer.DeserializeAsListAsync(CoreLibraryJSONContext.Default.FilePropertiesRemote, Token); } } finally @@ -107,7 +107,7 @@ public async Task StartPreparation() SourceFileManifest = BuildManifest(SourceFileRemote); TargetFileManifest = BuildManifest(TargetFileRemote); await Task.Run(() => PrepareIngredients(SourceFileManifest)); - await RepairIngredients(await VerifyIngredients(SourceFileManifest, IngredientsPath), IngredientsPath); + await RepairIngredients(downloadClient, await VerifyIngredients(SourceFileManifest, IngredientsPath), IngredientsPath); } long MakeIngredientsRead = 0; @@ -148,8 +148,10 @@ private void PrepareIngredients(List FileManifest) public async Task PostConversionVerify() { + DownloadClient downloadClient = DownloadClient.CreateInstance(_client); + string TargetPath = TargetProfile.ActualGameDataLocation; - await RepairIngredients(await VerifyIngredients(TargetFileManifest, TargetPath), TargetPath); + await RepairIngredients(downloadClient, await VerifyIngredients(TargetFileManifest, TargetPath), TargetPath); } private async Task> VerifyIngredients(List FileManifest, string GamePath) @@ -213,7 +215,7 @@ private List BuildManifest(List FileRemote }); } break; - case FileType.Blocks: + case FileType.Block: { _out.AddRange(BuildBlockManifest(Entry.BlkC, Entry.N)); } @@ -245,45 +247,37 @@ private List BuildBlockManifest(List BlockC, strin long RepairRead = 0; long RepairTotalSize = 0; - private async Task RepairIngredients(List BrokenFile, string GamePath) + private async Task RepairIngredients(DownloadClient downloadClient, List BrokenFile, string GamePath) { if (BrokenFile.Count == 0) return; ResetSw(); - string OutputPath; - string InputURL; RepairTotalSize = BrokenFile.Sum(x => x.FileSize); ConvertStatus = Lang._InstallConvert.Step3Title1; - foreach (FileProperties Entry in BrokenFile) + await Parallel.ForEachAsync(BrokenFile, new ParallelOptions + { + MaxDegreeOfParallelism = DownloadThread, + CancellationToken = Token + }, async (Entry, CoopToken) => { Token.ThrowIfCancellationRequested(); - OutputPath = Path.Combine(GamePath, Entry.FileName); - InputURL = CombineURLFromString(BaseURL, Entry.FileName); - ConvertDetail = string.Format("{0}: {1}", Lang._Misc.Downloading, string.Format(Lang._Misc.PerFromTo, Entry.FileName, Entry.FileSizeStr)); - if (!Directory.Exists(Path.GetDirectoryName(OutputPath))) - Directory.CreateDirectory(Path.GetDirectoryName(OutputPath)); + string OutputPath = Path.Combine(GamePath, Entry.FileName); + string OutputPathDir = Path.GetDirectoryName(OutputPath); + string InputURL = CombineURLFromString(BaseURL, Entry.FileName); - if (File.Exists(OutputPath)) - File.Delete(OutputPath); + ConvertDetail = string.Format("{0}: {1}", Lang._Misc.Downloading, string.Format(Lang._Misc.PerFromTo, Entry.FileName, Entry.FileSizeStr)); + if (!Directory.Exists(OutputPathDir)) + Directory.CreateDirectory(OutputPathDir!); - _http.DownloadProgress += RepairIngredients_Progress; - if (Entry.FileSize >= 20 << 20) - { - await _http.Download(InputURL, OutputPath, DownloadThread, true, Token); - await _http.Merge(Token); - } - else - await _http.Download(InputURL, new FileStream(OutputPath, FileMode.Create, FileAccess.Write), null, null, Token); - _http.DownloadProgress -= RepairIngredients_Progress; - } + await downloadClient.DownloadAsync(InputURL, OutputPath, true, progressDelegateAsync: RepairIngredients_Progress, maxConnectionSessions: DownloadThread, cancelToken: CoopToken); + }); } - private void RepairIngredients_Progress(object sender, DownloadEvent e) + private void RepairIngredients_Progress(int read, DownloadProgress downloadProgress) { - if (_http.DownloadState != DownloadState.Merging) - RepairRead += e.Read; + Interlocked.Add(ref RepairRead, read); UpdateProgress(RepairRead, RepairTotalSize, 1, 1, ConvertSw.Elapsed, ConvertStatus, ConvertDetail); diff --git a/CollapseLauncher/Classes/InstallManagement/Genshin/GenshinInstall.cs b/CollapseLauncher/Classes/InstallManagement/Genshin/GenshinInstall.cs index 5c00e841e..2d6d54850 100644 --- a/CollapseLauncher/Classes/InstallManagement/Genshin/GenshinInstall.cs +++ b/CollapseLauncher/Classes/InstallManagement/Genshin/GenshinInstall.cs @@ -70,10 +70,11 @@ public GenshinInstall(UIElement parentUI, IGameVersionCheck GameVersionManager) #region Public Methods +#nullable enable public override async ValueTask IsPreloadCompleted(CancellationToken token) { // Get the primary file first check - List resource = _gameVersionManager.GetGamePreloadZip(); + List? resource = _gameVersionManager.GetGamePreloadZip(); // Sanity Check: throw if resource returns null if (resource == null) @@ -84,8 +85,8 @@ public override async ValueTask IsPreloadCompleted(CancellationToken token bool primaryAsset = resource.All(x => { - string name = Path.GetFileName(x.path); - if (name == null) + string? name = Path.GetFileName(x.path); + if (string.IsNullOrEmpty(name)) return false; string path = Path.Combine(_gamePath, name); @@ -104,7 +105,7 @@ await TryAddOtherInstalledVoicePacks(resource.FirstOrDefault()?.voice_packs, voi return (primaryAsset && secondaryAsset) || await base.IsPreloadCompleted(token); } - +#nullable restore #endregion #region Override Methods - StartPackageInstallationInner @@ -124,7 +125,7 @@ protected override async Task StartPackageInstallationInner(List StartPackageVerification(List StartPackageVerification(List + new HonkaiRepair(_parentUI, + _gameVersionManager, + _gameCacheManager, _gameSettings, + true, + versionString); +#nullable restore + protected override async Task StartPackageInstallationInner(List gamePackage = null, bool isOnlyInstallPackage = false, bool doNotDeleteZipExplicit = false) @@ -264,6 +268,14 @@ protected override bool IsCategorizedAsGameFile(FileInfo fileInfo, string gamePa { return true; } + + // 9th check: If ACE-BASE.sys is detected + // AND game state is installed. + if (gameState == GameInstallStateEnum.Installed && + localFileInfo.RelativePath.EndsWith("ACE-BASE.sys", StringComparison.OrdinalIgnoreCase)) + { + return true; + } // If all those matches failed, then return them as a non-game file return false; diff --git a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs index 24b4f230a..eaa8b64da 100644 --- a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs +++ b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs @@ -76,10 +76,7 @@ public override async ValueTask StartPackageVerification(List StartPackageVerification(List + new StarRailRepair(_parentUI, + _gameVersionManager, true, + versionString); +#nullable restore + protected override async Task StartPackageInstallationInner(List gamePackage = null, bool isOnlyInstallPackage = false, bool doNotDeleteZipExplicit = false) @@ -96,9 +100,9 @@ protected override async Task StartPackageInstallationInner(List ZenlessSettings?.GeneralData?.DeviceLanguageVoiceType switch + { + LanguageVoice.zh_cn => 0, + LanguageVoice.en_us => 1, + LanguageVoice.ja_jp => 2, + LanguageVoice.ko_kr => 3, + LanguageVoice.Unset => 2, // Default to ja_jp + _ => throw new NotSupportedException("Type of the language voice type is not valid!") + }; + } + + protected override string? _gameAudioLangListPath + { + get + { + // If the persistent folder is not exist, then return null + if (!Directory.Exists(_gameDataPersistentPath)) + return null; + + // Get the audio lang path index + string audioLangPath = _gameAudioLangListPathStatic; + return File.Exists(audioLangPath) ? audioLangPath : null; + } + } + protected override string _gameAudioLangListPathStatic => + Path.Combine(_gameDataPersistentPath, "audio_lang_launcher"); + private string _gameAudioLangListPathAlternateStatic => + Path.Combine(_gameDataPersistentPath, "audio_lang"); #endregion - public ZenlessInstall(UIElement parentUI, IGameVersionCheck GameVersionManager) + public ZenlessInstall(UIElement parentUI, IGameVersionCheck GameVersionManager, ZenlessSettings zenlessSettings) : base(parentUI, GameVersionManager) { + ZenlessSettings = zenlessSettings; } - #region Override Methods - UninstallGame + #region Override Methods - StartPackageInstallationInner + + public override async ValueTask StartPackageVerification(List gamePackage) + { + IsRunning = true; + // Get the delta patch confirmation if the property is not null + if (_gameDeltaPatchProperty == null) + return await base.StartPackageVerification(gamePackage); + + // If the confirm is 1 (verified) or -1 (cancelled), then return the code + int deltaPatchConfirm = await ConfirmDeltaPatchDialog(_gameDeltaPatchProperty, + ZenlessGameRepairManager = GetGameRepairInstance(_gameDeltaPatchProperty.SourceVer) as ZenlessRepair); + if (deltaPatchConfirm is -1 or 1) + { + return deltaPatchConfirm; + } + + // If no delta patch is happening as deltaPatchConfirm returns 0 (normal update), then do the base verification + return await base.StartPackageVerification(gamePackage); + } + + protected override IRepair GetGameRepairInstance(string? versionString) => + new ZenlessRepair(_parentUI, + _gameVersionManager, ZenlessSettings!, true, + versionString); + + protected override async Task StartPackageInstallationInner(List? gamePackage = null, + bool isOnlyInstallPackage = false, + bool doNotDeleteZipExplicit = false) + { + // If the delta patch is performed, then return + if (!isOnlyInstallPackage && await StartDeltaPatch(ZenlessGameRepairManager, false, true)) + { + // Assign the game package to delta-patch requirement list + // and start the additional patching process (like Audio patch, etc) + gamePackage ??= _gameDeltaPatchPreReqList; + } + + // Run the base installation process + await base.StartPackageInstallationInner(gamePackage, isOnlyInstallPackage, doNotDeleteZipExplicit); + + // Then start on processing hdifffiles list and deletefiles list + await ApplyHdiffListPatch(); + await ApplyDeleteFileActionAsync(_token.Token); + + // Update the audio lang list if not in isOnlyInstallPackage mode + if (!isOnlyInstallPackage) + { + WriteAudioLangList(_assetIndex); + } + } + #endregion + + #region Override Methods - Audio Lang List + protected override void WriteAudioLangList(List gamePackage) + { + // Run the writing method from the base first + base.WriteAudioLangList(gamePackage); + + // Then create the one from the alternate one + // Read all the existing list + List langList = File.Exists(_gameAudioLangListPathAlternateStatic) + ? File.ReadAllLines(_gameAudioLangListPathAlternateStatic).ToList() + : []; + + // Try lookup if there is a new language list, then add it to the list + foreach (GameInstallPackage package in + _assetIndex.Where(x => x.PackageType == GameInstallPackageType.Audio)) + { + string langString = GetLanguageStringByLocaleCodeAlternate(package.LanguageID); + if (!langList.Contains(langString, StringComparer.OrdinalIgnoreCase)) + { + langList.Add(langString); + } + } + + // Create the audio lang list file + using StreamWriter sw = new StreamWriter(_gameAudioLangListPathAlternateStatic, + new FileStreamOptions + { Mode = FileMode.Create, Access = FileAccess.Write }); + // Iterate the package list + foreach (string langString in langList) + { + // Write the language string as per ID + sw.WriteLine(langString); + } + } + + protected override void WriteAudioLangListSophon(List sophonVOList) + { + // Run the writing method from the base first + base.WriteAudioLangListSophon(sophonVOList); + + // Then create the one from the alternate one + // Read all the existing list + List langList = File.Exists(_gameAudioLangListPathAlternateStatic) + ? File.ReadAllLines(_gameAudioLangListPathAlternateStatic).ToList() + : []; + + // Try lookup if there is a new language list, then add it to the list + for (int index = 0; index < sophonVOList.Count; index++) + { + var packageLocaleCodeString = sophonVOList[index]; + string langString = GetLanguageStringByLocaleCodeAlternate(packageLocaleCodeString); + if (!langList.Contains(langString, StringComparer.OrdinalIgnoreCase)) + { + langList.Add(langString); + } + } + + // Create the audio lang list file + using var sw = new StreamWriter(_gameAudioLangListPathAlternateStatic, + new FileStreamOptions + { Mode = FileMode.Create, Access = FileAccess.Write }); + // Iterate the package list + foreach (var voIds in langList) + // Write the language string as per ID + { + sw.WriteLine(voIds); + } + } + + private string GetLanguageStringByLocaleCodeAlternate(string localeCode) + { + return localeCode switch + { + "zh-cn" => "Cn", + "en-us" => "En", + "ja-jp" => "Jp", + "ko-kr" => "Kr", + _ => throw new KeyNotFoundException($"Alternate locale code: {localeCode} is not supported!") + }; + } + #endregion + + #region Override Methods - UninstallGame protected override UninstallGameProperty AssignUninstallFolders() { return new UninstallGameProperty @@ -40,7 +219,6 @@ protected override UninstallGameProperty AssignUninstallFolders() foldersToKeepInData = ["ScreenShots"] }; } - #endregion } } \ No newline at end of file diff --git a/CollapseLauncher/Classes/Interfaces/Class/CommunityToolsProperty.cs b/CollapseLauncher/Classes/Interfaces/Class/CommunityToolsProperty.cs index 355c39ddd..4fb8f21d3 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/CommunityToolsProperty.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/CommunityToolsProperty.cs @@ -1,4 +1,5 @@ -using CollapseLauncher.Helper.Metadata; +using CollapseLauncher.Extension; +using CollapseLauncher.Helper.Metadata; using Hi3Helper; using System; using System.Collections.Generic; @@ -35,7 +36,10 @@ public static async Task LoadCommunityTools(Stream fileS { try { - return await fileStream.DeserializeAsync(InternalAppJSONContext.Default); + CommunityToolsProperty communityToolkitProperty = await fileStream.DeserializeAsync(InternalAppJSONContext.Default.CommunityToolsProperty); + ResolveCommunityToolkitFontAwesomeGlyph(communityToolkitProperty.OfficialToolsDictionary); + ResolveCommunityToolkitFontAwesomeGlyph(communityToolkitProperty.CommunityToolsDictionary); + return communityToolkitProperty; } catch (Exception ex) { @@ -43,6 +47,55 @@ public static async Task LoadCommunityTools(Stream fileS return new CommunityToolsProperty(); } } + + private static void ResolveCommunityToolkitFontAwesomeGlyph(Dictionary> dictionary) + { + // Get font paths + string fontAwesomeSolidPath = FontCollections.FontAwesomeSolid.Source; + string fontAwesomeRegularPath = FontCollections.FontAwesomeRegular.Source; + string fontAwesomeBrandPath = FontCollections.FontAwesomeBrand.Source; + + // Enumerate key pairs + foreach (KeyValuePair> keyPair in dictionary) + { + // Skip if value list is null or empty + if ((keyPair.Value?.Count ?? 0) == 0) + continue; + + // Enumerate list + foreach (CommunityToolsEntry entry in keyPair.Value) + { + // Get the last index of font namespace. If none was found, then skip + int lastIndexOfNamespace = entry.IconFontFamily.LastIndexOf("#"); + if (lastIndexOfNamespace == -1) + continue; + + // Get the font path as its base only + ReadOnlySpan currentEntryFontPath = entry.IconFontFamily.AsSpan(0, lastIndexOfNamespace).TrimEnd('/'); + + // Check if the path has Solid font-family. + if (fontAwesomeSolidPath.AsSpan().StartsWith(currentEntryFontPath, StringComparison.OrdinalIgnoreCase)) + { + entry.IconFontFamily = fontAwesomeSolidPath; + continue; + } + + // Check if the path has Regular font-family + if (fontAwesomeRegularPath.AsSpan().StartsWith(currentEntryFontPath, StringComparison.OrdinalIgnoreCase)) + { + entry.IconFontFamily = fontAwesomeRegularPath; + continue; + } + + // Check if the path has Brands font-family + if (fontAwesomeBrandPath.AsSpan().StartsWith(currentEntryFontPath, StringComparison.OrdinalIgnoreCase)) + { + entry.IconFontFamily = fontAwesomeBrandPath; + continue; + } + } + } + } } public class CommunityToolsEntry diff --git a/CollapseLauncher/Classes/Interfaces/Class/Enums.cs b/CollapseLauncher/Classes/Interfaces/Class/Enums.cs index 9c8033fd8..df3d3a116 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/Enums.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/Enums.cs @@ -2,7 +2,7 @@ { internal enum RepairAssetType { - General, + Generic, Block, BlockUpdate, Audio, diff --git a/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs b/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs index 1348a80b5..f27772f41 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs @@ -1,5 +1,6 @@ using CollapseLauncher.Extension; using Microsoft.UI.Xaml; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -21,33 +22,48 @@ public GamePropertyBase(UIElement parentUI, IGameVersionCheck gameVersionManager public GamePropertyBase(UIElement parentUI, IGameVersionCheck gameVersionManager, string gamePath, string gameRepoURL, string versionOverride) { _gameVersionManager = gameVersionManager; - _parentUI = parentUI; - _gamePathField = gamePath; - _gameRepoURL = gameRepoURL; - _token = new CancellationTokenSourceWrapper(); - AssetEntry = new ObservableCollection(); + _parentUI = parentUI; + _gamePathField = gamePath; + _gameRepoURL = gameRepoURL; + _token = new CancellationTokenSourceWrapper(); + AssetEntry = new ObservableCollection(); + _isVersionOverride = versionOverride != null; // If the version override is not null, then assign the override value - if (_isVersionOverride = versionOverride != null) + if (_isVersionOverride) { _gameVersionOverride = new GameVersion(versionOverride); } } - protected const int _bufferLength = 4 << 10; - protected const int _bufferMediumLength = 512 << 10; - protected const int _bufferBigLength = 1 << 20; + protected const int _bufferLength = 4 << 10; // 4 KiB + protected const int _bufferMediumLength = 4 << 17; // 512 KiB + protected const int _bufferBigLength = 1 << 20; // 1 MiB protected const int _sizeForMultiDownload = 10 << 20; + protected const int _downloadThreadCountReserved = 16; protected virtual string _userAgent { get; set; } = "UnityPlayer/2017.4.18f1 (UnityWebRequest/1.0, libcurl/7.51.0-DEV)"; protected bool _isVersionOverride { get; init; } + protected bool _isBurstDownloadEnabled { get => IsBurstDownloadModeEnabled; } protected byte _downloadThreadCount { get => (byte)AppCurrentDownloadThread; } protected byte _threadCount { get => (byte)AppCurrentThread; } + protected int _downloadThreadCountSqrt { get => (int)Math.Max(Math.Sqrt(_downloadThreadCount), 4); } protected CancellationTokenSourceWrapper _token { get; set; } protected Stopwatch _stopwatch { get; set; } protected Stopwatch _refreshStopwatch { get; set; } protected Stopwatch _downloadSpeedRefreshStopwatch { get; set; } - protected GameVersion _gameVersion { get => _isVersionOverride ? _gameVersionOverride : _gameVersionManager.GetGameExistingVersion().Value; } + protected GameVersion _gameVersion + { + get + { + if (_gameVersionManager != null && _isVersionOverride) + { + return _gameVersionOverride; + } + return _gameVersionManager?.GetGameExistingVersion() ?? throw new NullReferenceException(); + } + } + protected IGameVersionCheck _gameVersionManager { get; set; } protected IGameSettings _gameSettings { get; set; } protected string _gamePath { get => string.IsNullOrEmpty(_gamePathField) ? _gameVersionManager.GameDirPath : _gamePathField; } diff --git a/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelper.cs b/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelper.cs index 6214d4e68..609b0e62e 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelper.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelper.cs @@ -5,7 +5,6 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; #nullable enable @@ -46,52 +45,52 @@ internal static partial class JSONSerializerHelper private static readonly Utf8JsonWriter jsonWriter = new Utf8JsonWriter(jsonBufferWriter, jsonWriterOptions); private static readonly Utf8JsonWriter jsonWriterIndented = new Utf8JsonWriter(jsonBufferWriter, jsonWriterOptionsIndented); - internal static T? Deserialize(this string data, JsonSerializerContext context, T? defaultType = null) - where T : class => InnerDeserialize(data, context, defaultType); + internal static T? Deserialize(this string data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : class => InnerDeserialize(data, typeInfo, defaultType); - internal static T? Deserialize(this string data, JsonSerializerContext context, T? defaultType = null) - where T : struct => InnerDeserialize(data, context, defaultType); + internal static T? Deserialize(this string data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : struct => InnerDeserialize(data, typeInfo, defaultType); internal static JsonNode? DeserializeAsJsonNode(this string data) => InnerDeserializeAsJsonNode(data); - internal static T? Deserialize(this ReadOnlySpan data, JsonSerializerContext context, T? defaultType = null) - where T : class => InnerDeserialize(data, context, defaultType); + internal static T? Deserialize(this ReadOnlySpan data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : class => InnerDeserialize(data, typeInfo, defaultType); - internal static T? Deserialize(this ReadOnlySpan data, JsonSerializerContext context, T? defaultType = null) - where T : struct => InnerDeserialize(data, context, defaultType); + internal static T? Deserialize(this ReadOnlySpan data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : struct => InnerDeserialize(data, typeInfo, defaultType); internal static JsonNode? DeserializeAsJsonNode(this ReadOnlySpan data) => InnerDeserializeAsJsonNode(data); - internal static T? Deserialize(this ReadOnlySpan data, JsonSerializerContext context, T? defaultType = null) - where T : class => InnerDeserialize(data, context, defaultType); + internal static T? Deserialize(this ReadOnlySpan data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : class => InnerDeserialize(data, typeInfo, defaultType); - internal static T? Deserialize(this ReadOnlySpan data, JsonSerializerContext context, T? defaultType = null) - where T : struct => InnerDeserialize(data, context, defaultType); + internal static T? Deserialize(this ReadOnlySpan data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : struct => InnerDeserialize(data, typeInfo, defaultType); internal static JsonNode? DeserializeAsJsonNode(this ReadOnlySpan data) => InnerDeserializeAsJsonNode(data); - internal static T? Deserialize(this Stream data, JsonSerializerContext context, T? defaultType = null) - where T : class => InnerDeserializeStream(data, context, defaultType); + internal static T? Deserialize(this Stream data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : class => InnerDeserializeStream(data, typeInfo, defaultType); - internal static T? Deserialize(this Stream data, JsonSerializerContext context, T? defaultType = null) - where T : struct => InnerDeserializeStream(data, context, defaultType); + internal static T? Deserialize(this Stream data, JsonTypeInfo typeInfo, T? defaultType = null) + where T : struct => InnerDeserializeStream(data, typeInfo, defaultType); internal static JsonNode? DeserializeAsJsonNode(this Stream data) => InnerDeserializeStreamAsJsonNode(data); - private static T? InnerDeserializeStream(Stream data, JsonSerializerContext context, T? defaultType) + private static T? InnerDeserializeStream(Stream data, JsonTypeInfo typeInfo, T? defaultType) { // Check if the data length is 0, then return default value if (data.Length == 0) return defaultType ?? default; // Try deserialize. If it returns a null, then return the default value - return (T?)JsonSerializer.Deserialize(data, typeof(T), context) ?? defaultType ?? default; + return JsonSerializer.Deserialize(data, typeInfo) ?? defaultType ?? default; } - private static T? InnerDeserialize(ReadOnlySpan data, JsonSerializerContext context, T? defaultType) + private static T? InnerDeserialize(ReadOnlySpan data, JsonTypeInfo typeInfo, T? defaultType) { // Check if the data length is less than 2 bytes (assuming the buffer is "{}"), then return default value if (data.Length <= MIN_ALLOWED_CHAR_LENGTH) return defaultType ?? default; @@ -109,7 +108,7 @@ internal static partial class JSONSerializerHelper // Convert the char[] buffer into byte[] int bufferWritten = Encoding.UTF8.GetBytes(dataTrimmed, tempBuffer); // Start deserialize and return - return InnerDeserialize(tempBuffer.AsSpan(0, bufferWritten), context, defaultType); + return InnerDeserialize(tempBuffer.AsSpan(0, bufferWritten), typeInfo, defaultType); } finally { @@ -118,7 +117,7 @@ internal static partial class JSONSerializerHelper } } - private static T? InnerDeserialize(ReadOnlySpan data, JsonSerializerContext context, T? defaultType) + private static T? InnerDeserialize(ReadOnlySpan data, JsonTypeInfo typeInfo, T? defaultType) { // Check if the data length is less than 2 bytes (assuming the buffer is "{}"), then return default value if (data.Length <= MIN_ALLOWED_CHAR_LENGTH) return defaultType ?? default; @@ -128,7 +127,6 @@ internal static partial class JSONSerializerHelper Utf8JsonReader jsonReader = new Utf8JsonReader(dataTrimmed, jsonReaderOptions); // Try deserialize. If it returns a null, then return the default value - JsonTypeInfo typeInfo = (JsonTypeInfo?)context.GetTypeInfo(typeof(T)) ?? throw new NullReferenceException($"The type info of {typeof(T)} is null!"); return JsonSerializer.Deserialize(ref jsonReader, typeInfo) ?? defaultType ?? default; } @@ -181,13 +179,13 @@ internal static partial class JSONSerializerHelper return JsonNode.Parse(ref jsonReader, jsonNodeOptions); } - internal static string Serialize(this T? value, JsonSerializerContext context, bool isIncludeNullEndChar = true, bool isWriteIndented = false) - => InnerSerialize(value, context, isIncludeNullEndChar, isWriteIndented); + internal static string Serialize(this T? value, JsonTypeInfo typeInfo, bool isIncludeNullEndChar = true, bool isWriteIndented = false) + => InnerSerialize(value, typeInfo, isIncludeNullEndChar, isWriteIndented); - internal static string SerializeJsonNode(this JsonNode? node, JsonSerializerContext context, bool isIncludeNullEndChar = true, bool isWriteIndented = false) - => InnerSerializeJsonNode(node, context, isIncludeNullEndChar, isWriteIndented); + internal static string SerializeJsonNode(this JsonNode? node, JsonTypeInfo typeInfo, bool isIncludeNullEndChar = true, bool isWriteIndented = false) + => InnerSerializeJsonNode(node, typeInfo, isIncludeNullEndChar, isWriteIndented); - private static string InnerSerialize(this T? data, JsonSerializerContext context, bool isIncludeNullEndChar, bool isWriteIndented) + private static string InnerSerialize(this T? data, JsonTypeInfo typeInfo, bool isIncludeNullEndChar, bool isWriteIndented) { const string _defaultValue = "{}"; // Check if the data is null, then return default value @@ -212,13 +210,10 @@ private static string InnerSerialize(this T? data, JsonSerializerContext cont // Lock the writer lock (writer) { - // Try get the JsonTypeInfo - JsonTypeInfo typeInfo = (JsonTypeInfo?)context.GetTypeInfo(typeof(T)) ?? throw new NullReferenceException($"The type info of {typeof(T)} is null!"); - // Try serialize the type into JSON string JsonSerializer.Serialize(writer, data, typeInfo); - // Flush the writter + // Flush the writer writer.Flush(); // Write the buffer to string @@ -232,7 +227,7 @@ private static string InnerSerialize(this T? data, JsonSerializerContext cont } } - private static string InnerSerializeJsonNode(this JsonNode? node, JsonSerializerContext context, bool isIncludeNullEndChar, bool isWriteIndented) + private static string InnerSerializeJsonNode(this JsonNode? node, JsonTypeInfo typeInfo, bool isIncludeNullEndChar, bool isWriteIndented) { const string _defaultValue = "{}"; // Check if the node is null, then return default value @@ -257,13 +252,10 @@ private static string InnerSerializeJsonNode(this JsonNode? node, JsonSerializer // Lock the writer lock (writer) { - // Try get the JsonSerializerOptions - JsonSerializerOptions jsonOptions = context.Options; - // Try serialize the JSON Node into JSON string - node.WriteTo(writer, jsonOptions); + node.WriteTo(writer, typeInfo.Options); - // Flush the writter + // Flush the writer writer.Flush(); // Write the buffer to string diff --git a/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelperAsync.cs b/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelperAsync.cs index f902b1f53..53b623205 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelperAsync.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/JSONSerializerHelperAsync.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; @@ -12,20 +12,39 @@ namespace CollapseLauncher { internal static partial class JSONSerializerHelper { - internal static async ValueTask DeserializeAsync(this Stream data, JsonSerializerContext context, CancellationToken token = default, T? defaultType = null) - where T : class => await InnerDeserializeStreamAsync(data, context, token, defaultType); + internal static async ValueTask DeserializeAsync(this Stream data, JsonTypeInfo typeInfo, CancellationToken token = default, T? defaultType = null) + where T : class => await InnerDeserializeStreamAsync(data, typeInfo, token, defaultType); - internal static async ValueTask DeserializeAsync(this Stream data, JsonSerializerContext context, CancellationToken token = default, T? defaultType = null) - where T : struct => await InnerDeserializeStreamAsync(data, context, token, defaultType); + internal static async ValueTask DeserializeAsync(this Stream data, JsonTypeInfo typeInfo, CancellationToken token = default, T? defaultType = null) + where T : struct => await InnerDeserializeStreamAsync(data, typeInfo, token, defaultType); internal static async Task DeserializeAsNodeAsync(this Stream data, CancellationToken token = default) => await InnerDeserializeStreamAsNodeAsync(data, token); - private static async ValueTask InnerDeserializeStreamAsync(Stream data, JsonSerializerContext context, CancellationToken token, T? defaultType) => + internal static IAsyncEnumerable DeserializeAsEnumerable(this Stream data, JsonTypeInfo typeInfo, CancellationToken token = default) + => JsonSerializer.DeserializeAsyncEnumerable(data, typeInfo, token); + + internal static async Task> DeserializeAsListAsync(this Stream data, JsonTypeInfo typeInfo, CancellationToken token = default) + { + // Create List of T + List listItem = new List(); + + // Enumerate in async + await foreach (T? item in data.DeserializeAsEnumerable(typeInfo, token)) + { + // Add an item to List + listItem.Add(item); + } + + // Return the list + return listItem; + } + + private static async ValueTask InnerDeserializeStreamAsync(Stream data, JsonTypeInfo typeInfo, CancellationToken token, T? defaultType) => // Check if the data cannot be read, then throw !data.CanRead ? throw new NotSupportedException("Stream is not readable! Cannot deserialize the stream to JSON!") : // Try deserialize. If it returns a null, then return the default value - (T?)await JsonSerializer.DeserializeAsync(data, typeof(T), context, token) ?? defaultType; + await JsonSerializer.DeserializeAsync(data, typeInfo, token) ?? defaultType; private static async Task InnerDeserializeStreamAsNodeAsync(Stream data, CancellationToken token) => // Check if the data cannot be read, then throw @@ -33,18 +52,17 @@ internal static partial class JSONSerializerHelper // Try deserialize to JSON Node await JsonNode.ParseAsync(data, jsonNodeOptions, jsonDocumentOptions, token); - internal static async ValueTask SerializeAsync(this T? value, Stream targetStream, JsonSerializerContext context, CancellationToken token = default) - where T : class => await InnerSerializeStreamAsync(value, targetStream, context, token); + internal static async ValueTask SerializeAsync(this T? value, Stream targetStream, JsonTypeInfo typeInfo, CancellationToken token = default) + where T : class => await InnerSerializeStreamAsync(value, targetStream, typeInfo, token); - internal static async ValueTask SerializeAsync(this T? value, Stream targetStream, JsonSerializerContext context, CancellationToken token = default) - where T : struct => await InnerSerializeStreamAsync(value, targetStream, context, token); + internal static async ValueTask SerializeAsync(this T? value, Stream targetStream, JsonTypeInfo typeInfo, CancellationToken token = default) + where T : struct => await InnerSerializeStreamAsync(value, targetStream, typeInfo, token); - private static async ValueTask InnerSerializeStreamAsync(this T? value, Stream targetStream, JsonSerializerContext context, CancellationToken token) + private static async ValueTask InnerSerializeStreamAsync(this T? value, Stream targetStream, JsonTypeInfo typeInfo, CancellationToken token) { if (!targetStream.CanWrite) throw new NotSupportedException("Stream is not writeable! Cannot serialize the object into Stream!"); - JsonTypeInfo? typeInfo = (JsonTypeInfo?)context.GetTypeInfo(typeof(T)); if (typeInfo == null) throw new NotSupportedException($"Context does not contain a type info of type {typeof(T).Name}!"); diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index 552d62d6a..9144141bb 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -26,15 +26,16 @@ using static Hi3Helper.Locale; using static Hi3Helper.Logger; +#nullable enable namespace CollapseLauncher.Interfaces { internal class ProgressBase : GamePropertyBase where T1 : IAssetIndexSummary { - public ProgressBase(UIElement parentUI, IGameVersionCheck GameVersionManager, IGameSettings GameSettings, string gamePath, string gameRepoURL, string versionOverride) - : base(parentUI, GameVersionManager, GameSettings, gamePath, gameRepoURL, versionOverride) => Init(); + public ProgressBase(UIElement parentUI, IGameVersionCheck gameVersionManager, IGameSettings gameSettings, string? gamePath, string? gameRepoURL, string? versionOverride) + : base(parentUI, gameVersionManager, gameSettings, gamePath, gameRepoURL, versionOverride) => Init(); - public ProgressBase(UIElement parentUI, IGameVersionCheck GameVersionManager, string gamePath, string gameRepoURL, string versionOverride) - : base(parentUI, GameVersionManager, gamePath, gameRepoURL, versionOverride) => Init(); + public ProgressBase(UIElement parentUI, IGameVersionCheck gameVersionManager, string? gamePath, string? gameRepoURL, string? versionOverride) + : base(parentUI, gameVersionManager, gamePath, gameRepoURL, versionOverride) => Init(); private void Init() { @@ -48,25 +49,25 @@ private void Init() _assetIndex = new List(); } - private object _objLock = new object(); + private readonly Lock _objLock = new(); - public event EventHandler ProgressChanged; - public event EventHandler StatusChanged; + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; - protected TotalPerfileStatus _sophonStatus; - protected TotalPerfileProgress _sophonProgress; - protected TotalPerfileStatus _status; - protected TotalPerfileProgress _progress; + protected TotalPerfileStatus? _sophonStatus; + protected TotalPerfileProgress? _sophonProgress; + protected TotalPerfileStatus? _status; + protected TotalPerfileProgress? _progress; protected int _progressAllCountCurrent; protected int _progressAllCountFound; protected int _progressAllCountTotal; protected long _progressAllSizeCurrent; protected long _progressAllSizeFound; protected long _progressAllSizeTotal; - protected double _progressAllIOReadCurrent; + protected long _progressAllIOReadCurrent; protected long _progressPerFileSizeCurrent; protected long _progressPerFileSizeTotal; - protected double _progressPerFileIOReadCurrent; + protected long _progressPerFileIOReadCurrent; // Extension for IGameInstallManager @@ -76,65 +77,73 @@ private void Init() protected bool _isSophonInUpdateMode { get; set; } #region ProgressEventHandlers - Fetch - protected void _innerObject_ProgressAdapter(object sender, TotalPerfileProgress e) => ProgressChanged?.Invoke(sender, e); - protected void _innerObject_StatusAdapter(object sender, TotalPerfileStatus e) => StatusChanged?.Invoke(sender, e); + protected void _innerObject_ProgressAdapter(object? sender, TotalPerfileProgress e) => ProgressChanged?.Invoke(sender, e); + protected void _innerObject_StatusAdapter(object? sender, TotalPerfileStatus e) => StatusChanged?.Invoke(sender, e); - protected virtual void _httpClient_FetchAssetProgress(object sender, DownloadEvent e) + protected virtual async void _httpClient_FetchAssetProgress(int size, DownloadProgress downloadProgress) { - lock (_status!) + if (await CheckIfNeedRefreshStopwatch()) { - // Update fetch status - _status.IsProgressPerFileIndetermined = false; - _status.IsProgressAllIndetermined = false; - _status.ActivityPerFile = string.Format(Lang!._GameRepairPage!.PerProgressSubtitle3!, ConverterTool.SummarizeSizeSimple(e!.Speed)); - } + double speed = (downloadProgress.BytesDownloaded / _stopwatch.Elapsed.TotalSeconds).ClampLimitedSpeedNumber(); + TimeSpan timeLeftSpan = ((downloadProgress.BytesTotal - downloadProgress.BytesDownloaded) / speed).ToTimeSpanNormalized(); + double percentage = ConverterTool.GetPercentageNumber(downloadProgress.BytesDownloaded, downloadProgress.BytesTotal); - lock (_progress!) - { - // Update fetch progress - _progress.ProgressPerFilePercentage = e.ProgressPercentage; - _progress.ProgressAllSizeCurrent = e.SizeDownloaded; - _progress.ProgressAllSizeTotal = e.SizeToBeDownloaded; - _progress.ProgressAllSpeed = e.Speed; - _progress.ProgressAllTimeLeft = e.TimeLeft; - } + lock (_status!) + { + // Update fetch status + _status.IsProgressPerFileIndetermined = false; + _status.IsProgressAllIndetermined = false; + _status.ActivityPerFile = string.Format(Lang!._GameRepairPage!.PerProgressSubtitle3!, ConverterTool.SummarizeSizeSimple(speed)); + } - // Push status and progress update - UpdateStatus(); - UpdateProgress(); + lock (_progress!) + { + // Update fetch progress + _progress.ProgressPerFilePercentage = percentage; + _progress.ProgressAllSizeCurrent = downloadProgress.BytesDownloaded; + _progress.ProgressAllSizeTotal = downloadProgress.BytesTotal; + _progress.ProgressAllSpeed = speed; + _progress.ProgressAllTimeLeft = timeLeftSpan; + } + + // Push status and progress update + UpdateStatus(); + UpdateProgress(); + } } + #endregion #region ProgressEventHandlers - Repair - protected virtual async void _httpClient_RepairAssetProgress(object sender, DownloadEvent e) + protected virtual async void _httpClient_RepairAssetProgress(int size, DownloadProgress downloadProgress) { - lock (_progress!) - { - _progress.ProgressPerFilePercentage = e!.ProgressPercentage; - _progress.ProgressPerFileSizeCurrent = e!.SizeDownloaded; - _progress.ProgressPerFileSizeTotal = e!.SizeToBeDownloaded; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - - // Calculate speed - long speed = (long)(_progressAllSizeCurrent / _stopwatch!.Elapsed.TotalSeconds); - _progress.ProgressAllSpeed = speed; - _progress.ProgressAllTimeLeft = ((_progressAllSizeCurrent - _progressAllSizeTotal) / ConverterTool.Unzeroed(speed)) - .ToTimeSpanNormalized(); + Interlocked.Add(ref _progressAllSizeCurrent, size); + Interlocked.Add(ref _progressAllIOReadCurrent, size); - // Update current progress percentages - _progress.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? - ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal) : - 0; + if (await CheckIfNeedRefreshStopwatch()) + { + double speed = (_progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds).ClampLimitedSpeedNumber(); + TimeSpan timeLeftSpan = ((_progressAllSizeCurrent - _progressAllSizeTotal) / speed).ToTimeSpanNormalized(); + double percentagePerFile = ConverterTool.GetPercentageNumber(downloadProgress.BytesDownloaded, downloadProgress.BytesTotal); - if (e.State != DownloadState.Merging) + lock (_progress!) { - _progressAllSizeCurrent += e.Read; + _progress.ProgressPerFilePercentage = percentagePerFile; + _progress.ProgressPerFileSizeCurrent = downloadProgress.BytesDownloaded; + _progress.ProgressPerFileSizeTotal = downloadProgress.BytesTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + + // Calculate speed + _progress.ProgressAllSpeed = speed; + _progress.ProgressAllTimeLeft = timeLeftSpan; + + // Update current progress percentages + _progress.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? + ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal) : + 0; } - } - if (await CheckIfNeedRefreshStopwatch()) - { lock (_status!) { // Update current activity status @@ -145,23 +154,29 @@ protected virtual async void _httpClient_RepairAssetProgress(object sender, Down string timeLeftString = string.Format(Lang!._Misc!.TimeRemainHMSFormat!, _progress!.ProgressAllTimeLeft); _status.ActivityPerFile = string.Format(Lang._Misc.Speed!, ConverterTool.SummarizeSizeSimple(_progress.ProgressAllSpeed)); - _status.ActivityAll = string.Format(Lang._GameRepairPage!.PerProgressSubtitle2!, - ConverterTool.SummarizeSizeSimple(_progressAllSizeCurrent), + _status.ActivityAll = string.Format(Lang._GameRepairPage!.PerProgressSubtitle2!, + ConverterTool.SummarizeSizeSimple(_progressAllSizeCurrent), ConverterTool.SummarizeSizeSimple(_progressAllSizeTotal)) + $" | {timeLeftString}"; // Trigger update UpdateAll(); } + + if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) + { + _progressAllIOReadCurrent = 0; + _downloadSpeedRefreshStopwatch.Restart(); + } } } - protected virtual void UpdateRepairStatus(string activityStatus, string ActivityAll, bool isPerFileIndetermined) + protected virtual void UpdateRepairStatus(string activityStatus, string activityAll, bool isPerFileIndetermined) { lock (_status!) { // Set repair activity status _status.ActivityStatus = activityStatus; - _status.ActivityAll = ActivityAll; + _status.ActivityAll = activityAll; _status.IsProgressPerFileIndetermined = isPerFileIndetermined; } @@ -170,13 +185,55 @@ protected virtual void UpdateRepairStatus(string activityStatus, string Activity } #endregion + #region ProgressEventHandlers - UpdateCache + protected virtual async void _httpClient_UpdateAssetProgress(int size, DownloadProgress downloadProgress) + { + Interlocked.Add(ref _progressAllSizeCurrent, size); + Interlocked.Add(ref _progressAllIOReadCurrent, size); + + if (await CheckIfNeedRefreshStopwatch()) + { + double speed = (_progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds).ClampLimitedSpeedNumber(); + TimeSpan timeLeftSpan = ((_progressAllSizeTotal - _progressAllSizeCurrent) / speed).ToTimeSpanNormalized(); + double percentage = ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal); + + // Update current progress percentages and speed + if (_progress != null) + { + _progress.ProgressAllPercentage = percentage; + } + + // Update current activity status + if (_status != null) + { + _status.IsProgressAllIndetermined = false; + string timeLeftString = string.Format(Lang._Misc.TimeRemainHMSFormat, timeLeftSpan); + _status.ActivityAll = string.Format(Lang._Misc.Downloading + ": {0}/{1} ", _progressAllCountCurrent, + _progressAllCountTotal) + + string.Format($"({Lang._Misc.SpeedPerSec})", + ConverterTool.SummarizeSizeSimple(speed)) + + $" | {timeLeftString}"; + } + + if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) + { + _progressAllIOReadCurrent = 0; + _downloadSpeedRefreshStopwatch.Restart(); + } + + // Trigger update + UpdateAll(); + } + } + #endregion + #region ProgressEventHandlers - Patch - protected virtual async void RepairTypeActionPatching_ProgressChanged(object sender, BinaryPatchProgress e) + protected virtual async void RepairTypeActionPatching_ProgressChanged(object? sender, BinaryPatchProgress e) { lock (_progress!) { - _progress.ProgressPerFilePercentage = e!.ProgressPercentage; - _progress.ProgressAllSpeed = e!.Speed; + _progress.ProgressPerFilePercentage = e.ProgressPercentage; + _progress.ProgressAllSpeed = e.Speed; // Update current progress percentages _progress.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? @@ -204,46 +261,48 @@ protected virtual async void RepairTypeActionPatching_ProgressChanged(object sen #region ProgressEventHandlers - CRC/HashCheck protected virtual async void UpdateProgressCRC() { - if (await CheckIfNeedRefreshStopwatch()) + if (!await CheckIfNeedRefreshStopwatch()) { - lock (_progress!) - { - // Update current progress percentages - _progress.ProgressPerFilePercentage = _progressPerFileSizeCurrent != 0 ? - ConverterTool.GetPercentageNumber(_progressPerFileSizeCurrent, _progressPerFileSizeTotal) : - 0; - _progress.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? - ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal) : - 0; - - // Update the progress of total size - _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + return; + } - // Calculate current speed and update the status and progress speed - _progress.ProgressAllSpeed = _progressAllSizeCurrent / _stopwatch!.Elapsed.TotalSeconds; + lock (_progress!) + { + // Update current progress percentages + _progress.ProgressPerFilePercentage = _progressPerFileSizeCurrent != 0 ? + ConverterTool.GetPercentageNumber(_progressPerFileSizeCurrent, _progressPerFileSizeTotal) : + 0; + _progress.ProgressAllPercentage = _progressAllSizeCurrent != 0 ? + ConverterTool.GetPercentageNumber(_progressAllSizeCurrent, _progressAllSizeTotal) : + 0; - // Calculate the timelapse - _progress.ProgressAllTimeLeft = ((_progressAllSizeTotal - _progressAllSizeCurrent) / ConverterTool.Unzeroed(_progress.ProgressAllSpeed)).ToTimeSpanNormalized(); - } + // Update the progress of total size + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - lock (_status!) - { - // Set time estimation string - string timeLeftString = string.Format(Lang!._Misc!.TimeRemainHMSFormat!, _progress.ProgressAllTimeLeft); + // Calculate current speed and update the status and progress speed + _progress.ProgressAllSpeed = _progressAllSizeCurrent / _stopwatch!.Elapsed.TotalSeconds; - // Update current activity status - _status.ActivityPerFile = string.Format(Lang._Misc.Speed!, ConverterTool.SummarizeSizeSimple(_progress.ProgressAllSpeed)); - _status.ActivityAll = string.Format(Lang._GameRepairPage!.PerProgressSubtitle2!, - ConverterTool.SummarizeSizeSimple(_progressAllSizeCurrent), - ConverterTool.SummarizeSizeSimple(_progressAllSizeTotal)) + $" | {timeLeftString}"; - } + // Calculate the timelapse + _progress.ProgressAllTimeLeft = ((_progressAllSizeTotal - _progressAllSizeCurrent) / ConverterTool.Unzeroed(_progress.ProgressAllSpeed)).ToTimeSpanNormalized(); + } - // Trigger update - UpdateAll(); + lock (_status!) + { + // Set time estimation string + string timeLeftString = string.Format(Lang!._Misc!.TimeRemainHMSFormat!, _progress.ProgressAllTimeLeft); + + // Update current activity status + _status.ActivityPerFile = string.Format(Lang._Misc.Speed!, ConverterTool.SummarizeSizeSimple(_progress.ProgressAllSpeed)); + _status.ActivityAll = string.Format(Lang._GameRepairPage!.PerProgressSubtitle2!, + ConverterTool.SummarizeSizeSimple(_progressAllSizeCurrent), + ConverterTool.SummarizeSizeSimple(_progressAllSizeTotal)) + $" | {timeLeftString}"; } + + // Trigger update + UpdateAll(); } #endregion @@ -265,7 +324,7 @@ protected virtual async void UpdateProgressCopyStream(long currentPosition, int _progress.ProgressAllSpeed = currentPosition / _stopwatch!.Elapsed.TotalSeconds; // Calculate the timelapse - _progress.ProgressAllTimeLeft = ((totalReadSize - currentPosition) / ConverterTool.Unzeroed(_progress.ProgressAllSpeed)).ToTimeSpanNormalized(); + _progress.ProgressAllTimeLeft = ((totalReadSize - currentPosition) / _progress.ProgressAllSpeed.Unzeroed()).ToTimeSpanNormalized(); } lock (_status!) @@ -292,65 +351,84 @@ protected async void UpdateSophonFileTotalProgress(long read) Interlocked.Add(ref _progressAllSizeCurrent, read); _progressAllIOReadCurrent += read; - if (_refreshStopwatch!.ElapsedMilliseconds > _refreshInterval) + if (_refreshStopwatch!.ElapsedMilliseconds <= _refreshInterval) + { + return; + } + + // Assign local sizes to progress + if (_sophonProgress != null && _status != null) { - // Assign local sizes to progress - _sophonProgress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _sophonProgress.ProgressAllSizeTotal = _progressAllSizeTotal; - _sophonProgress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _sophonProgress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _sophonProgress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _sophonProgress.ProgressAllSizeTotal = _progressAllSizeTotal; + _sophonProgress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _sophonProgress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; // Calculate the speed - double speedAll = _progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds; - double speedPerFile = _progressPerFileIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds; - double speedAllNoReset = _progressAllSizeCurrent / _stopwatch.Elapsed.TotalSeconds; - _sophonProgress.ProgressAllSpeed = speedAll; - _sophonProgress.ProgressPerFileSpeed = speedPerFile; + double speedAll = _progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds; + double speedPerFile = + (_progressPerFileIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds) + .ClampLimitedSpeedNumber(); + double speedAllNoReset = _progressAllSizeCurrent / _stopwatch.Elapsed.TotalSeconds; + _sophonProgress.ProgressAllSpeed = speedAll; + _sophonProgress.ProgressPerFileSpeed = speedPerFile; // Calculate Count - _sophonProgress.ProgressAllEntryCountCurrent = _progressAllCountCurrent; - _sophonProgress.ProgressAllEntryCountTotal = _progressAllCountTotal; + _sophonProgress.ProgressAllEntryCountCurrent = _progressAllCountCurrent; + _sophonProgress.ProgressAllEntryCountTotal = _progressAllCountTotal; + + // Always change the status progress to determined + _status.IsProgressAllIndetermined = false; + _status.IsProgressPerFileIndetermined = false; + StatusChanged?.Invoke(this, _status); // Calculate percentage - _sophonProgress.ProgressAllPercentage = + _sophonProgress.ProgressAllPercentage = Math.Round((double)_progressAllSizeCurrent / _progressAllSizeTotal * 100, 2); - _sophonProgress.ProgressPerFilePercentage = + _sophonProgress.ProgressPerFilePercentage = Math.Round((double)_progressPerFileSizeCurrent / _progressPerFileSizeTotal * 100, 2); // Calculate the timelapse - double progressTimeAvg = (_progressAllSizeTotal - _progressAllSizeCurrent) / speedAllNoReset; + double progressTimeAvg = speedPerFile > 0 + ? (_progressPerFileSizeTotal - _progressPerFileSizeCurrent) / speedPerFile + : (_progressAllSizeTotal - _progressAllSizeCurrent) / speedAllNoReset; + _sophonProgress.ProgressAllTimeLeft = progressTimeAvg.ToTimeSpanNormalized(); // Update progress ProgressChanged?.Invoke(this, _sophonProgress); + } - if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) - { - _progressAllIOReadCurrent = 0; - _progressPerFileIOReadCurrent = 0; - _downloadSpeedRefreshStopwatch.Restart(); - } - - _refreshStopwatch.Restart(); - await Task.Delay(_refreshInterval); + if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) + { + _progressAllIOReadCurrent = 0; + _progressPerFileIOReadCurrent = 0; + _downloadSpeedRefreshStopwatch.Restart(); } + + _refreshStopwatch.Restart(); + await Task.Delay(_refreshInterval); } protected void UpdateSophonFileDownloadProgress(long downloadedWrite, long currentWrite) { Interlocked.Add(ref _progressPerFileSizeCurrent, downloadedWrite); - lock (_objLock) - { - _progressPerFileIOReadCurrent += currentWrite; - } + Interlocked.Add(ref _progressPerFileIOReadCurrent, currentWrite); } protected void UpdateSophonDownloadStatus(SophonAsset asset) { Interlocked.Add(ref _progressAllCountCurrent, 1); - _status.ActivityStatus = string.Format("{0}: {1}", - _isSophonInUpdateMode ? Lang._Misc.Updating : Lang._Misc.Downloading, - string.Format(Lang._Misc.PerFromTo, _progressAllCountCurrent, _progressAllCountTotal)); + if (_status != null) + { + _status.ActivityStatus = string.Format("{0}: {1}", + _isSophonInUpdateMode + ? Lang._Misc.Updating + : Lang._Misc.Downloading, + string.Format(Lang._Misc.PerFromTo, _progressAllCountCurrent, + _progressAllCountTotal)); + } + UpdateStatus(); } @@ -375,7 +453,7 @@ protected async Task DoCopyStreamProgress(Stream source, Stream target, Cancella { int read; // ReSharper disable once ConstantNullCoalescingCondition - long inputSize = estimatedSize != null ? estimatedSize ?? 0 : source!.Length; + long inputSize = estimatedSize != null ? estimatedSize ?? 0 : source.Length; long currentPos = 0; RestartStopwatch(); @@ -388,14 +466,13 @@ protected async Task DoCopyStreamProgress(Stream source, Stream target, Cancella byte[] buffer = ArrayPool.Shared.Rent(16 << 10); try { - while ((read = await source!.ReadAsync(buffer, token)) > 0) + while ((read = await source.ReadAsync(buffer, token)) > 0) { - await target!.WriteAsync(buffer, 0, read, token); + await target.WriteAsync(buffer, 0, read, token); currentPos += read; UpdateProgressCopyStream(currentPos, read, inputSize); } } - catch { throw; } finally { _status!.IsProgressPerFileIndetermined = isLastPerfileStateIndetermined; @@ -406,17 +483,20 @@ protected async Task DoCopyStreamProgress(Stream source, Stream target, Cancella protected string EnsureCreationOfDirectory(string str) { - string dir = Path.GetDirectoryName(str); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir!); + if (string.IsNullOrEmpty(str)) + ArgumentException.ThrowIfNullOrEmpty(str); + + string? dir = Path.GetDirectoryName(str); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); return str; } protected void TryUnassignReadOnlyFiles(string path) { - // Iterate every files and set the read-only flag to false - foreach (string file in Directory.EnumerateFiles(path!, "*", SearchOption.AllDirectories)) + // Iterate every file and set the read-only flag to false + foreach (string file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) { FileInfo fileInfo = new FileInfo(file); if (fileInfo.IsReadOnly) @@ -427,7 +507,7 @@ protected void TryUnassignReadOnlyFiles(string path) protected void TryUnassignReadOnlyFileSingle(string path) { FileInfo fileInfo = new FileInfo(path); - if (fileInfo.Exists && fileInfo.IsReadOnly) + if (fileInfo is { Exists: true, IsReadOnly: true }) fileInfo.IsReadOnly = false; } @@ -462,8 +542,10 @@ protected void TryDeleteReadOnlyFile(string path) if (!File.Exists(path)) return; try { - FileInfo file = new FileInfo(path!); - file.IsReadOnly = false; + FileInfo file = new FileInfo(path) + { + IsReadOnly = false + }; file.Delete(); } catch (Exception ex) @@ -472,23 +554,23 @@ protected void TryDeleteReadOnlyFile(string path) } } - protected void MoveFolderContent(string SourcePath, string DestPath) + protected void MoveFolderContent(string sourcePath, string destPath) { // Get the source folder path length + 1 - int DirLength = SourcePath!.Length + 1; + int dirLength = sourcePath.Length + 1; - // Initializw paths and error status - string destFilePath; - string destFolderPath; - bool ErrorOccured = false; + // Initialize paths and error status + string destFilePath; + string? destFolderPath; + bool errorOccured = false; // Enumerate files inside of source path - foreach (string filePath in Directory.EnumerateFiles(SourcePath, "*", SearchOption.AllDirectories)) + foreach (string filePath in Directory.EnumerateFiles(sourcePath, "*", SearchOption.AllDirectories)) { // Get the relative path of the file from source path - ReadOnlySpan relativePath = filePath.AsSpan().Slice(DirLength); + ReadOnlySpan relativePath = filePath.AsSpan().Slice(dirLength); // Get the absolute path for destination - destFilePath = Path.Combine(DestPath!, relativePath.ToString()); + destFilePath = Path.Combine(destPath, relativePath.ToString()); // Get folder path for destination destFolderPath = Path.GetDirectoryName(destFilePath); @@ -500,21 +582,23 @@ protected void MoveFolderContent(string SourcePath, string DestPath) { // Try moving the file LogWriteLine($"Moving \"{relativePath.ToString()}\" to \"{destFolderPath}\"", LogType.Default, true); - FileInfo filePathInfo = new FileInfo(filePath); - filePathInfo.IsReadOnly = false; + FileInfo filePathInfo = new FileInfo(filePath) + { + IsReadOnly = false + }; filePathInfo.MoveTo(destFilePath, true); } catch (Exception ex) { // If failed, flag ErrorOccured as true and skip to the next file LogWriteLine($"Error while moving \"{relativePath.ToString()}\" to \"{destFolderPath}\"\r\nException: {ex}", LogType.Error, true); - ErrorOccured = true; + errorOccured = true; } } // If no error occurred, then delete the source folder - if (!ErrorOccured) - Directory.Delete(SourcePath, true); + if (!errorOccured) + Directory.Delete(sourcePath, true); } protected virtual void ResetStatusAndProgress() @@ -568,12 +652,14 @@ protected void ResetStatusAndProgressProperty() _progress.ProgressPerFileEntryCountTotal = 0; // Reset all inner counter - _progressAllCountCurrent = 0; - _progressAllCountTotal = 0; - _progressAllSizeCurrent = 0; - _progressAllSizeTotal = 0; - _progressPerFileSizeCurrent = 0; - _progressPerFileSizeTotal = 0; + _progressAllCountCurrent = 0; + _progressAllCountTotal = 0; + _progressAllSizeCurrent = 0; + _progressAllSizeTotal = 0; + _progressAllIOReadCurrent = 0; + _progressPerFileSizeCurrent = 0; + _progressPerFileSizeTotal = 0; + _progressPerFileIOReadCurrent = 0; } } @@ -585,7 +671,7 @@ protected async Task BufferSourceStreamToMemoryStream(Stream input MemoryStream stream = new MemoryStream(); // Initialize length and Stopwatch - double sizeToDownload = input!.Length; + double sizeToDownload = input.Length; double downloaded = 0; Stopwatch sw = Stopwatch.StartNew(); @@ -615,11 +701,11 @@ protected async Task BufferSourceStreamToMemoryStream(Stream input protected async ValueTask FetchBilibiliSDK(CancellationToken token) { // Check whether the sdk is not null, - if (_gameVersionManager!.GameAPIProp!.data!.sdk == null) return; + if (_gameVersionManager.GameAPIProp.data?.sdk == null) return; // Initialize SDK DLL path variables - string sdkDllPath; - string sdkDllDir; + string sdkDllPath; + string? sdkDllDir; FileInfo sdkDllFile; // Set total activity string as "Loading Indexes..." @@ -628,61 +714,69 @@ protected async ValueTask FetchBilibiliSDK(CancellationToken token) // Get the URL and get the remote stream of the zip file // Also buffer the stream to memory - string url = _gameVersionManager!.GameAPIProp.data.sdk.path; - using (HttpResponseMessage httpResponse = await FallbackCDNUtil.GetURLHttpResponse(url, token)) - await using (BridgedNetworkStream httpStream = await FallbackCDNUtil.GetHttpStreamFromResponse(httpResponse, token)) - await using (MemoryStream bufferedStream = await BufferSourceStreamToMemoryStream(httpStream, token)) - using (ZipArchive zip = new ZipArchive(bufferedStream!, ZipArchiveMode.Read, true)) + string? url = _gameVersionManager.GameAPIProp.data.sdk.path; + using HttpResponseMessage httpResponse = await FallbackCDNUtil.GetURLHttpResponse(url, token); + await using BridgedNetworkStream httpStream = await FallbackCDNUtil.GetHttpStreamFromResponse(httpResponse, token); + await using MemoryStream bufferedStream = await BufferSourceStreamToMemoryStream(httpStream, token); + using ZipArchive zip = new ZipArchive(bufferedStream, ZipArchiveMode.Read, true); + // Iterate the Zip Entry + foreach (var entry in zip.Entries) { - // Iterate the Zip Entry - foreach (var entry in zip.Entries) - { - // Get the filename of the entry without ext. - string fileName = Path.GetFileNameWithoutExtension(entry.FullName); + // Get the filename of the entry without ext. + string fileName = Path.GetFileNameWithoutExtension(entry.FullName); - // If the entry is the "sdk_pkg_version", then override the info to sdk_pkg_version - switch (fileName) - { - case "PCGameSDK": - // Set the SDK DLL path - sdkDllPath = Path.Combine(_gamePath!, $"{Path.GetFileNameWithoutExtension(_gameVersionManager!.GamePreset!.GameExecutableName)}_Data", "Plugins", "PCGameSDK.dll"); - sdkDllDir = Path.GetDirectoryName(sdkDllPath); - sdkDllFile = new FileInfo(sdkDllPath); - - // Create the folder of the SDK DLL if doesn't exist - if (!Directory.Exists(sdkDllDir)) Directory.CreateDirectory(sdkDllDir!); - break; - case "sdk_pkg_version": - // Set the SDK DLL path to be used for sdk_pkg_version - sdkDllPath = Path.Combine(_gamePath!, "sdk_pkg_version"); - sdkDllFile = new FileInfo(sdkDllPath); - break; - default: - continue; - } + // If the entry is the "sdk_pkg_version", then override the info to sdk_pkg_version + switch (fileName) + { + case "PCGameSDK": + // Set the SDK DLL path + sdkDllPath = Path.Combine(_gamePath!, $"{Path.GetFileNameWithoutExtension(_gameVersionManager!.GamePreset!.GameExecutableName)}_Data", "Plugins", "PCGameSDK.dll"); + sdkDllDir = Path.GetDirectoryName(sdkDllPath); + sdkDllFile = new FileInfo(sdkDllPath); + + // Create the folder of the SDK DLL if it doesn't exist + if (!Directory.Exists(sdkDllDir)) Directory.CreateDirectory(sdkDllDir!); + break; + case "sdk_pkg_version": + // Set the SDK DLL path to be used for sdk_pkg_version + sdkDllPath = Path.Combine(_gamePath!, "sdk_pkg_version"); + sdkDllFile = new FileInfo(sdkDllPath); + break; + default: + continue; + } - // Do check if sdkDllFile is not null - // Try create the file if not exist or open an existing one - await using (Stream sdkDllStream = sdkDllFile.Open(!sdkDllFile.Exists || entry.Length < sdkDllFile.Length ? FileMode.Create : FileMode.OpenOrCreate)) - { - // Initiate the Crc32 hash - Crc32 hash = new Crc32(); + // Do check if sdkDllFile is not null + // Try to create the file if not exist or open an existing one + await using Stream sdkDllStream = sdkDllFile.Open(!sdkDllFile.Exists || entry.Length < sdkDllFile.Length ? FileMode.Create : FileMode.OpenOrCreate); + // Initiate the Crc32 hash + Crc32 hash = new Crc32(); + + // Append the SDK DLL stream to hash and get the result + await hash.AppendAsync(sdkDllStream, token); + byte[] hashByte = hash.GetHashAndReset(); + uint hashInt = BitConverter.ToUInt32(hashByte); + + // If the hash is the same, then skip + if (hashInt == entry.Crc32) continue; + await using Stream entryStream = entry.Open(); + // Reset the SDK DLL stream pos and write the data + sdkDllStream.Position = 0; + await entryStream.CopyToAsync(sdkDllStream, token); + } + } - // Append the SDK DLL stream to hash and get the result - await hash.AppendAsync(sdkDllStream, token); - byte[] hashByte = hash.GetHashAndReset(); - uint hashInt = BitConverter.ToUInt32(hashByte); + protected IEnumerable<(T1 AssetIndex, T2 AssetProperty)> PairEnumeratePropertyAndAssetIndexPackage + (IEnumerable assetIndex, IEnumerable assetProperty) + where T2 : IAssetProperty + { + using IEnumerator assetIndexEnumerator = assetIndex.GetEnumerator(); + using IEnumerator assetPropertyEnumerator = assetProperty.GetEnumerator(); - // If the hash is the same, then skip - if (hashInt == entry.Crc32) continue; - await using (Stream entryStream = entry.Open()) - { - // Reset the SDK DLL stream pos and write the data - sdkDllStream.Position = 0; - await entryStream.CopyToAsync(sdkDllStream, token); - } - } - } + while (assetIndexEnumerator.MoveNext() + && assetPropertyEnumerator.MoveNext()) + { + yield return (assetIndexEnumerator.Current, assetPropertyEnumerator.Current); } } @@ -691,16 +785,16 @@ protected IEnumerable EnforceHTTPSchemeToAssetIndex(IEnumerable assetInd const string HTTPSScheme = "https://"; const string HTTPScheme = "http://"; // Get the check if HTTP override is enabled - bool IsUseHTTPOverride = LauncherConfig.GetAppConfigValue("EnableHTTPRepairOverride").ToBool(); + bool isUseHttpOverride = LauncherConfig.GetAppConfigValue("EnableHTTPRepairOverride").ToBool(); // Iterate the IAssetIndexSummary asset - foreach (T1 asset in assetIndex!) + foreach (T1 asset in assetIndex) { // If the HTTP override is enabled, then start override the HTTPS scheme - if (IsUseHTTPOverride) + if (isUseHttpOverride) { // Get the remote url as span - ReadOnlySpan url = asset!.GetRemoteURL().AsSpan(); + ReadOnlySpan url = asset.GetRemoteURL().AsSpan(); // If the url starts with HTTPS scheme, then... if (url.StartsWith(HTTPSScheme)) { @@ -728,7 +822,7 @@ protected async Task TryRunExamineThrow(Task action) _status!.IsRunning = true; // Run the task - return await action!; + return await action; } catch (TaskCanceledException) { @@ -754,14 +848,17 @@ protected async Task TryRunExamineThrow(Task action) } finally { - // Clear the _assetIndex after that - if (!_status!.IsCompleted) + // Define that the status is not running + if (_status != null) { - _assetIndex!.Clear(); - } + // Clear the _assetIndex after that + if (_status is { IsCompleted: false }) + { + _assetIndex.Clear(); + } - // Define that the status is not running - _status.IsRunning = false; + _status.IsRunning = false; + } } } @@ -785,40 +882,57 @@ protected bool SummarizeStatusAndProgress(List assetIndex, string msgIfFound SetFoundToTotalValue(); // Set check if broken asset is found or not - bool IsBrokenFound = assetIndex!.Count > 0; + bool isBrokenFound = assetIndex.Count > 0; // Set status - _status!.IsAssetEntryPanelShow = IsBrokenFound; + _status!.IsAssetEntryPanelShow = isBrokenFound; _status.IsCompleted = true; _status.IsCanceled = false; - _status.ActivityStatus = IsBrokenFound ? msgIfFound : msgIfClear; + _status.ActivityStatus = isBrokenFound ? msgIfFound : msgIfClear; // Update status and progress UpdateAll(); // Return broken asset check - return IsBrokenFound; + return isBrokenFound; } protected virtual bool IsArrayMatch(ReadOnlySpan source, ReadOnlySpan target) => source.SequenceEqual(target); - protected virtual async Task RunDownloadTask(long assetSize, string assetPath, string assetURL, Http _httpClient, CancellationToken token) + protected virtual async Task RunDownloadTask(long assetSize, string assetPath, string assetURL, + DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token, bool isOverwrite = true) { - // Check for directory availability - if (!Directory.Exists(Path.GetDirectoryName(assetPath))) + // For any instances that uses Burst Download and if the speed limiter is null when + // _isBurstDownloadEnabled set to false, then create the speed limiter instance + bool isUseSelfSpeedLimiter = !_isBurstDownloadEnabled; + DownloadSpeedLimiter? downloadSpeedLimiter = null; + if (isUseSelfSpeedLimiter) { - Directory.CreateDirectory(Path.GetDirectoryName(assetPath)!); + // Create the speed limiter instance and register the listener + downloadSpeedLimiter = DownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached); + LauncherConfig.DownloadSpeedLimitChanged += downloadSpeedLimiter.GetListener(); } - // Start downloading asset - if (assetSize >= _sizeForMultiDownload) + try { - await _httpClient!.Download(assetURL, assetPath, _downloadThreadCount, true, token); - await _httpClient.Merge(token); + // Always do multi-session download with the new DownloadClient regardless of any sizes (if applicable) + await downloadClient.DownloadAsync( + assetURL, + EnsureCreationOfDirectory(assetPath), + isOverwrite, + sessionChunkSize: LauncherConfig.DownloadChunkSize, + progressDelegateAsync: downloadProgress, + cancelToken: token, + downloadSpeedLimiter: downloadSpeedLimiter + ); } - else + finally { - await _httpClient!.Download(assetURL, assetPath, true, null, null, token); + // If the self speed listener is used, then unregister the listener + if (isUseSelfSpeedLimiter && downloadSpeedLimiter != null) + { + LauncherConfig.DownloadSpeedLimitChanged -= downloadSpeedLimiter.GetListener(); + } } } @@ -828,66 +942,124 @@ protected virtual async Task RunDownloadTask(long assetSize, string assetPath, s ///
internal static async ValueTask NaivelyOpenFileStreamAsync(FileInfo info, FileMode fileMode, FileAccess fileAccess, FileShare fileShare) { - const int MaxTry = 10; + const int maxTry = 10; int currentTry = 1; while (true) { try { - return info.Open(fileMode, fileAccess, fileShare); + return info.Open(new FileStreamOptions + { + Mode = fileMode, + Access = fileAccess, + Share = fileShare + }); } catch { - if (currentTry <= MaxTry) + if (currentTry > maxTry) { - LogWriteLine($"Failed while trying to open: {info.FullName}. Retry attempt: {++currentTry} / {MaxTry}", LogType.Warning, true); - await Task.Delay(50); // Adding 50ms delay - continue; + throw; // Throw this MFs } - throw; // Throw this MFs + + LogWriteLine($"Failed while trying to open: {info.FullName}. Retry attempt: {++currentTry} / {maxTry}", LogType.Warning, true); + await Task.Delay(50); // Adding 50ms delay } } } #endregion #region HashTools - protected virtual async ValueTask CheckHashAsync(Stream stream, HashAlgorithm hashProvider, CancellationToken token, bool updateTotalProgress = true) + protected virtual async Task CheckHashAsync(Stream stream, T hashProvider, CancellationToken token, bool updateTotalProgress = true) + where T : HashAlgorithm { - // Initialize MD5 instance and assign buffer - byte[] buffer = new byte[_bufferBigLength]; + // Get length based on stream length or at least if bigger, use the default one + int bufferLen = _bufferMediumLength > stream.Length ? (int)stream.Length : _bufferMediumLength; - // Do read activity - int read; - while ((read = await stream!.ReadAsync(buffer, token)) > 0) + // Initialize buffer + byte[] buffer = ArrayPool.Shared.Rent(bufferLen); + + try { - // Throw Cancellation exception if detected - token.ThrowIfCancellationRequested(); + // Do read activity + int read; + while ((read = await stream.ReadAsync(buffer, token)) > 0) + { + // Throw Cancellation exception if detected + token.ThrowIfCancellationRequested(); - // Append buffer into hash block - hashProvider!.TransformBlock(buffer, 0, read, buffer, 0); + // Append buffer into hash block + hashProvider.TransformBlock(buffer, 0, read, buffer, 0); - lock (this) - { - // Increment total size counter - if (updateTotalProgress) _progressAllSizeCurrent += read; - // Increment per file size counter - _progressPerFileSizeCurrent += read; + lock (this) + { + // Increment total size counter + if (updateTotalProgress) _progressAllSizeCurrent += read; + // Increment per file size counter + _progressPerFileSizeCurrent += read; + } + + // Update status and progress for MD5 calculation + UpdateProgressCRC(); } - // Update status and progress for MD5 calculation - UpdateProgressCRC(); + // Finalize the hash calculation + hashProvider.TransformFinalBlock(buffer, 0, read); + + // Return computed hash byte + return hashProvider.Hash ?? []; + } + finally + { + ArrayPool.Shared.Return(buffer); } + } + + protected virtual async Task CheckNonCryptoHashAsync(Stream stream, T hashProvider, CancellationToken token, bool updateTotalProgress = true) + where T : NonCryptographicHashAlgorithm + { + // Get length based on stream length or at least if bigger, use the default one + int bufferLen = _bufferMediumLength > stream.Length ? (int)stream.Length : _bufferMediumLength; + + // Initialize buffer + byte[] buffer = ArrayPool.Shared.Rent(bufferLen); + + try + { + // Do read activity + int read; + while ((read = await stream.ReadAsync(buffer, token)) > 0) + { + // Throw Cancellation exception if detected + token.ThrowIfCancellationRequested(); - // Finalize the hash calculation - hashProvider!.TransformFinalBlock(buffer, 0, read); + // Append buffer into hash block + hashProvider.Append(buffer.AsSpan(0, read)); + + lock (this) + { + // Increment total size counter + if (updateTotalProgress) _progressAllSizeCurrent += read; + // Increment per file size counter + _progressPerFileSizeCurrent += read; + } + + // Update status and progress for Xxh64 calculation + UpdateProgressCRC(); + } - // Return computed hash byte - return hashProvider.Hash; + // Return computed hash byte + return hashProvider.GetHashAndReset(); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } #endregion #region PatchTools - protected virtual async ValueTask RunPatchTask(Http _httpClient, CancellationToken token, long patchSize, Memory patchHash, + protected virtual async ValueTask RunPatchTask(DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token, long patchSize, Memory patchHash, string patchURL, string patchOutputFile, string inputFile, string outputFile, bool isNeedRename = false) { ArgumentNullException.ThrowIfNull(patchOutputFile); @@ -901,25 +1073,23 @@ protected virtual async ValueTask RunPatchTask(Http _httpClient, CancellationTok if (!patchInfo.Exists || patchInfo.Length != patchSize) { // Download patch File first - await RunDownloadTask(patchSize, patchOutputFile, patchURL, _httpClient, token)!; + await RunDownloadTask(patchSize, patchOutputFile, patchURL, downloadClient, downloadProgress, token); } // Always do loop if patch doesn't get downloaded properly while (true) { - using (FileStream patchfs = new FileStream(patchOutputFile, FileMode.Open, FileAccess.Read, FileShare.None, _bufferBigLength)) + await using FileStream patchfs = new FileStream(patchOutputFile, FileMode.Open, FileAccess.Read, FileShare.None, _bufferBigLength); + // Verify the patch file and if it doesn't match, then redownload it + byte[] patchCrc = await CheckHashAsync(patchfs, MD5.Create(), token, false); + if (!IsArrayMatch(patchCrc, patchHash.Span)) { - // Verify the patch file and if it doesn't match, then redownload it - byte[] patchCRC = await CheckHashAsync(patchfs, MD5.Create(), token, false); - if (!IsArrayMatch(patchCRC, patchHash.Span)) - { - // Revert back the total size - _progressAllSizeCurrent -= patchSize; + // Revert back the total size + _progressAllSizeCurrent -= patchSize; - // Redownload the patch file - await RunDownloadTask(patchSize, patchOutputFile, patchURL, _httpClient, token)!; - continue; - } + // Redownload the patch file + await RunDownloadTask(patchSize, patchOutputFile, patchURL, downloadClient, downloadProgress, token); + continue; } // else, break and quit from loop @@ -933,7 +1103,7 @@ protected virtual async ValueTask RunPatchTask(Http _httpClient, CancellationTok // Subscribe patching progress and start applying patch patchUtil.ProgressChanged += RepairTypeActionPatching_ProgressChanged; patchUtil.Initialize(inputFile, patchOutputFile, outputFile); - await Task.Run(() => patchUtil.Apply(token)); + await Task.Run(() => patchUtil.Apply(token), token); // Delete old block File.Delete(inputFile); @@ -943,7 +1113,6 @@ protected virtual async ValueTask RunPatchTask(Http _httpClient, CancellationTok File.Move(outputFile, inputFile, true); } } - catch { throw; } finally { // Delete the patch file and unsubscribe the patching progress @@ -959,19 +1128,19 @@ protected virtual async ValueTask RunPatchTask(Http _httpClient, CancellationTok #endregion #region DialogTools - protected async Task SpawnRepairDialog(List assetIndex, Action actionIfInteractiveCancel) + protected async Task SpawnRepairDialog(List assetIndex, Action? actionIfInteractiveCancel) { ArgumentNullException.ThrowIfNull(assetIndex); - long totalSize = assetIndex.Sum(x => x!.GetAssetSize()); - StackPanel Content = UIElementExtensions.CreateStackPanel(); + long totalSize = assetIndex.Sum(x => x.GetAssetSize()); + StackPanel content = UIElementExtensions.CreateStackPanel(); - Content.AddElementToStackPanel(new TextBlock() + content.AddElementToStackPanel(new TextBlock() { Text = string.Format(Lang._InstallMgmt.RepairFilesRequiredSubtitle!, assetIndex.Count, ConverterTool.SummarizeSizeSimple(totalSize)), Margin = new Thickness(0, 0, 0, 16), TextWrapping = TextWrapping.Wrap }); - Button ShowBrokenFilesButton = Content.AddElementToStackPanel( + Button showBrokenFilesButton = content.AddElementToStackPanel( UIElementExtensions.CreateButtonWithIcon