├── Updater-windows7-x64-packed-runtime └── run.bat ├── Tutorial Images ├── rel_v2.1.2+ │ ├── img01.jpg │ └── img02.jpg └── rel_v3.2.4+ │ ├── cn_img01.png │ └── cn_img02.png ├── HDiffPatchCreator ├── bin │ ├── Debug │ │ └── net6.0-windows7.0 │ │ │ ├── 7z.exe │ │ │ ├── hdiffz.exe │ │ │ └── hpatchz.exe │ └── Release │ │ └── net6.0-windows7.0 │ │ ├── 7z.exe │ │ ├── hdiffz.exe │ │ └── hpatchz.exe ├── HDiffPatchCreator.csproj └── Program.cs ├── HappyGenyuanImsactUpdate ├── bin │ ├── Debug │ │ └── net6.0-windows7.0 │ │ │ ├── 7z.exe │ │ │ └── hpatchz.exe │ └── Release │ │ └── net6.0-windows7.0 │ │ ├── 7z.exe │ │ └── hpatchz.exe ├── HappyGenyuanImsactUpdate.csproj ├── MyMD5.cs ├── Unzipped.cs ├── ConfigIni.cs ├── Patch.cs ├── UpCheck.cs ├── Helper.cs ├── OuterInvoke.cs └── Program.cs ├── .gitignore ├── LICENSE ├── create-release.bat ├── HappyGenyuanImsactUpdate.sln ├── .github └── workflows │ └── dotnet-desktop.yml ├── README_CN.md └── README.md /Updater-windows7-x64-packed-runtime/run.bat: -------------------------------------------------------------------------------- 1 | .\bin\HappyGenyuanImsactUpdate.exe -------------------------------------------------------------------------------- /Tutorial Images/rel_v2.1.2+/img01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/Tutorial Images/rel_v2.1.2+/img01.jpg -------------------------------------------------------------------------------- /Tutorial Images/rel_v2.1.2+/img02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/Tutorial Images/rel_v2.1.2+/img02.jpg -------------------------------------------------------------------------------- /Tutorial Images/rel_v3.2.4+/cn_img01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/Tutorial Images/rel_v3.2.4+/cn_img01.png -------------------------------------------------------------------------------- /Tutorial Images/rel_v3.2.4+/cn_img02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/Tutorial Images/rel_v3.2.4+/cn_img02.png -------------------------------------------------------------------------------- /HDiffPatchCreator/bin/Debug/net6.0-windows7.0/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HDiffPatchCreator/bin/Debug/net6.0-windows7.0/7z.exe -------------------------------------------------------------------------------- /HDiffPatchCreator/bin/Release/net6.0-windows7.0/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HDiffPatchCreator/bin/Release/net6.0-windows7.0/7z.exe -------------------------------------------------------------------------------- /HDiffPatchCreator/bin/Debug/net6.0-windows7.0/hdiffz.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HDiffPatchCreator/bin/Debug/net6.0-windows7.0/hdiffz.exe -------------------------------------------------------------------------------- /HDiffPatchCreator/bin/Debug/net6.0-windows7.0/hpatchz.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HDiffPatchCreator/bin/Debug/net6.0-windows7.0/hpatchz.exe -------------------------------------------------------------------------------- /HDiffPatchCreator/bin/Release/net6.0-windows7.0/hdiffz.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HDiffPatchCreator/bin/Release/net6.0-windows7.0/hdiffz.exe -------------------------------------------------------------------------------- /HDiffPatchCreator/bin/Release/net6.0-windows7.0/hpatchz.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HDiffPatchCreator/bin/Release/net6.0-windows7.0/hpatchz.exe -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/bin/Debug/net6.0-windows7.0/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HappyGenyuanImsactUpdate/bin/Debug/net6.0-windows7.0/7z.exe -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/bin/Release/net6.0-windows7.0/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HappyGenyuanImsactUpdate/bin/Release/net6.0-windows7.0/7z.exe -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/bin/Debug/net6.0-windows7.0/hpatchz.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HappyGenyuanImsactUpdate/bin/Debug/net6.0-windows7.0/hpatchz.exe -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/bin/Release/net6.0-windows7.0/hpatchz.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/HEAD/HappyGenyuanImsactUpdate/bin/Release/net6.0-windows7.0/hpatchz.exe -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /Release-windows10-x64 3 | /HappyGenyuanImsactUpdate/bin 4 | /HappyGenyuanImsactUpdate/obj 5 | /HappyGenyuanImsactUpdate/Properties 6 | /HDiffPatchCreator/bin 7 | /HDiffPatchCreator/obj 8 | /HDiffPatchCreator/Properties 9 | /.vscode 10 | /Release-windows7-x64 11 | /Updater-windows7-x64-packed-runtime 12 | /Release-windows7-x64.zip 13 | /Updater-windows7-x64-packed-runtime.zip 14 | /Release-windows7-x86 15 | /Updater-windows7-x86-packed-runtime 16 | /Release-windows7-x86.zip 17 | /Updater-windows7-x86-packed-runtime.zip 18 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/HappyGenyuanImsactUpdate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0-windows7.0 6 | enable 7 | enable 8 | 7.0 9 | 3.2.4 10 | YYHEggEgg 11 | Copyright (c) 2023 YYHEggEgg 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/MyMD5.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace HappyGenyuanImsactUpdate 5 | { 6 | public class MyMD5 7 | { 8 | public static string GetMD5HashFromFile(string fileName) 9 | { 10 | FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read); 11 | //MD5 md5 = new MD5CryptoServiceProvider(); 12 | MD5 md5 = MD5.Create(); 13 | byte[] retVal = md5.ComputeHash(file); 14 | file.Close(); 15 | StringBuilder sb = new StringBuilder(); 16 | for (int i = 0; i < retVal.Length; i++) 17 | { 18 | sb.Append(retVal[i].ToString("x2")); 19 | } 20 | return sb.ToString(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /HDiffPatchCreator/HDiffPatchCreator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0-windows7.0 6 | enable 7 | enable 8 | 7.0 9 | 3.2.4 10 | YYHEggEgg 11 | Copyright (c) 2024 YYHEggEgg 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 YYHEggEgg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /create-release.bat: -------------------------------------------------------------------------------- 1 | mkdir .\Release-windows7-x64 2 | dotnet publish --configuration Release .\HappyGenyuanImsactUpdate --output .\Release-windows7-x64\Updater 3 | copy ".\HappyGenyuanImsactUpdate\bin\Release\net6.0-windows7.0\7z.exe" ".\Release-windows7-x64\Updater\7z.exe" /y 4 | copy ".\HappyGenyuanImsactUpdate\bin\Release\net6.0-windows7.0\hpatchz.exe" ".\Release-windows7-x64\Updater\hpatchz.exe" /y 5 | 6 | dotnet publish --configuration Release .\HDiffPatchCreator --output ".\Release-windows7-x64\Patch Creator" 7 | copy ".\HDiffPatchCreator\bin\Release\net6.0-windows7.0\7z.exe" ".\Release-windows7-x64\Patch Creator\7z.exe" /y 8 | copy ".\HDiffPatchCreator\bin\Release\net6.0-windows7.0\hdiffz.exe" ".\Release-windows7-x64\Patch Creator\hdiffz.exe" /y 9 | copy ".\HDiffPatchCreator\bin\Release\net6.0-windows7.0\hpatchz.exe" ".\Release-windows7-x64\Patch Creator\hpatchz.exe" /y 10 | 11 | mkdir .\Updater-windows7-x64-packed-runtime 12 | dotnet publish --configuration Release --runtime win7-x64 --self-contained .\HappyGenyuanImsactUpdate --output .\Updater-windows7-x64-packed-runtime\bin 13 | copy ".\HappyGenyuanImsactUpdate\bin\Release\net6.0-windows7.0\7z.exe" ".\Updater-windows7-x64-packed-runtime\bin\7z.exe" /y 14 | copy ".\HappyGenyuanImsactUpdate\bin\Release\net6.0-windows7.0\hpatchz.exe" ".\Updater-windows7-x64-packed-runtime\bin\hpatchz.exe" /y 15 | 16 | pause -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/Unzipped.cs: -------------------------------------------------------------------------------- 1 | namespace HappyGenyuanImsactUpdate 2 | { 3 | // NOTE: Because some dawn packages from gdrive has a sub folder, we should move it back. 4 | internal class Unzipped 5 | { 6 | #region Move Back Sub Folder from zip file 7 | public static void MoveBackSubFolder(DirectoryInfo datadir, string[]? predirs) 8 | { 9 | var nowdirs = Directory.GetDirectories(datadir.FullName); 10 | var newappeared_dirs = GetNewlyAppearedFolders(predirs, nowdirs); 11 | foreach (var dir in newappeared_dirs) 12 | { 13 | MoveDir(dir, datadir.FullName); 14 | } 15 | } 16 | 17 | private static List GetNewlyAppearedFolders(string[]? predirs, string[]? nowdirs) 18 | { 19 | if (nowdirs == null) return new(); 20 | List newdirs = new(); 21 | foreach (var dir in nowdirs) 22 | { 23 | if (predirs == null || !predirs.Contains(dir)) newdirs.Add(dir); 24 | } 25 | return newdirs; 26 | } 27 | 28 | private static void MoveDir(string source, string target) 29 | { 30 | foreach (var file in Directory.GetFiles(source)) 31 | { 32 | File.Move(file, $"{target}\\{new FileInfo(file).Name}", true); 33 | } 34 | foreach (var dir in Directory.GetDirectories(source)) 35 | { 36 | string newdir = $"{target}\\{new DirectoryInfo(dir).Name}"; 37 | Directory.CreateDirectory(newdir); 38 | MoveDir(dir, newdir); 39 | } 40 | Directory.Delete(source); 41 | } 42 | #endregion 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32526.322 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HappyGenyuanImsactUpdate", "HappyGenyuanImsactUpdate\HappyGenyuanImsactUpdate.csproj", "{CA429FBB-149E-4B0E-ACAC-B31845A675D4}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HDiffPatchCreator", "HDiffPatchCreator\HDiffPatchCreator.csproj", "{7BC7537D-76F1-4AA7-8617-7C069FB82561}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {CA429FBB-149E-4B0E-ACAC-B31845A675D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {CA429FBB-149E-4B0E-ACAC-B31845A675D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {CA429FBB-149E-4B0E-ACAC-B31845A675D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {CA429FBB-149E-4B0E-ACAC-B31845A675D4}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {7BC7537D-76F1-4AA7-8617-7C069FB82561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7BC7537D-76F1-4AA7-8617-7C069FB82561}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7BC7537D-76F1-4AA7-8617-7C069FB82561}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {7BC7537D-76F1-4AA7-8617-7C069FB82561}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2FCDC790-7724-49DA-9D4B-13BF716BD188} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/ConfigIni.cs: -------------------------------------------------------------------------------- 1 | namespace HappyGenyuanImsactUpdate 2 | { 3 | //config.ini contains version info 4 | internal static class ConfigIni 5 | { 6 | #region Find Version from zip name 7 | //index 012345678901234567890 8 | // zip: game_2.9.0_3.10.0_hdiff_abCdEFgHIjKLMnOP.zip 9 | public static string FindStartVersion(string zipName) 10 | { 11 | int index = zipName.IndexOf("_hdiff"); 12 | if (index == -1) return string.Empty; 13 | string substr = zipName.Substring(0, index); 14 | int veridx = substr.IndexOf('_'); 15 | int endidx = substr.IndexOf('_', veridx + 1); 16 | if (veridx == -1 || endidx == -1) return string.Empty; 17 | string rtn = substr.Substring(veridx + 1, endidx - veridx - 1); 18 | // Custom packages may not use standard version string. 19 | // return VerifyVersionString(rtn) ? rtn : string.Empty; 20 | return rtn; 21 | } 22 | 23 | public static string FindToVersion(string zipName) 24 | { 25 | int index = zipName.IndexOf("_hdiff"); 26 | string substr = zipName.Substring(0, index); 27 | if (index == -1) return string.Empty; 28 | int veridx = substr.LastIndexOf('_'); 29 | if (veridx == -1) return string.Empty; 30 | string rtn = substr.Substring(veridx + 1, substr.Length - veridx - 1); 31 | // return VerifyVersionString(rtn) ? rtn : string.Empty; 32 | return rtn; 33 | } 34 | 35 | public static bool VerifyVersionString(string verstr) 36 | { 37 | var strs = verstr.Split('.'); 38 | foreach (string str in strs) 39 | { 40 | if (!int.TryParse(str, out _)) return false; 41 | } 42 | return true; 43 | } 44 | #endregion 45 | 46 | // i don't want to write a real ini writer lol 47 | public static void ApplyConfigChange(FileInfo configfile, string version) 48 | { 49 | string fullfile = string.Empty; 50 | using (StreamReader reader = new(configfile.FullName)) 51 | { 52 | while (true) 53 | { 54 | string? line = reader.ReadLine(); 55 | if (line == null || line == string.Empty) break; 56 | if (line.StartsWith("game_version=")) 57 | line = $"game_version={version}"; 58 | fullfile += line + "\r\n"; 59 | } 60 | } 61 | 62 | File.WriteAllText(configfile.FullName, fullfile); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-desktop.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will build, test, sign and package a WPF or Windows Forms desktop application 7 | # built on .NET Core. 8 | # To learn how to migrate your existing application to .NET Core, 9 | # refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework 10 | # 11 | # 12 | # To configure this workflow: 13 | # 14 | # 1. Configure environment variables 15 | # GitHub sets default environment variables for every workflow run. 16 | # Replace the variables relative to your project in the "env" section below. 17 | # 18 | # 2. Signing 19 | # Generate a signing certificate in the Windows Application 20 | # Packaging Project or add an existing signing certificate to the project. 21 | # Next, use PowerShell to encode the .pfx file using Base64 encoding 22 | # by running the following Powershell script to generate the output string: 23 | # 24 | # $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte 25 | # [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt' 26 | # 27 | # Open the output file, SigningCertificate_Encoded.txt, and copy the 28 | # string inside. Then, add the string to the repo as a GitHub secret 29 | # and name it "Base64_Encoded_Pfx." 30 | # For more information on how to configure your signing certificate for 31 | # this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing 32 | # 33 | # Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key". 34 | # See "Build the Windows Application Packaging project" below to see how the secret is used. 35 | # 36 | # For more information on GitHub Actions, refer to https://github.com/features/actions 37 | # For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, 38 | # refer to https://github.com/microsoft/github-actions-for-desktop-apps 39 | 40 | name: Normal dotnet publish 41 | 42 | on: 43 | push: 44 | branches: [ "development-(have-not-been-tested)" ] 45 | pull_request: 46 | branches: [ "main", "development-(have-not-been-tested)" ] 47 | 48 | jobs: 49 | 50 | build: 51 | runs-on: windows-latest # For a list of available runner types, refer to 52 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v3 57 | with: 58 | fetch-depth: 0 59 | 60 | # Install the .NET Core workload 61 | - name: Install .NET Core 62 | uses: actions/setup-dotnet@v3 63 | with: 64 | dotnet-version: 6.0.x 65 | 66 | # Build 67 | - name: Create release 68 | working-directory: ${{env.GITHUB_WORKSPACE}} 69 | run: ./create-release.bat 70 | 71 | # Upload artifact 72 | - name: Upload release 73 | uses: actions/upload-artifact@v2 74 | with: 75 | name: Release-windows7-x64 76 | path: Release-windows7-x64 77 | retention-days: 7 78 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/Patch.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YYHEggEgg.Utils; 3 | 4 | namespace HappyGenyuanImsactUpdate 5 | { 6 | internal class Patch 7 | { 8 | public Patch(DirectoryInfo datadir, string path7z, string pathHdiff) 9 | { 10 | Path7z = path7z; 11 | PathHdiff = pathHdiff; 12 | this.datadir = datadir; 13 | } 14 | 15 | public string Path7z { get; set; } 16 | public string PathHdiff { get; set; } 17 | public DirectoryInfo datadir { get; set; } 18 | 19 | #region Patch hdiff 20 | /// 21 | /// Patch hdiff 22 | /// 23 | /// hdiff files used for deleteing 24 | public async Task Hdiff() 25 | { 26 | var hdiffs = new List(); 27 | var invokes = new List(); 28 | 29 | var hdifftxtPath = $"{datadir}\\hdifffiles.txt"; 30 | if (File.Exists(hdifftxtPath)) 31 | { 32 | using (StreamReader hdiffreader = new(hdifftxtPath)) 33 | { 34 | while (true) 35 | { 36 | string? output = hdiffreader.ReadLine(); 37 | if (output == null) break; 38 | else 39 | { 40 | var doc = JsonDocument.Parse(output); 41 | //{"remoteName": "name.pck"} 42 | string hdiffName = datadir.FullName + '/' 43 | + doc.RootElement.GetProperty("remoteName").GetString(); 44 | //command: -f (original file) (patch file) (output file) 45 | // hpatchz -f name.pck name.pck.hdiff name.pck 46 | string hdiffPathstd = new FileInfo(hdiffName).FullName; 47 | // If package is created by an individual, he may include 48 | // unnecessary files like cache and live updates, 49 | // So it's essential to skip some files that doesn't exist. 50 | if (!File.Exists(hdiffPathstd)) continue; 51 | 52 | invokes.Add(new OuterInvokeInfo 53 | { 54 | ProcessPath = PathHdiff, 55 | CmdLine = $"-f \"{hdiffName}\" \"{hdiffName}.hdiff\" \"{hdiffName}\"", 56 | AutoTerminateReason = $"hdiff patch for \"{hdiffName}\" failed." 57 | }); 58 | hdiffs.Add(hdiffPathstd); 59 | } 60 | } 61 | } 62 | 63 | File.Delete(hdifftxtPath); 64 | } 65 | 66 | await OuterInvoke.RunMultiple(invokes, 3851, 2); 67 | 68 | // Delete .hdiff afterwards 69 | foreach (var hdiffFile in hdiffs) 70 | { 71 | File.Delete($"{hdiffFile}.hdiff"); 72 | } 73 | } 74 | #endregion 75 | 76 | #region Delete Files 77 | /// 78 | /// Process deletedFiles.txt. Notice that files that failed to be deleted will be returned. 79 | /// 80 | /// files failed to be deleted 81 | public List DeleteFiles() 82 | { 83 | var delete_delays = new List(); 84 | 85 | var deletetxtPath = $"{datadir}\\deletefiles.txt"; 86 | if (File.Exists(deletetxtPath)) 87 | { 88 | using (StreamReader hdiffreader = new(deletetxtPath)) 89 | { 90 | while (true) 91 | { 92 | string? output = hdiffreader.ReadLine(); 93 | if (output == null) break; 94 | else 95 | { 96 | string deletedName = datadir.FullName + '\\' + output; 97 | if (File.Exists(deletedName)) 98 | File.Delete(deletedName); 99 | else delete_delays.Add(deletedName); 100 | } 101 | } 102 | } 103 | 104 | File.Delete(deletetxtPath); 105 | } 106 | 107 | return delete_delays; 108 | } 109 | #endregion 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | 中文 | [EN](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/blob/main/README.md) 2 | 3 | # HappyGenyuanImsactUpdate 4 | A hdiff-using update program of a certain anime game. 5 | 6 | ## 可用包体下载 7 | 8 | 您可以前往这里下载来自官方的 hdiff 更新包与完整包: 9 | 10 | - [Anime Game Downloads Archive](https://git.xeondev.com/YYHEggEgg/GI-Download-Library) 11 | - [Honkai: March 7th Downloads Archive](https://github.com/keitarogg/HSR-Download-Library) 12 | - [3Z Downloads Archive](https://github.com/360NENZ/ZZZ-Download-Library) 13 | 14 | ## 公告 15 | ### 许可证更改通知 16 | 自 2023 年 8 月 30 日 起,本项目已经更改为 MIT 许可证。所有以前和未来的贡献都受此新许可证约束。 17 | 18 | ### [请勿使用该程序来更新至3.6版本](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/issues/15) 19 | 20 | 从3.6开始,miHoYo 将 `StreamingAssets/Audio/GeneratedSoundBanks/Windows` 更改为 `StreamingAssets/AudioAssets`,但由启动器负责修改,不包含在更新包中。 21 | 22 | 这不会被修复,因为代码可能面临被污染的风险。 23 | 24 | 这很可能是一个临时的特例,**此更新程序在 3.7 及以后的版本中仍可用**。有关详细信息,请转至 [该 issue](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/issues/15)。 25 | 26 | ### 关于有限支持的说明 27 | 28 | 自 4.6 版本起,HoYoPlay 逐渐将 `sophon chunk` 作为其主要的更新分发与安装模式。 29 | 30 | 本程序仅对 hdiff 更新包适配,故不会添加对相应功能的支持;但本软件仍然会进行基本 Bug 修复与优化的更新(如果必要)。 31 | 32 | ## 新版本特性 33 | ### v3.2.4 34 | - 在使用 `Updater` 时,目录下如果不存在(可被程序识别的)游戏可执行文件,允许用户手动二次确认继续。 35 | 36 | ### v3.2.3 37 | - 由于提供的 `7z.exe`、`hpatchz.exe` 与 `hdiffz.exe` 均为 64 位程序,取消了对于 32 位 Windows 的支持计划。 38 | 39 | #### Patch Creator 40 | - 更新了 `--only-include-pkg-defined-files`、`--include-audios` 选项。有关详细信息,请参阅 [如何使用 - 创建更新包 / Patch Creater](#创建更新包--patch-creater). 41 | 42 | ### v3.2.2 43 | #### Updater 44 | - 修复了 Updater 无法正确支持 崩坏:整活铁道 的问题。 45 | - 软件现在打包 .NET 6.0 运行时发布。 46 | - 在发行版中将会支持 32 位 Windows。 47 | - 在 Windows 7 上添加了右下角气泡提示。Windows 10 上此功能仍然表现为“通知”。 48 | 49 | ### v3.2.1 50 | #### Updater 51 | - 修复了 Updater 在 <=1.5 版本下异常提示无法检查音频包完整性的问题。 52 | - 修复了 Updater 在 >=3.6 版本下无法检测到已安装语音包的问题。 53 | - 添加了对 崩坏:整活铁道 更新包的支持。 54 | 55 | #### Patch Creator 56 | 现在,当程序检测到提供的两个版本文件夹包含的文件名称相同时,会请求用户检查提供的参数是否正确。 57 | 58 | ### v3.2.0 59 | - 将整个项目迁移到了 `EggEgg.CSharp-Logger v3.0.0`。 60 | 现在无论是更新还是创建更新包都可以有日志记录保留在程序目录下,但调试日志不会显示在控制台而会输出在 `latest.debug.log` 中。 61 | - 在更新时,hdiff patch 失败支持进行自动重试(3 次)。 62 | - 如果不解压程序直接运行现在会触发警告。 63 | 64 | ### v3.0.0 65 | 现在您可以像官方一样自己创建更新包了! 66 | 使用命令行调用 `Patch Creater\HDiffPatchCreator.exe` 即可。 67 | 68 | 注意:强烈建议**仅使用原本的官方包为源文件**创建更新包。 69 | 70 | 供您自己电脑上使用的文件可能会包含小更新和缓存内容,使用包的人可能并不具备这些文件。**将缓存放入包内甚至可能导致您的个人信息泄露。** 有关信息参见 [可用包体下载](#可用包体下载). 71 | 72 | ## 如何找到游戏目录文件夹 73 | 1. 打开米哈游启动器 74 | 2. 点击“开始游戏”或“更新游戏”旁的菜单 75 | ![Launcher UI](https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/main/Tutorial%20Images/rel_v3.2.4%2B/cn_img01.png) 76 | 77 | 3. 点击“游戏设置”选项 78 | 4. 找到启动器显示的文件夹 (**注意:图片仅供参考,目录在您自己的电脑上与图片中不同!**) 79 | 80 | 5. 您也可以点击下方的“打开所在目录”按钮,直接点击地址栏并复制目录。如果您已从官方启动器预下载了更新文件,可以在内看到 `chunk` 文件夹。**本更新程序不适用于此种更新模式,请另行下载 hdiff 更新包。** 有关信息参见 [可用包体下载](#可用包体下载). 81 | 82 | ![Installation Location](https://raw.githubusercontent.com/YYHEggEgg/HappyGenyuanImsactUpdate/main/Tutorial%20Images/rel_v3.2.4%2B/cn_img02.png) 83 | ## 如何使用 84 | ### 补丁工具使用 / Updater 85 | 你需要以下文件: 86 | 87 | - 游戏文件 88 | - 一个或多个zip更新包 89 | - 在 [release](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/releases) 下载的本程序最新发行版 90 | 91 | 您可以参考这里的指示使用程序。 92 | 首先,您需要将游戏目录(目录下有 Yuansact.exe 或 GenyuanImsact.exe)输入程序, 93 | 其次,程序会询问您是否要在更新后检查文件正确性。 94 | - 输入0 - 请勿进行任何检查 95 | - 输入1 - _(推荐使用)_ 仅检查文件大小是否符合预期(过程一般在 10s 以内,大多数情况下足够) 96 | - 输入2 - 获取 MD5 进行完全检查(速度取决于硬盘性能,如果游戏数据没有存放在 SSD 之类的高速驱动器上将会需要很长时间) 97 | 98 | 然后输入您的更新包(通常是 zip 文件)**数量**。 99 | 在这之后,您只需要依次拖入所有更新包(每拖入一个包需要回车确认)即可开始更新。 100 | 101 | 通常,在更新完成后,如果您使用从官方启动器下载的游戏版本,程序将会指导您使官方启动器显示正确的游戏版本。 102 | 程序会告知您检测到的版本更新状态,如果版本正确只需按 `y` 确认即可,不想进行更改也可按 `n` 取消。 103 | 如果程序显示的版本不正确,您也可以输入正确的版本并继续。 104 | 105 | Enjoy it! 106 | 107 | ### 创建更新包 / Patch Creater 108 | 你需要以下文件: 109 | 110 | - **从官方下载的**两个版本的**已解压游戏文件** 111 | - 在 [release](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/releases) 下载的本程序最新发行版 112 | 113 | 您可以参考这里的命令行使用指南。 114 | ``` 115 | Usage: hdiffpatchcreator 116 | -from <更新前版本> <所在文件夹> 117 | -to <更新后版本> <所在文件夹> 118 | -output_to <输出文件夹,存放更新包和临时文件> 119 | [-p <前缀>] [-reverse] [--skip-check] 120 | [--only-include-pkg-defined-files [--include-audios]] 121 | ``` 122 | 123 | 使用程序可以得到这样一个更新包: 124 | ``` 125 | [前缀]_<更新前版本>_<更新后版本>_hdiff_<16位随机字符串>.zip 126 | ``` 127 | 如 `game_3.4_8.0_hdiff_nj89iGjh4d.zip` 128 | 前缀默认为 `game`. 129 | 130 | `-reverse` 选项在创建更新包后,更换“更新前版本”与“更新后版本”并再创建一个包。 131 | 132 | `--skip-check` 选项跳过基于文件大小比较的基础检查。但注意:对于 Patch Creator,由于需要进行严格的文件比较,程序一定会对文件进行 MD5 计算。建议您在读写速度较快的存储设备上(如 SSD)进行创建更新包的操作。 133 | 134 | `--only-include-pkg-defined-files` 可以在创建更新包时忽略 `pkg_version` 定义以外的所有文件,以避免本地的热更新、缓存、错误日志等无关内容被包括在更新包中。 135 | `--only-include-pkg-defined-files` 并不包括 `Audio_*_pkg_version` 定义的特定语言配音音频包文件。如果需要包含它们,请指定 `--include-audios` 选项。 136 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/UpCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YYHEggEgg.Logger; 3 | 4 | namespace HappyGenyuanImsactUpdate 5 | { 6 | public enum CheckMode 7 | { 8 | None = 0, 9 | Basic = 1, //file size 10 | Full = 2, //size + md5 11 | Null = -1 12 | } 13 | 14 | public static class UpCheck 15 | { 16 | #region pkg_versions 17 | /// 18 | /// It has two formats: 19 | /// pkg_version 20 | /// Audio_[Language]_pkg_version 21 | /// 22 | public static List GetPkgVersion(DirectoryInfo datadir) 23 | { 24 | List rtns = new(); 25 | 26 | string originVersionPath = $"{datadir.FullName}\\pkg_version"; 27 | if (File.Exists(originVersionPath)) rtns.Add(originVersionPath); 28 | foreach (var file in datadir.GetFiles()) 29 | { 30 | if (file.Name.StartsWith("Audio_") && file.Name.EndsWith("_pkg_version")) 31 | { 32 | #if DEBUG 33 | Log.Info( 34 | $"The lauguage of this audio package is {file.Name.Substring(6, file.Name.Length - 18)}.", nameof(GetPkgVersion)); 35 | #endif 36 | rtns.Add(file.FullName); 37 | } 38 | } 39 | return rtns; 40 | } 41 | #endregion 42 | 43 | #region Package Check 44 | public static bool CheckByPkgVersion(DirectoryInfo datadir, List pkgversionPaths, 45 | CheckMode checkAfter) 46 | { 47 | if (checkAfter == CheckMode.None) return true; 48 | 49 | bool checkPassed = true; 50 | 51 | foreach (var pkgversionPath in pkgversionPaths) 52 | { 53 | checkPassed = checkPassed && CheckByPkgVersion(datadir, pkgversionPath, checkAfter); 54 | } 55 | 56 | return checkPassed; 57 | } 58 | 59 | public static bool CheckByPkgVersion(DirectoryInfo datadir, string pkgversionPath, 60 | CheckMode checkAfter) 61 | { 62 | if (checkAfter == CheckMode.None) return true; 63 | 64 | bool checkPassed = true; 65 | using (StreamReader versionreader = new(pkgversionPath)) 66 | { 67 | while (true) 68 | { 69 | string? output = versionreader.ReadLine(); 70 | if (output == null) break; 71 | else 72 | { 73 | var doce = JsonDocument.Parse(output).RootElement; 74 | /* { 75 | * "remoteName": "name.pck", 76 | * "md5": "123456QWERTYUIOPASDFGHJKLZXCVBNM", 77 | * "fileSize": 1919810 78 | * } 79 | */ 80 | string checkName = datadir.FullName + '\\' 81 | + doce.GetProperty("remoteName").GetString(); 82 | //command: -f (original file) (patch file) (output file) 83 | // hpatchz -f name.pck name.pck.hdiff name.pck 84 | var checkFile = new FileInfo(checkName); 85 | string checkPathstd = checkFile.FullName; 86 | 87 | Log.PushLog($"Checking: {checkPathstd}", 88 | checkAfter == CheckMode.Full ? LogLevel.Information : LogLevel.Verbose, 89 | nameof(CheckByPkgVersion)); 90 | 91 | 92 | if (!File.Exists(checkPathstd)) 93 | { 94 | Log.Warn(ReportFileError(checkPathstd, "The file does not exist"), nameof(CheckByPkgVersion)); 95 | checkPassed = false; 96 | continue; 97 | } 98 | 99 | RemoveReadOnly(checkFile); 100 | 101 | #region File Size Check 102 | long sizeExpected = doce.GetProperty("fileSize").GetInt64(); 103 | if (checkFile.Length != sizeExpected) 104 | { 105 | Log.Warn(ReportFileError(checkPathstd, "The file is not correct"), nameof(CheckByPkgVersion)); 106 | checkPassed = false; 107 | continue; 108 | } 109 | #endregion 110 | 111 | if (checkAfter == CheckMode.Full) 112 | { 113 | #region MD5 Check 114 | string? md5Expected = doce.GetProperty("md5").GetString(); 115 | if (MyMD5.GetMD5HashFromFile(checkPathstd) != md5Expected) 116 | { 117 | Log.Warn(ReportFileError(checkPathstd, "The file is not correct"), nameof(CheckByPkgVersion)); 118 | checkPassed = false; 119 | continue; 120 | } 121 | #endregion 122 | } 123 | } 124 | } 125 | } 126 | 127 | return checkPassed; 128 | } 129 | 130 | private static void RemoveReadOnly(FileInfo checkFile) 131 | { 132 | if (checkFile.Attributes.HasFlag(FileAttributes.ReadOnly)) 133 | checkFile.Attributes = FileAttributes.Normal; 134 | } 135 | 136 | private static string ReportFileError(string checkPathstd, string reason) 137 | { 138 | return $"{reason} : {checkPathstd}"; 139 | } 140 | #endregion 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/blob/main/README_CN.md) | EN 2 | 3 | # HappyGenyuanImsactUpdate 4 | A hdiff-using update program of a certain anime game. 5 | 6 | ## Available Package Download 7 | You can turn to this repository to download files from `the anime game company`: 8 | 9 | - [Anime Game Downloads Archive](https://git.xeondev.com/YYHEggEgg/GI-Download-Library) 10 | - [Honkai: March 7th Downloads Archive](https://github.com/keitarogg/HSR-Download-Library) 11 | - [3Z Downloads Archive](https://github.com/360NENZ/ZZZ-Download-Library) 12 | 13 | ## Annoucements 14 | ### License Change Notice 15 | As of Aug 30, 2023, this project has been re-licensed under the MIT License. All previous and future contributions are subject to this new license. 16 | 17 | ### [Don't use this for ->3.6 Update](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/issues/15) 18 | From 3.6, miHoYo changed `StreamingAssets/Audio/GeneratedSoundBanks/Windows` to `StreamingAssets/AudioAssets`, but the launcher is responsible for the modification, not the update package. 19 | 20 | This won't be fixed as I don't want to pollute the code any more. 21 | 22 | This is most probably a temporaily a corner case and **this Updater program is still avaliable in >=3.7 versions**. For more information, go to [this issue](https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/issues/15). 23 | 24 | ### Explanation of Limited Support 25 | 26 | Since version 4.6, HoYoPlay has gradually adopted `sophon chunk` as its primary update distribution and installation mode. 27 | 28 | This program only adapts to hdiff update packages, so it will not add support for the corresponding feature. However, this software will still receive basic bug fixes and optimization updates (if necessary). 29 | 30 | ## New feature 31 | 32 | ### v3.2.4 33 | - When using the `Updater`, if the executable file of the game (that can be recognized by program) does not exist in the directory, users are allowed to manually confirm to continue. 34 | 35 | ### v3.2.3 36 | 37 | - Due to the fact that the provided `7z.exe`, `hpatchz.exe` and `hdiffz.exe` are all 64-bit programs, the support plan for 32-bit Windows has been cancelled. 38 | 39 | #### Patch Creator 40 | 41 | - The `--only-include-pkg-defined-files` and `--include-audios` option have been supported. For more information, please refer to [Usage - How to create a patch / Patch Creator](#how-to-create-a-patch--patch-creater). 42 | 43 | ### v3.2.2 44 | #### Updater 45 | - Fixed the issue where the Updater won't work with `Honkai: March 7th`. 46 | - The software is now packed with dotnet runtime (6.0), allowing user not to install runtime. 47 | - Release version supported Windows x86. 48 | - Supported balloon tip notice on Windows 7 and newer (notifications instead on >= Windows 10). 49 | 50 | ### v3.2.1 51 | #### Updater 52 | - Fixed the issue where the Updater won't work with anime game version <= `1.5`. 53 | - Fixed an issue with Audio packages where the Updater won't detect them in anime game version >= `3.6`. 54 | - Supported packages from `Honkai: March 7th`. 55 | 56 | #### Patch Creator 57 | Now, when the Patch Creator detected that the given two directories have files with the same names, it will request the user for review and confirmation. 58 | 59 | ### v3.2.0 60 | - Migrated the entire project to `EggEgg.CSharp-Logger v3.0.0`. 61 | Now, whether updating or creating update packages, the logs will be kept in the program directory, but the debug logs will not be displayed on the console and will be output to `latest.debug.log`. 62 | - During updates, hdiff patch failure is supported for automatic retries (3 times). 63 | - A warning will be triggered if the program is run without extracting it. 64 | 65 | ### v3.0.0 66 | Now you can create hdiff patch packages on your own, like `the anime game company`! 67 | Just invoke `Patch Creater\HDiffPatchCreator.exe` in command line. 68 | 69 | Notice: It's highly recommended to **use original packages from the anime game company only** to create patches. 70 | 71 | Files from your own computer will probably contains live updates and caches, which some users don't have. **Putting caches into the package will be likely to make your personal information got leaked.** For more information, see [Available Package Download](#available-package-download). 72 | 73 | ## Usage 74 | ### How to use the patcher / Updater 75 | You should have the following things: 76 | 77 | - A game (for sure) 78 | - One or more upgrade packages (zip file) 79 | - A release of this program 80 | 81 | You can use it by the instruction here or in the program. 82 | First of all, it will ask for the full path of game directory. 83 | Next, it will ask you to choose how to check the files after update: 84 | - 0 - Don't have any check 85 | - 1 - _(Recommended)_ Only check file size (usually < 10s, very fast, in most cases enough) 86 | - 2 - Full check on MD5 (the speed depends on your disk, it will take a long time if the data isn't on a fast-speed drive like SSD) 87 | 88 | Then, you need to type how many zip files you have. 89 | After that, you just need to drag zip files one by one (press enter after dragging in), then the update program will finish the update process automatically. 90 | 91 | Enjoy it! 92 | 93 | ### How to create a patch / Patch Creater 94 | You can refer to the following command line usage. 95 | ``` 96 | Usage: hdiffpatchcreator 97 | -from 98 | -to 99 | -output_to 100 | [-p ] [-reverse] [--skip-check] 101 | ``` 102 | 103 | By using this program, you can get a package named: 104 | ``` 105 | [prefix]___hdiff_.zip 106 | ``` 107 | e.g. `game_3.4_8.0_hdiff_nj89iGjh4d.zip` 108 | If not given, prefix will be `game`. 109 | 110 | `-reverse` option: After package is created, reverse 'versionFrom' and 'versionTo' and create another package. 111 | 112 | `--skip-check` option: skip the Basic Mode check (only compare file size). Notice: For the patch creator, MD5 computing when comparing files is essential. You are recommended to create Update packages on a disk with high I/O speed (like SSD). 113 | 114 | `--only-include-pkg-defined-files` option: ignore all files not defined in 'pkg_version' file to avoid unrelated content, like local live updates, cache and error logs, to be included in the update package. 115 | `--only-include-pkg-defined-files` excludes specified language audio files defined in `Audio_*_pkg_version`. If want to include them, use `--include-audios` option. 116 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Windows.Forms; 3 | using YYHEggEgg.Logger; 4 | 5 | namespace HappyGenyuanImsactUpdate 6 | { 7 | /// 8 | /// Methods in console interface. 9 | /// 10 | public class Helper 11 | { 12 | /// 13 | /// 7z.exe and hdiff.exe check. 14 | /// 15 | public static void CheckForTools(bool requirediff = false) 16 | { 17 | bool ok = true; 18 | if (!File.Exists($"{exePath}\\7z.exe")) 19 | { 20 | Log.Erro("7z.exe was missing. " + 21 | "Please copy it to the path of this program " + 22 | "or download the newest release in " + 23 | "https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/releases"); 24 | ok = false; 25 | } 26 | if (!File.Exists($"{exePath}\\hpatchz.exe")) 27 | { 28 | Log.Erro("hpatchz.exe was missing. " + 29 | "Please copy it to the path of this program " + 30 | "or download the newest release in " + 31 | "https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/releases"); 32 | ok = false; 33 | } 34 | if (requirediff) 35 | { 36 | if (!File.Exists($"{exePath}\\hdiffz.exe")) 37 | { 38 | Log.Erro("hpatchz.exe was missing. " + 39 | "Please copy it to the path of this program " + 40 | "or download the newest release in " + 41 | "https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/releases"); 42 | ok = false; 43 | } 44 | } 45 | if (!ok) 46 | { 47 | Log.Erro("The program will exit after an enter. " + 48 | "Please get missing file(s) the right location and restart."); 49 | Console.ReadLine(); 50 | Environment.Exit(1); 51 | } 52 | } 53 | 54 | /// 55 | /// Program executable path. 56 | /// 57 | public static string exePath { get => AppDomain.CurrentDomain.BaseDirectory; } 58 | 59 | //"ei hei" 60 | public static readonly string[] certaingames = new string[] 61 | { 62 | // anime game 63 | "\u0067\u0065\u006e\u0073\u0068\u0069\u006e\u0069\u006d\u0070\u0061\u0063\u0074", 64 | "\u0079\u0075\u0061\u006e\u0073\u0068\u0065\u006e", 65 | // Houtai: March 7th 66 | "\u0053\u0074\u0061\u0072\u0052\u0061\u0069\u006C", 67 | // "Reserved Executable Name:/\\<>" // OS exename == CN 68 | // Sleep 69 | "\u004a\u0075\u0065\u0051\u0075\u004c\u0069\u006e\u0067", // Former used 70 | "\u005a\u0065\u006e\u006c\u0065\u0073\u0073\u005a\u006f\u006e\u0065\u005a\u0065\u0072\u006f", 71 | }; 72 | 73 | public static bool AnyCertainGameExists(DirectoryInfo checkdir) 74 | { 75 | foreach (var certaingame in Helper.certaingames) 76 | { 77 | if (File.Exists($"{checkdir.FullName}\\{certaingame}.exe")) return true; 78 | if (File.Exists($"{checkdir.FullName}\\{certaingame}Beta.exe")) return true; 79 | } 80 | return false; 81 | } 82 | 83 | private static string? _tmpdir = null; 84 | private static FileStream? _tmpdirhandle = null; 85 | public static string tempPath 86 | { 87 | get 88 | { 89 | if (string.IsNullOrEmpty(_tmpdir)) 90 | { 91 | _tmpdir = $"{exePath}\\Temp-HappyGenyuanImsactUpdate-{DateTime.Now:yyyyMMdd-HHmmss}-{new Random().NextInt64()}"; 92 | Directory.CreateDirectory(_tmpdir); 93 | _tmpdirhandle = File.Create($"{_tmpdir}\\_Created By HappyGenyuanImsactUpdate for temp files.txt"); 94 | } 95 | return _tmpdir; 96 | } 97 | } 98 | 99 | public static void TryDisposeTempFiles() 100 | { 101 | try 102 | { 103 | if (_tmpdir == null) return; 104 | _tmpdirhandle?.Dispose(); 105 | Directory.Delete(_tmpdir, true); 106 | _tmpdir = null; 107 | } 108 | catch (Exception ex) 109 | { 110 | Log.Warn($"----------------------------\n{ex}\n----------------------------", nameof(TryDisposeTempFiles)); 111 | Log.Warn($"Failed to delete program temp files. You may delete directory {_tmpdir} by yourself.", nameof(TryDisposeTempFiles)); 112 | } 113 | } 114 | 115 | public static void CheckForRunningInZipFile() 116 | { 117 | if (Environment.CurrentDirectory.StartsWith( 118 | $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/Local/Temp")) 119 | { 120 | Log.Warn("You may be running the program without extracting, and that will make" + 121 | "logs lost since closing the zip file. "); 122 | Log.Warn("Consider extracting the program in case you meet some problem updating."); 123 | } 124 | } 125 | 126 | public static void ShowWarningBalloonTip(int persisting_ms, string title, string text) 127 | => ShowBalloonTipCore(title, text, persisting_ms, SystemIcons.Warning); 128 | 129 | public static void ShowInformationBalloonTip(int persisting_ms, string title, string text) 130 | => ShowBalloonTipCore(title, text, persisting_ms, SystemIcons.Information); 131 | 132 | public static void ShowErrorBalloonTip(int persisting_ms, string title, string text) 133 | => ShowBalloonTipCore(title, text, persisting_ms, SystemIcons.Error); 134 | 135 | private static void ShowBalloonTipCore(string title, string text, int persisting_ms, 136 | Icon notifyShowingIcon) 137 | { 138 | // Generated by ChatGPT 139 | NotifyIcon notifyIcon = new NotifyIcon(); 140 | notifyIcon.Visible = true; 141 | notifyIcon.Icon = notifyShowingIcon; 142 | notifyIcon.BalloonTipTitle = title; 143 | notifyIcon.BalloonTipText = text; 144 | notifyIcon.ShowBalloonTip(persisting_ms); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/OuterInvoke.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using YYHEggEgg.Logger; 3 | 4 | namespace YYHEggEgg.Utils 5 | { 6 | public struct OuterInvokeInfo 7 | { 8 | /// 9 | /// The path of the executable. 10 | /// 11 | public string ProcessPath; 12 | /// 13 | /// The command line args that will be provided to the process. 14 | /// 15 | public string? CmdLine; 16 | /// 17 | /// The notice that will be send to the user when the invoke start. 18 | /// 19 | public string? StartingNotice; 20 | /// 21 | /// The reason for auto terminate. May be a complete sentence. 22 | /// If it's given not null, the program will auto terminate before the method return. 23 | /// 24 | public string? AutoTerminateReason; 25 | /// 26 | /// The working directory of the program. The default value is . 27 | /// 28 | public string? WorkingDir; 29 | } 30 | 31 | public static class OuterInvoke 32 | { 33 | private static async Task MinorRun(ProcessStartInfo startInfo, int max_rerun = 0) 34 | { 35 | Process? p = Process.Start(startInfo); 36 | await (p?.WaitForExitAsync() ?? Task.CompletedTask); 37 | if (p?.ExitCode != 0 && max_rerun > 0) 38 | { 39 | return await MinorRun(startInfo, max_rerun - 1); 40 | } 41 | return p?.ExitCode ?? int.MinValue; 42 | } 43 | 44 | private static async Task InnerRun(OuterInvokeInfo invokeInfo, int max_rerun = 0) 45 | { 46 | ProcessStartInfo startInfo = new ProcessStartInfo( 47 | invokeInfo.ProcessPath, invokeInfo.CmdLine ?? "") 48 | { 49 | WorkingDirectory = invokeInfo.WorkingDir ?? Environment.CurrentDirectory 50 | }; 51 | if (invokeInfo.StartingNotice != null) Log.Info(invokeInfo.StartingNotice, nameof(OuterInvoke)); 52 | return await MinorRun(startInfo, max_rerun); 53 | } 54 | 55 | /// 56 | /// Run a process with certain args and wait for exit. 57 | /// 58 | /// The details of the invoke. 59 | /// The exut code for the program's auto terminate. 60 | /// The maximum retry times if process exited with not 0. 61 | /// The exit code of the program. 62 | public static async Task Run(OuterInvokeInfo invokeInfo, 63 | int autoTerminateCode = -1, int max_rerun = 0) 64 | { 65 | var exitcode = await InnerRun(invokeInfo, max_rerun); 66 | if (exitcode != 0 && invokeInfo.AutoTerminateReason != null) 67 | { 68 | Log.Erro($"{invokeInfo.AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 69 | Log.Info("Press any key to exit..."); 70 | Console.ReadLine(); 71 | Environment.Exit(autoTerminateCode); 72 | } 73 | return exitcode; 74 | } 75 | 76 | /// 77 | /// Run multiple processes with certain args and wait for exit. 78 | /// 79 | /// The details of the first invoke. 80 | /// The details of the second invoke. 81 | /// The exut code for the program's auto terminate. 82 | /// The maximum retry times if process exited with not 0. 83 | /// The exit code of the program. 84 | public static async Task RunMultiple( 85 | OuterInvokeInfo invokeInfo1, OuterInvokeInfo invokeInfo2, 86 | int autoTerminateCode = -1, int max_rerun = 0) 87 | { 88 | int[] rtn = new int[2]; 89 | rtn[0] = await InnerRun(invokeInfo1, max_rerun); 90 | rtn[1] = await InnerRun(invokeInfo2, max_rerun); 91 | 92 | bool exiting = false; 93 | if (rtn[0] != 0 && invokeInfo1.AutoTerminateReason != null) 94 | { 95 | exiting = true; 96 | Log.Erro($"{invokeInfo1.AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 97 | } 98 | if (rtn[1] != 0 && invokeInfo2.AutoTerminateReason != null) 99 | { 100 | exiting = true; 101 | Log.Erro($"{invokeInfo2.AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 102 | } 103 | if (exiting) 104 | { 105 | Log.Info("Press any key to exit..."); 106 | Console.ReadLine(); 107 | Environment.Exit(autoTerminateCode); 108 | } 109 | return rtn; 110 | } 111 | 112 | /// 113 | /// Run multiple processes with certain args and wait for exit. 114 | /// 115 | /// The details of the first invoke. 116 | /// The details of the second invoke. 117 | /// The details of the third invoke. 118 | /// The exut code for the program's auto terminate. 119 | /// The maximum retry times if process exited with not 0. 120 | /// The exit code of the program. 121 | public static async Task RunMultiple( 122 | OuterInvokeInfo invokeInfo1, OuterInvokeInfo invokeInfo2, OuterInvokeInfo invokeInfo3, 123 | int autoTerminateCode = -1, int max_rerun = 0) 124 | { 125 | int[] rtn = new int[3]; 126 | rtn[0] = await InnerRun(invokeInfo1, max_rerun); 127 | rtn[1] = await InnerRun(invokeInfo2, max_rerun); 128 | rtn[2] = await InnerRun(invokeInfo3, max_rerun); 129 | 130 | bool exiting = false; 131 | if (rtn[0] != 0 && invokeInfo1.AutoTerminateReason != null) 132 | { 133 | exiting = true; 134 | Log.Erro($"{invokeInfo1.AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 135 | } 136 | if (rtn[1] != 0 && invokeInfo2.AutoTerminateReason != null) 137 | { 138 | exiting = true; 139 | Log.Erro($"{invokeInfo2.AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 140 | } 141 | if (rtn[2] != 0 && invokeInfo3.AutoTerminateReason != null) 142 | { 143 | exiting = true; 144 | Log.Erro($"{invokeInfo3.AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 145 | } 146 | if (exiting) 147 | { 148 | Log.Info("Press any key to exit..."); 149 | Console.ReadLine(); 150 | Environment.Exit(autoTerminateCode); 151 | } 152 | return rtn; 153 | } 154 | 155 | /// 156 | /// Syncronously run multiple processes with certain args and wait for exit. 157 | /// 158 | /// The details of the invokes. 159 | /// The exut code for the program's auto terminate. 160 | /// The maximum retry times if process exited with not 0. 161 | /// The exit code of the program. 162 | public static async Task RunMultiple(OuterInvokeInfo[] invokeInfos, 163 | int autoTerminateCode = -1, int max_rerun = 0) 164 | { 165 | int[] rtn = new int[invokeInfos.Length]; 166 | for (int i = 0; i < invokeInfos.Length; i++) 167 | { 168 | rtn[i] = await InnerRun(invokeInfos[i], max_rerun); 169 | } 170 | 171 | bool exiting = false; 172 | for (int i = 0; i < invokeInfos.Length; i++) 173 | { 174 | if (rtn[i] != 0 && invokeInfos[i].AutoTerminateReason != null) 175 | { 176 | exiting = true; 177 | Log.Erro($"{invokeInfos[i].AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 178 | } 179 | } 180 | if (exiting) 181 | { 182 | Log.Info("Press any key to exit..."); 183 | Console.ReadLine(); 184 | Environment.Exit(autoTerminateCode); 185 | } 186 | return rtn; 187 | } 188 | 189 | /// 190 | /// Syncronously run multiple processes with certain args and wait for exit. 191 | /// 192 | /// The details of the invokes. 193 | /// The exut code for the program's auto terminate. 194 | /// The maximum retry times if process exited with not 0. 195 | /// The exit code of the program. 196 | public static async Task RunMultiple(List invokeInfos, 197 | int autoTerminateCode = -1, int max_rerun = 0) 198 | { 199 | int[] rtn = new int[invokeInfos.Count]; 200 | for (int i = 0; i < invokeInfos.Count; i++) 201 | { 202 | rtn[i] = await InnerRun(invokeInfos[i], max_rerun); 203 | } 204 | 205 | bool exiting = false; 206 | for (int i = 0; i < invokeInfos.Count; i++) 207 | { 208 | if (rtn[i] != 0 && invokeInfos[i].AutoTerminateReason != null) 209 | { 210 | exiting = true; 211 | Log.Erro($"{invokeInfos[i].AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 212 | } 213 | } 214 | if (exiting) 215 | { 216 | Log.Info("Press any key to exit..."); 217 | Console.ReadLine(); 218 | Environment.Exit(autoTerminateCode); 219 | } 220 | return rtn; 221 | } 222 | 223 | /// 224 | /// Run multiple processes parallel with certain args and wait for exit. Notice: may cause unexpected behaviour. 225 | /// 226 | /// The details of the invokes. 227 | /// The exut code for the program's auto terminate. 228 | /// The maximum retry times if process exited with not 0. 229 | /// The exit code of the program. 230 | public static int[] RunParallel(OuterInvokeInfo[] invokeInfos, 231 | int autoTerminateCode = -1, int max_rerun = 0) 232 | { 233 | int[] rtn = new int[invokeInfos.Length]; 234 | Parallel.For(0, invokeInfos.Length, async (i) => 235 | { 236 | rtn[i] = await InnerRun(invokeInfos[i], max_rerun); 237 | }); 238 | 239 | bool exiting = false; 240 | for (int i = 0; i < invokeInfos.Length; i++) 241 | { 242 | if (rtn[i] != 0 && invokeInfos[i].AutoTerminateReason != null) 243 | { 244 | exiting = true; 245 | Log.Erro($"{invokeInfos[i].AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 246 | } 247 | } 248 | if (exiting) 249 | { 250 | Log.Info("Press any key to exit..."); 251 | Console.ReadLine(); 252 | Environment.Exit(autoTerminateCode); 253 | } 254 | return rtn; 255 | } 256 | 257 | /// 258 | /// Run multiple processes parallel with certain args and wait for exit. Notice: may cause unexpected behaviour. 259 | /// 260 | /// The details of the invokes. 261 | /// The exut code for the program's auto terminate. 262 | /// The maximum retry times if process exited with not 0. 263 | /// The exit code of the program. 264 | public static int[] RunParallel(List invokeInfos, 265 | int autoTerminateCode = -1, int max_rerun = 0) 266 | { 267 | int[] rtn = new int[invokeInfos.Count]; 268 | Parallel.For(0, invokeInfos.Count, async (i) => 269 | { 270 | rtn[i] = await InnerRun(invokeInfos[i], max_rerun); 271 | }); 272 | 273 | bool exiting = false; 274 | for (int i = 0; i < invokeInfos.Count; i++) 275 | { 276 | if (rtn[i] != 0 && invokeInfos[i].AutoTerminateReason != null) 277 | { 278 | exiting = true; 279 | Log.Erro($"{invokeInfos[i].AutoTerminateReason} Exit code is {autoTerminateCode}. ", "OuterInvoke"); 280 | } 281 | } 282 | if (exiting) 283 | { 284 | Log.Info("Press any key to exit..."); 285 | Console.ReadLine(); 286 | Environment.Exit(autoTerminateCode); 287 | } 288 | return rtn; 289 | } 290 | } 291 | } -------------------------------------------------------------------------------- /HDiffPatchCreator/Program.cs: -------------------------------------------------------------------------------- 1 | using HappyGenyuanImsactUpdate; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Web; 8 | using YYHEggEgg.Logger; 9 | using YYHEggEgg.Utils; 10 | 11 | namespace HDiffPatchCreator 12 | { 13 | internal class Program 14 | { 15 | static string path7z = $"{Helper.exePath}\\7z.exe"; 16 | static string hdiffzPath = $"{Helper.exePath}\\hdiffz.exe"; 17 | 18 | static async Task Main(string[] args) 19 | { 20 | Log.Initialize(new LoggerConfig( 21 | max_Output_Char_Count: -1, 22 | use_Console_Wrapper: false, 23 | use_Working_Directory: false, 24 | global_Minimum_LogLevel: LogLevel.Verbose, 25 | console_Minimum_LogLevel: LogLevel.Information, 26 | debug_LogWriter_AutoFlush: true)); 27 | 28 | string? version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3); 29 | Log.Info($"----------Happy hdiff creator (v{version ?? ""})----------"); 30 | Helper.CheckForRunningInZipFile(); 31 | 32 | Helper.CheckForTools(); 33 | 34 | Log.Info("This program is used to create Patch from two versions of a certain anime game."); 35 | 36 | string verFrom = "Unknown", verTo = "Unknown", prefix = "game"; 37 | DirectoryInfo? dirFrom = null, dirTo = null, outputAt = null; 38 | bool createReverse = false, performCheck = true, 39 | onlyIncludeDefinedFiles = false, includeAudioVersions = false; 40 | #region Command Line 41 | bool[] arghaveread = new bool[8]; 42 | 43 | for (int i = 0; i < args.Length; i++) 44 | { 45 | if (args[i].StartsWith('"') && args[i].EndsWith('"')) 46 | args[i] = args[i].Substring(1, args[i].Length - 2); 47 | } 48 | 49 | if (args.Length > 0) 50 | { 51 | for (int i = 0; i < args.Length; i++) 52 | { 53 | switch (args[i]) 54 | { 55 | case "-from": 56 | ReadAssert(0); 57 | verFrom = HttpUtility.UrlEncode(args[i + 1]); 58 | dirFrom = new DirectoryInfo(args[i + 2]); 59 | i += 2; 60 | break; 61 | case "-to": 62 | ReadAssert(1); 63 | verTo = HttpUtility.UrlEncode(args[i + 1]); 64 | dirTo = new DirectoryInfo(args[i + 2]); 65 | i += 2; 66 | break; 67 | case "-output_to": 68 | ReadAssert(2); 69 | outputAt = new DirectoryInfo(args[i + 1]); 70 | i += 1; 71 | break; 72 | case "-p": 73 | ReadAssert(3); 74 | prefix = HttpUtility.UrlEncode(args[i + 1]); 75 | i += 1; 76 | break; 77 | case "-reverse": 78 | ReadAssert(4); 79 | createReverse = true; 80 | break; 81 | case "--skip-check": 82 | ReadAssert(5); 83 | performCheck = false; 84 | break; 85 | case "--only-include-pkg-defined-files": 86 | ReadAssert(6); 87 | onlyIncludeDefinedFiles = true; 88 | break; 89 | case "--include-audios": 90 | ReadAssert(7); 91 | includeAudioVersions = true; 92 | break; 93 | default: 94 | Usage(); 95 | return; 96 | } 97 | } 98 | } 99 | else 100 | { 101 | Usage(); 102 | return; 103 | } 104 | #endregion 105 | 106 | #region Input Assert 107 | if (dirFrom == null || dirTo == null || outputAt == null) 108 | { 109 | Log.Erro("Input param lack!"); 110 | Usage(); 111 | Environment.Exit(1); 112 | } 113 | if (!Helper.AnyCertainGameExists(dirFrom)) 114 | { 115 | Log.Warn("WARNING: No known game executable under game path. (verFrom)"); 116 | } 117 | if (!Helper.AnyCertainGameExists(dirTo)) 118 | { 119 | Log.Warn("WARNING: No known game executable under game path. (verTo)"); 120 | } 121 | if (!onlyIncludeDefinedFiles && includeAudioVersions) 122 | { 123 | Log.Erro("--include-audios option is only valid when --only-include-pkg-defined-files option exists!", "InputAssert"); 124 | Environment.Exit(1); 125 | } 126 | #endregion 127 | 128 | if (performCheck) 129 | { 130 | if (!HappyGenyuanImsactUpdate.Program.UpdateCheck(dirFrom, CheckMode.Basic) 131 | || !HappyGenyuanImsactUpdate.Program.UpdateCheck(dirTo, CheckMode.Basic)) 132 | { 133 | Log.Erro("Original files not correct. Not supported in current version."); 134 | Environment.Exit(1); 135 | } 136 | } 137 | 138 | // Take a snapshot of the file system 139 | Log.Info($"Start Emunerating directories, it'll probably take a long time..."); 140 | IEnumerable list1 = dirFrom.GetFiles("*.*", SearchOption.AllDirectories); 141 | IEnumerable list2 = dirTo.GetFiles("*.*", SearchOption.AllDirectories); 142 | 143 | #region Select files (--only-include-pkg-defined-files) 144 | if (onlyIncludeDefinedFiles) 145 | { 146 | List pkgVersions = new() 147 | { Path.GetFullPath("pkg_version", dirFrom.FullName) }; 148 | if (includeAudioVersions) 149 | pkgVersions.AddRange(UpCheck.GetPkgVersion(dirFrom)); 150 | 151 | var definedFiles1 = new SortedSet(( 152 | from pkgVersion in pkgVersions 153 | from json in File.ReadLines(pkgVersion) 154 | let doc = JsonDocument.Parse(json) 155 | let fullName = Path.GetFullPath(dirFrom.FullName + '/' 156 | + doc.RootElement.GetProperty("remoteName").GetString()) 157 | select fullName).Concat( 158 | from pkgVersion in pkgVersions 159 | select Path.Combine(dirFrom.FullName, pkgVersion))); 160 | list1 = from fileInfo in list1 161 | where definedFiles1.Contains(fileInfo.FullName) 162 | select fileInfo; 163 | 164 | pkgVersions = new() 165 | { Path.GetFullPath("pkg_version", dirTo.FullName) }; 166 | if (includeAudioVersions) 167 | pkgVersions.AddRange(UpCheck.GetPkgVersion(dirTo)); 168 | 169 | var definedFiles2 = new SortedSet(( 170 | from pkgVersion in pkgVersions 171 | from json in File.ReadLines(pkgVersion) 172 | let doc = JsonDocument.Parse(json) 173 | let fullName = Path.GetFullPath(dirTo.FullName + '/' 174 | + doc.RootElement.GetProperty("remoteName").GetString()) 175 | select fullName).Concat( 176 | from pkgVersion in pkgVersions 177 | select Path.Combine(dirTo.FullName, pkgVersion))); 178 | list2 = from fileInfo in list2 179 | where definedFiles2.Contains(fileInfo.FullName) 180 | select fileInfo; 181 | } 182 | #endregion 183 | 184 | //A custom file comparer defined below 185 | FileCompare cmp = new FileCompare(dirFrom, dirTo); 186 | 187 | #region Fail Tips 188 | // This query determines whether the two folders contain 189 | // identical file lists, based on the custom file comparer 190 | // that is defined in the FileCompare class. 191 | // The query executes immediately because it returns a bool. 192 | bool areIdentical = list1.SequenceEqual(list2, cmp); 193 | 194 | if (areIdentical == true) 195 | { 196 | Log.Warn("The two folders are the same! Seem not need patch."); 197 | Log.Info($"from {verFrom}: {dirFrom}"); 198 | Log.Info($"to {verTo}: {dirTo}"); 199 | Log.Warn($"Please confirm the paths are true. Press Enter to continue, or Press Ctrl+C to cancel."); 200 | Console.ReadLine(); 201 | } 202 | #endregion 203 | 204 | await CreatePatch(list1, list2, dirFrom, dirTo, 205 | $"{outputAt}\\{prefix}_{verFrom}_{verTo}_hdiff_{Randomstr(16)}.zip", cmp); 206 | 207 | if (createReverse) 208 | { 209 | await CreatePatch(list2, list1, dirTo, dirFrom, 210 | $"{outputAt}\\{prefix}_{verTo}_{verFrom}_hdiff_{Randomstr(16)}.zip", cmp); 211 | } 212 | 213 | #region Multiple Read Assert 214 | void ReadAssert(int expected) 215 | { 216 | if (arghaveread[expected]) 217 | { 218 | Log.Erro("Duplicated param!"); 219 | Usage(); 220 | Environment.Exit(1); 221 | } 222 | arghaveread[expected] = true; 223 | } 224 | #endregion 225 | } 226 | 227 | static void Usage() 228 | { 229 | Log.Info("Usage: hdiffpatchcreator", "CommandLine"); 230 | Log.Info(" -from ", "CommandLine"); 231 | Log.Info(" -to ", "CommandLine"); 232 | Log.Info(" -output_to ", "CommandLine"); 233 | Log.Info(" [-p ] [-reverse] [--skip-check]", "CommandLine"); 234 | Log.Info(" [--only-include-pkg-defined-files [--include-audios]]", "CommandLine"); 235 | Log.Info("", "CommandLine"); 236 | Log.Info("By using this program, you can get a package named: ", "CommandLine"); 237 | Log.Info("[prefix]___hdiff_.zip", "CommandLine"); 238 | Log.Info("e.g. game_3.4_8.0_hdiff_nj89iGjh4d.zip", "CommandLine"); 239 | Log.Info("If not given, prefix will be 'game'.", "CommandLine"); 240 | Log.Info("", "CommandLine"); 241 | Log.Info("-reverse: After package is created, reverse 'versionFrom' and 'versionTo' and create another package.", "CommandLine"); 242 | Log.Info("", "CommandLine"); 243 | Log.Info("--skip-check: skip the check (Basic Mode, only compare file size). ", "CommandLine"); 244 | Log.Info("Notice: For the patch creator, MD5 computing when comparing files is essential.", "CommandLine"); 245 | Log.Info("You can't choose not to use it.", "CommandLine"); 246 | Log.Info("", "CommandLine"); 247 | Log.Info("--only-include-pkg-defined-files: ignore all files not defined in 'pkg_version' file.", "CommandLine"); 248 | Log.Info(" --include-audios: Apply files defined in 'Audio_*_version' with an exception for ignore.", "CommandLine"); 249 | } 250 | 251 | static async Task CreatePatch(IEnumerable filesFrom, IEnumerable filesTo, 252 | DirectoryInfo dirFrom, DirectoryInfo dirTo, string createpakPath, FileCompare cmp) 253 | { 254 | var tmpFilePath = $"{new FileInfo(createpakPath).DirectoryName}\\Temp-{Randomstr(32)}"; 255 | Directory.CreateDirectory(tmpFilePath); 256 | 257 | // Files in From but not in To should be deleted 258 | var fromOnly = filesFrom.Except(filesTo, cmp); 259 | #region deletefiles.txt 260 | StringBuilder strb = new(); 261 | foreach (var file in fromOnly) 262 | { 263 | strb.AppendLine(FileCompare.GetRelativePath(file, dirFrom)); 264 | } 265 | File.WriteAllText($"{tmpFilePath}\\deletefiles.txt", strb.ToString()); 266 | #endregion 267 | 268 | // Files in To but not in From could be directly reserved 269 | var toOnly = filesTo.Except(filesFrom, cmp); 270 | #region Copy 271 | foreach (var file in toOnly) 272 | { 273 | var newfile = new FileInfo($"{tmpFilePath}\\{FileCompare.GetRelativePath(file, dirTo)}"); 274 | CreateDirectoryFor(newfile); 275 | Log.Info($"Copying: {file.FullName} -> {newfile.FullName}", $"{nameof(CreatePatch)}_FileCopy"); 276 | File.Copy(file.FullName, newfile.FullName); 277 | } 278 | #endregion 279 | 280 | // Files in both should create hdiff patch 281 | var queryCommonFiles = filesFrom.Intersect(filesTo, cmp); 282 | #region Hdiff Create 283 | strb = new(); 284 | foreach (var file in queryCommonFiles) 285 | { 286 | var relativePath = FileCompare.GetRelativePath(file, dirFrom); 287 | 288 | var fromPath = new FileInfo($"{dirFrom}\\{relativePath}"); 289 | var toPath = new FileInfo($"{dirTo}\\{relativePath}"); 290 | var diffPath = new FileInfo($"{tmpFilePath}\\{relativePath}.hdiff"); 291 | 292 | if (cmp.RealEqual(fromPath, toPath)) 293 | { 294 | Log.Verb($"Skip: {fromPath.FullName} == {toPath.FullName}", $"{nameof(CreatePatch)}_Hdiff"); 295 | continue; 296 | } 297 | 298 | if (file.Name.EndsWith("pkg_version")) 299 | { 300 | // pkg_versions shouldn't use hdiff 301 | File.Copy(toPath.FullName, $"{tmpFilePath}\\{relativePath}"); 302 | continue; 303 | } 304 | 305 | if (await InvokeHDiffz(fromPath.FullName, toPath.FullName, diffPath.FullName)) 306 | { 307 | if (diffPath.Length >= toPath.Length) 308 | { 309 | // Fallback to diff 310 | Log.Info($"HDiff = {diffPath.Length}, To = {toPath.Length}, fallback to diff", $"{nameof(CreatePatch)}_Hdiff"); 311 | diffPath.Delete(); 312 | File.Copy(toPath.FullName, $"{tmpFilePath}\\{relativePath}"); 313 | } 314 | else 315 | { 316 | // Don't mistake it to diffPath! 317 | strb.AppendLine($"{{\"remoteName\": \"{relativePath.Replace('\\', '/')}\"}}"); 318 | } 319 | } 320 | else 321 | { 322 | Log.Warn($"HDiff failed, fallback to diff, file fromVer: {fromPath.FullName}, toVer: {toPath.FullName}", $"{nameof(CreatePatch)}_Hdiff"); 323 | CreateDirectoryFor(diffPath); 324 | Debug.Assert(false); 325 | File.Copy(toPath.FullName, $"{tmpFilePath}\\{relativePath}"); 326 | } 327 | } 328 | 329 | File.WriteAllText($"{tmpFilePath}\\hdifffiles.txt", strb.ToString()); 330 | #endregion 331 | 332 | #region Write README 333 | string readme = 334 | "This is a hdiff update package created by HappuGenyuanImsactUpdate. \n" + 335 | "For using, you may download our patcher release here: \n" + 336 | "https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/releases\n" + 337 | "Then run Updater\\HappyGenyuanImsactUpdate.exe to perform a update.\n" + 338 | "\n" + 339 | "Have a good day! Thanks for using!"; 340 | File.WriteAllText($"{tmpFilePath}\\README.txt", readme); 341 | #endregion 342 | 343 | #region Create Compressed File 344 | await OuterInvoke.Run(new OuterInvokeInfo 345 | { 346 | ProcessPath = path7z, 347 | CmdLine = $"a -tzip \"{createpakPath}\" \"{tmpFilePath}\\*\" -mmt", 348 | StartingNotice = "Compressing output zip archive...", 349 | AutoTerminateReason = "Output compressing failed. You may retry by yourself." 350 | }, 6); 351 | #endregion 352 | 353 | // Clear temp files 354 | Directory.Delete(tmpFilePath, true); 355 | } 356 | 357 | static async Task InvokeHDiffz(string source, string target, string outdiffpath, int retry = 5) 358 | { 359 | CreateDirectoryFor(new FileInfo(outdiffpath)); 360 | 361 | return await OuterInvoke.Run(new OuterInvokeInfo 362 | { 363 | ProcessPath = hdiffzPath, 364 | CmdLine = $"-f \"{source}\" \"{target}\" \"{outdiffpath}\"" 365 | }, max_rerun: retry) == 0; 366 | } 367 | 368 | #region Random 369 | static Random ran = new Random(); 370 | 371 | static string Randomstr(int len) 372 | { 373 | string charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"; 374 | Debug.Assert(charset.Length == 62); 375 | string res = ""; 376 | while (len-- > 0) 377 | { 378 | res += charset[ran.Next(0, 61)]; 379 | } 380 | return res; 381 | } 382 | #endregion 383 | 384 | private static void CreateDirectoryFor(FileInfo file) 385 | { 386 | if (file.DirectoryName == null) return; 387 | Directory.CreateDirectory(file.DirectoryName); 388 | } 389 | } 390 | 391 | internal class FileCompare : IEqualityComparer 392 | { 393 | public FileCompare(DirectoryInfo dir1, DirectoryInfo dir2) 394 | { 395 | rel1 = dir1; 396 | rel2 = dir2; 397 | } 398 | 399 | #region Relative Path 400 | DirectoryInfo rel1, rel2; 401 | 402 | public static string GetRelativePath(FileInfo info, DirectoryInfo relative_path_start_from) 403 | { 404 | string filePath = info.FullName; 405 | string dirPath = relative_path_start_from.FullName; 406 | 407 | if (!filePath.StartsWith(dirPath)) 408 | throw new ArgumentException("File not in directory so can't create relative path."); 409 | 410 | return filePath.Remove(0, dirPath.Length + 1); 411 | } 412 | 413 | public static bool TryGetRelativePath(FileInfo info, 414 | DirectoryInfo relative_path_start_from, out string rtn) 415 | { 416 | string filePath = info.FullName; 417 | string dirPath = relative_path_start_from.FullName; 418 | 419 | if (!filePath.StartsWith(dirPath)) 420 | { 421 | rtn = "File not in directory so can't create relative path."; 422 | return false; 423 | } 424 | rtn = filePath.Remove(0, dirPath.Length + 1); 425 | return true; 426 | } 427 | #endregion 428 | 429 | public bool Equals(FileInfo? f1, FileInfo? f2) 430 | { 431 | if (f1 == null && f2 == null) return true; 432 | if (f1 == null || f2 == null) return false; 433 | bool tryf1 = TryGetRelativePath(f1, rel1, out string relf1); 434 | if (!tryf1) 435 | { 436 | relf1 = GetRelativePath(f1, rel2); 437 | } 438 | bool tryf2 = TryGetRelativePath(f2, rel2, out string relf2); 439 | if (!tryf2) 440 | { 441 | relf2 = GetRelativePath(f2, rel1); 442 | } 443 | return relf1 == relf2; 444 | } 445 | 446 | private Dictionary _MD5memory = new(); 447 | 448 | /// 449 | /// MD5 memory. Invoke to get a file's MD5, so you can get it without read file again next time. 450 | /// 451 | /// File's Full path 452 | /// MD5 value of this file 453 | public string this[string filePath] 454 | { 455 | get 456 | { 457 | lock (this) 458 | { 459 | if (!_MD5memory.ContainsKey(filePath)) 460 | { 461 | Log.Info($"Start Computing MD5 of: {filePath}"); 462 | _MD5memory.Add(filePath, MyMD5.GetMD5HashFromFile(filePath)); 463 | } 464 | return _MD5memory[filePath]; 465 | } 466 | } 467 | } 468 | 469 | public bool RealEqual(FileInfo? a, FileInfo? b) 470 | { 471 | if (a == null && b == null) return true; 472 | if (a == null || b == null) return false; 473 | Debug.Assert(a.Name == b.Name); 474 | if (a.Length != b.Length) return false; 475 | return this[a.FullName] == this[b.FullName]; 476 | } 477 | 478 | // Return a hash that reflects the comparison criteria. According to the 479 | // rules for IEqualityComparer, if Equals is true, then the hash codes must 480 | // also be equal. Because equality as defined here is a simple value equality, not 481 | // reference identity, it is possible that two or more objects will produce the same 482 | // hash code. 483 | public int GetHashCode(FileInfo fi) 484 | { 485 | bool tryf1 = TryGetRelativePath(fi, rel1, out string relf1); 486 | if (!tryf1) 487 | { 488 | relf1 = GetRelativePath(fi, rel2); 489 | } 490 | return relf1.GetHashCode(); 491 | } 492 | } 493 | } -------------------------------------------------------------------------------- /HappyGenyuanImsactUpdate/Program.cs: -------------------------------------------------------------------------------- 1 | /*******HappyGenyuanImsactUpdate*******/ 2 | // A hdiff-using update program of a certain anime game. 3 | 4 | using System.Reflection; 5 | using YYHEggEgg.Logger; 6 | using YYHEggEgg.Utils; 7 | 8 | namespace HappyGenyuanImsactUpdate 9 | { 10 | public class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | Log.Initialize(new LoggerConfig( 15 | max_Output_Char_Count: -1, 16 | use_Console_Wrapper: false, 17 | use_Working_Directory: false, 18 | global_Minimum_LogLevel: LogLevel.Verbose, 19 | console_Minimum_LogLevel: LogLevel.Information, 20 | debug_LogWriter_AutoFlush: true)); 21 | 22 | string? version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3); 23 | Log.Info($"Welcome to the update program! (v{version ?? ""})"); 24 | Helper.CheckForRunningInZipFile(); 25 | 26 | //Not working path, but the path where the program located 27 | Helper.CheckForTools(); 28 | 29 | var path7z = $"{Helper.exePath}\\7z.exe"; 30 | var hpatchzPath = $"{Helper.exePath}\\hpatchz.exe"; 31 | 32 | #region Variables 33 | DirectoryInfo? datadir = null; 34 | Patch patch; 35 | CheckMode checkAfter = CheckMode.Null; 36 | int t = 0; 37 | List zips = new(); 38 | bool ifconfigchange = true; 39 | bool? ifdeletepackage = null; 40 | bool[] arghaveread = new bool[6]; 41 | 42 | bool usingcommandline = false; 43 | #endregion 44 | 45 | #region Console Usage 46 | if (args.Length == 0) 47 | { 48 | Log.Info("You can also use command line args to execute this program.", "CommandLine"); 49 | 50 | datadir = GetDataPath(); 51 | 52 | Log.Info(""); 53 | // 0 -> none, 1 -> basic check (file size), 2 -> full check (size + md5) 54 | checkAfter = (CheckMode)AskForCheck(); 55 | 56 | Log.Info(""); 57 | 58 | if (!PkgVersionCheck(datadir, checkAfter)) 59 | { 60 | Log.Erro("Sorry, the update process was exited because the original files aren't correct.", nameof(PkgVersionCheck)); 61 | Log.Erro("Press any key to continue. ", nameof(PkgVersionCheck)); 62 | Console.Read(); 63 | Environment.Exit(1); 64 | } 65 | else Log.Info("Congratulations! Check passed!", nameof(PkgVersionCheck)); 66 | 67 | t = GetZipCount(); 68 | 69 | Log.Info(""); 70 | 71 | for (int i = 0; i < t; i++) 72 | { 73 | Log.Info(""); 74 | if (i > 0) Log.Info("Now you should paste the path of another zip file.", nameof(GetUpdatePakPath)); 75 | zips.Add(GetUpdatePakPath(datadir.FullName)); 76 | } 77 | } 78 | #endregion 79 | #region Command LIne Usage 80 | else 81 | { 82 | usingcommandline = true; 83 | 84 | #region Remove '.\' 85 | for (int i = 0; i < args.Length; i++) 86 | { 87 | if (args[i].StartsWith('.')) 88 | args[i] = args[i].Substring(1); 89 | } 90 | #endregion 91 | 92 | for (int i = 0; i < args.Length; i++) 93 | { 94 | args[i] = RemoveDoubleQuotes(args[i]) ?? string.Empty; 95 | } 96 | 97 | if (args.Length > 0) 98 | { 99 | for (int i = 0; i < args.Length; i++) 100 | { 101 | switch (args[i]) 102 | { 103 | case "-patchAt": 104 | ReadAssert(0); 105 | datadir = new DirectoryInfo(args[i + 1]); 106 | i += 1; 107 | break; 108 | case "-checkmode": 109 | ReadAssert(1); 110 | checkAfter = (CheckMode)int.Parse(args[i + 1]); 111 | i += 1; 112 | break; 113 | case "-zip_count": 114 | ReadAssert(2); 115 | t = int.Parse(args[i + 1]); 116 | for (int j = 0; j < t; j++) 117 | { 118 | zips.Add(new FileInfo(args[i + 2 + j])); 119 | } 120 | i += t + 1; 121 | break; 122 | case "--config_change_guidance": 123 | ReadAssert(3); 124 | ifconfigchange = bool.Parse(args[i + 1]); 125 | i += 1; 126 | break; 127 | case "--delete_update_packages": 128 | ReadAssert(4); 129 | ifdeletepackage = true; 130 | break; 131 | default: 132 | Usage(); 133 | return; 134 | } 135 | } 136 | } 137 | else 138 | { 139 | Usage(); 140 | return; 141 | } 142 | 143 | ifdeletepackage ??= false; 144 | } 145 | #endregion 146 | 147 | #region Input lost Assert 148 | if (datadir == null || checkAfter == CheckMode.Null || t == 0) 149 | { 150 | Usage(); 151 | throw new ArgumentException("Input param lack!"); 152 | } 153 | #endregion 154 | 155 | patch = new Patch(datadir, path7z, hpatchzPath); 156 | 157 | // Backup the original pkg_version file 158 | var pkgversionpaths = UpCheck.GetPkgVersion(datadir); 159 | foreach (var pkgversionpath in pkgversionpaths) 160 | { 161 | FileInfo pkgver = new(pkgversionpath); 162 | if (pkgver.Name == "pkg_version") continue; 163 | File.Move(pkgversionpath, $"{Helper.tempPath}\\{pkgver.Name}"); 164 | } 165 | 166 | // Due to some reasons, if the deleted files are not there, 167 | // we'll try to delete them afterwards. 168 | List delete_delays = new(); 169 | 170 | foreach (var zipfile in zips) 171 | { 172 | #region Unzip the package 173 | // NOTE: Because some dawn packages from gdrive has a sub folder, we should move it back. 174 | // Record the directories now 175 | var predirs = Directory.GetDirectories(datadir.FullName); 176 | 177 | await OuterInvoke.Run(new OuterInvokeInfo 178 | { 179 | ProcessPath = path7z, 180 | CmdLine = $"x \"{zipfile.FullName}\" -o\"{datadir.FullName}\" -aoa -bsp1", 181 | StartingNotice = "Unzip the package...", 182 | AutoTerminateReason = $"7z decompress package: {zipfile.FullName} to {datadir.FullName} failed." 183 | }, 3750); 184 | 185 | Unzipped.MoveBackSubFolder(datadir, predirs); 186 | #endregion 187 | 188 | await patch.Hdiff(); 189 | delete_delays.AddRange(patch.DeleteFiles()); 190 | 191 | Log.Info("\n\n"); 192 | } 193 | 194 | // For some reasons, the package check is delayed to the end. 195 | // It is a proper change because only the newest pkg_version is valid. 196 | if (!UpdateCheck(datadir, checkAfter)) 197 | { 198 | Helper.ShowErrorBalloonTip(5000, "Update failed.", 199 | "Sorry, the update process was exited because files aren't correct."); 200 | 201 | Log.Erro("Sorry, the update process was exited because the original files aren't correct.", nameof(PkgVersionCheck)); 202 | Log.Erro("Press any key to continue. ", nameof(PkgVersionCheck)); 203 | Console.Read(); 204 | Environment.Exit(1); 205 | } 206 | else Log.Info("Congratulations! Check passed!", nameof(PkgVersionCheck)); 207 | 208 | foreach (var pkgversionpath in pkgversionpaths) 209 | { 210 | FileInfo pkgver = new(pkgversionpath); 211 | if (pkgver.Name == "pkg_version") continue; 212 | if (pkgver.Exists) continue; // pkg_version Overrided 213 | 214 | var backuppath = $"{Helper.tempPath}\\{pkgver.Name}"; 215 | File.Move(backuppath, pkgversionpath); 216 | if (checkAfter == CheckMode.None) 217 | { 218 | Log.Warn($"{pkgver.Name} hasn't checked and may not fit the current version."); 219 | continue; 220 | } 221 | 222 | var checkres = UpCheck.CheckByPkgVersion(datadir, pkgversionpath, checkAfter); 223 | 224 | if (!checkres) 225 | { 226 | Log.Warn($"{pkgver.Name} isn't fit with current version any more. You may fix the error or remove the file under the game data directory."); 227 | } 228 | } 229 | 230 | Log.Info("\n\n\n\n\n---------------------\n\n\n\n\n"); 231 | 232 | // Change the config.ini of official launcher 233 | if ((usingcommandline && ifconfigchange) || !usingcommandline) 234 | ConfigChange(datadir, zips[0], zips[zips.Count - 1]); 235 | 236 | // Handling with delayed deletions 237 | foreach (var deletedfile in delete_delays) 238 | if (File.Exists(deletedfile)) 239 | File.Delete(deletedfile); 240 | 241 | Helper.ShowInformationBalloonTip(5000, "Update process is done!", "Enjoy the new version!"); 242 | 243 | DeleteZipFilesReq(zips, ifdeletepackage); 244 | Log.Info("-------------------------"); 245 | 246 | Helper.TryDisposeTempFiles(); 247 | 248 | Log.Info("Update process is done!"); 249 | 250 | if (args.Length == 0) 251 | { 252 | Log.Info("Press Enter to continue."); 253 | 254 | Console.ReadLine(); 255 | } 256 | else 257 | { 258 | Log.Info("The program will exit in 3 seconds."); 259 | await Task.Delay(3000); 260 | } 261 | 262 | #region Multiple Read Assert 263 | void ReadAssert(int expected) 264 | { 265 | if (arghaveread[expected]) 266 | { 267 | Log.Info("Duplicated param!"); 268 | Usage(); 269 | Environment.Exit(1); 270 | } 271 | arghaveread[expected] = true; 272 | } 273 | #endregion 274 | } 275 | 276 | private static void Usage() 277 | { 278 | Log.Info("CommandLine usage: \n" + 279 | "happygenyuanimsactupdate \n" + 280 | "-patchAt \n" + 281 | "-checkmode <0/1/2> (0 -> none, 1 -> basic check (file size), 2 -> full check (size + md5))\n" + 282 | "-zip_count \n" + 283 | "[--config_change_guidance ] (change the showing version of official launcher, default is true)\n" + 284 | "[--delete_update_packages] (delete update packages, won't delete if the param isn't given)" + 285 | "\n\n" + 286 | "e.g. happygenyuanimsactupdate -patchAt \"D:\\Game\" -checkmode 1 -zip_count 2 \"game_1_hdiff.zip\" \"zh-cn_hdiff.zip\" " + 287 | "--config_change_guidance false\n", "CommandLine"); 288 | } 289 | 290 | #region Change config for official launcher 291 | /// 292 | /// Change config for official launcher 293 | /// 294 | /// Game Data dir 295 | /// Used for infering the update version 296 | /// Used for infering the update version 297 | public static void ConfigChange(DirectoryInfo datadir, FileInfo zipstart, FileInfo zipend) 298 | { 299 | if (!File.Exists($"{datadir}\\config.ini")) return; 300 | 301 | Helper.ShowInformationBalloonTip(5000, "The update program needs you decision.", 302 | "You need to apply change to the Launcher config in the console."); 303 | 304 | Log.Info("We have noticed that you're probably using an official launcher.", nameof(ConfigChange)); 305 | Log.Info("To make it display the correct version, we would make some change on related file.", nameof(ConfigChange)); 306 | 307 | string verstart = ConfigIni.FindStartVersion(zipstart.Name); 308 | string verto = ConfigIni.FindToVersion(zipend.Name); 309 | 310 | FileInfo configfile = new($"{datadir}\\config.ini"); 311 | 312 | if (verstart == string.Empty || verto == string.Empty) 313 | { 314 | Log.Warn("We can't infer the version you're updating to.", nameof(ConfigChange)); 315 | CustomChangeVersion(configfile); 316 | } 317 | else 318 | { 319 | GetConfigUpdateOptions(configfile, verstart, verto); 320 | } 321 | } 322 | 323 | /// 324 | /// Ask user for applying the inferred update options 325 | /// 326 | /// config.ini 327 | /// the update version 328 | /// the update version 329 | public static void GetConfigUpdateOptions(FileInfo configfile, string verstart, string verto) 330 | { 331 | Log.Info($"We infer that you're updating from {verstart} to {verto} .", nameof(ConfigChange)); 332 | Log.Info("Is it true? Type 'y' to apply the change " + 333 | "or type the correct version you're updating to.", nameof(ConfigChange)); 334 | Log.Info("If you don't use a launcher or don't want to change the display version, type 'n' to refuse it.", nameof(ConfigChange)); 335 | string? s = Console.ReadLine(); 336 | if (s == null || s == string.Empty) 337 | { 338 | Log.Warn("Invaild version!", nameof(ConfigChange)); 339 | CustomChangeVersion(configfile); 340 | } 341 | else if (s.ToLower() == "y") 342 | ConfigIni.ApplyConfigChange(configfile, verto); 343 | else if (s.ToLower() == "n") return; 344 | else if (ConfigIni.VerifyVersionString(s)) 345 | ConfigIni.ApplyConfigChange(configfile, s); 346 | else 347 | { 348 | Log.Warn("Invaild version!", nameof(ConfigChange)); 349 | CustomChangeVersion(configfile); 350 | } 351 | } 352 | 353 | /// 354 | /// Type a custom version for update 355 | /// 356 | /// config.ini 357 | public static void CustomChangeVersion(FileInfo configfile) 358 | { 359 | Log.Info("Please type the version you're updating to, and we'll apply the change:", nameof(CustomChangeVersion)); 360 | Log.Info("If you don't use a launcher or don't want to change the display version, type 'n' to refuse it.", nameof(CustomChangeVersion)); 361 | 362 | string? s = Console.ReadLine(); 363 | if (s == null || s == string.Empty) 364 | { 365 | Log.Warn("Invaild version!", nameof(CustomChangeVersion)); 366 | CustomChangeVersion(configfile); 367 | } 368 | else if (s.ToLower() == "n") return; 369 | else if (ConfigIni.VerifyVersionString(s)) 370 | ConfigIni.ApplyConfigChange(configfile, s); 371 | else 372 | { 373 | Log.Warn("Invaild version!", nameof(CustomChangeVersion)); 374 | CustomChangeVersion(configfile); 375 | } 376 | } 377 | #endregion 378 | 379 | #region Package Verify 380 | public static bool UpdateCheck(DirectoryInfo datadir, CheckMode checkAfter) 381 | { 382 | Log.Info("Start verifying...\n", nameof(UpdateCheck)); 383 | 384 | if (checkAfter == CheckMode.None) 385 | { 386 | Log.Info("Due to user's demanding, no checks are performed.", nameof(UpdateCheck)); 387 | return true; 388 | } 389 | 390 | var pkgversionPaths = UpCheck.GetPkgVersion(datadir); 391 | if (pkgversionPaths == null || pkgversionPaths.Count == 0) 392 | { 393 | Log.Info("Can't find version file. No checks are performed.", nameof(UpdateCheck)); 394 | Log.Info("If you can find it, please tell to us: " + 395 | "https://github.com/YYHEggEgg/HappyGenyuanImsactUpdate/issues", nameof(UpdateCheck)); 396 | return true; 397 | } 398 | 399 | return UpCheck.CheckByPkgVersion(datadir, pkgversionPaths, checkAfter); 400 | } 401 | 402 | // Check if pkg_version and Audio_pkg_version can match the real condition 403 | static bool PkgVersionCheck(DirectoryInfo datadir, CheckMode checkAfter) 404 | { 405 | if (checkAfter == CheckMode.None) 406 | { 407 | Log.Info("No checks are performed.", nameof(PkgVersionCheck)); 408 | return true; 409 | } 410 | 411 | var pkgversionPaths = UpCheck.GetPkgVersion(datadir); 412 | if (!pkgversionPaths.Contains($"{datadir}\\pkg_version")) 413 | { 414 | Log.Warn($"Can't find pkg_version file. No checks are performed.", nameof(PkgVersionCheck)); 415 | Log.Info($"It's normal if you're attempting to update Honkai: March 7th.", nameof(PkgVersionCheck)); 416 | return true; 417 | } 418 | 419 | // ...\??? game\???_Data\StreamingAssets\Audio\GeneratedSoundBanks\Windows 420 | string old_audio1 = $@"{datadir.FullName}\{Helper.certaingames[0]}_Data\StreamingAssets\Audio\GeneratedSoundBanks\Windows"; 421 | string old_audio2 = $@"{datadir.FullName}\{Helper.certaingames[1]}_Data\StreamingAssets\Audio\GeneratedSoundBanks\Windows"; 422 | string[]? audio_pkgversions = null; 423 | if (Directory.Exists(old_audio1)) audio_pkgversions = Directory.GetDirectories(old_audio1); 424 | else if (Directory.Exists(old_audio2)) audio_pkgversions = Directory.GetDirectories(old_audio2); 425 | else // ver >= 3.6 426 | { 427 | // ...\??? game\???_Data\StreamingAssets\AudioAssets 428 | string new_audio1 = $@"{datadir.FullName}\{Helper.certaingames[0]}_Data\StreamingAssets\AudioAssets"; 429 | string new_audio2 = $@"{datadir.FullName}\{Helper.certaingames[1]}_Data\StreamingAssets\Audio\AudioAssets"; 430 | 431 | if (Directory.Exists(new_audio1)) audio_pkgversions = Directory.GetDirectories(new_audio1); 432 | else if (Directory.Exists(new_audio2)) audio_pkgversions = Directory.GetDirectories(new_audio2); 433 | else return UpdateCheck(datadir, checkAfter); 434 | } 435 | 436 | foreach (string audiopath in audio_pkgversions) 437 | { 438 | string audioname = new DirectoryInfo(audiopath).Name; 439 | if (!pkgversionPaths.Contains($"{datadir}\\Audio_{audioname}_pkg_version")) 440 | { 441 | // ver <= 1.4 442 | Log.Warn($"Not checking Audio: {audioname} for Audio_{audioname}_pkg_version does not exist.", nameof(PkgVersionCheck)); 443 | } 444 | } 445 | 446 | return UpdateCheck(datadir, checkAfter); 447 | } 448 | #endregion 449 | 450 | #region Param Getting 451 | //For standarlizing, we use a DirectoryInfo object. 452 | //The same goes for the following methods. 453 | static DirectoryInfo GetDataPath() 454 | { 455 | Log.Info("Paste the full path of game directory here. " + 456 | "It's usually ended with \"Genyuan Imsact game\".", nameof(GetDataPath)); 457 | string? dataPath = RemoveDoubleQuotes(Console.ReadLine()); 458 | if (dataPath == null || dataPath == string.Empty) 459 | { 460 | Log.Warn("Invaild game path!", nameof(GetDataPath)); 461 | return GetDataPath(); 462 | } 463 | 464 | DirectoryInfo datadir = new(dataPath); 465 | if (!Helper.AnyCertainGameExists(datadir)) 466 | { 467 | Log.Warn("No known game executable found under directory. Do you believe the path is correct? Type 'y' to confirm.", nameof(GetDataPath)); 468 | if (Console.ReadLine()?.ToLower() == "y") return datadir; 469 | return GetDataPath(); 470 | } 471 | else return datadir; 472 | } 473 | 474 | static FileInfo GetUpdatePakPath(string gamePath) 475 | { 476 | Log.Info("Drag the update package here. " + 477 | "It should be a zip file.", nameof(GetUpdatePakPath)); 478 | Log.Info("If it's under the game directory, you can just paste the name of zip file here.", nameof(GetUpdatePakPath)); 479 | string? pakPath = RemoveDoubleQuotes(Console.ReadLine()); 480 | if (pakPath == null || pakPath == string.Empty) 481 | { 482 | Log.Warn("Invaild update package!", nameof(GetUpdatePakPath)); 483 | return GetUpdatePakPath(gamePath); 484 | } 485 | 486 | FileInfo zipfile = new(pakPath); 487 | 488 | // Fuck why I have tested this 489 | if (pakPath.Length >= 3) 490 | if (pakPath.Substring(1, 2) != ":\\") 491 | { 492 | //Support relative path 493 | pakPath = $"{gamePath}\\{pakPath}"; 494 | zipfile = new(pakPath); 495 | } 496 | 497 | //To protect fools who really just paste its name 498 | if (zipfile.Extension != ".zip" 499 | || zipfile.Extension != ".rar" 500 | || zipfile.Extension != ".7z" 501 | || zipfile.Extension != ".001") 502 | { 503 | if (File.Exists($"{pakPath}.zip")) pakPath += ".zip"; 504 | else if (File.Exists($"{pakPath}.rar")) pakPath += ".rar"; 505 | else if (File.Exists($"{pakPath}.7z")) pakPath += ".7z"; 506 | else if (File.Exists($"{pakPath}.001")) pakPath += ".001"; 507 | zipfile = new(pakPath); 508 | } 509 | 510 | if (!zipfile.Exists) 511 | { 512 | Log.Warn("Invaild update package!", nameof(GetUpdatePakPath)); 513 | return GetUpdatePakPath(gamePath); 514 | } 515 | 516 | return zipfile; 517 | } 518 | 519 | static int GetZipCount() 520 | { 521 | int rtn = 0; 522 | Log.Info("Please type the count of zip file you have.", nameof(GetUpdatePakPath)); 523 | if (!int.TryParse(Console.ReadLine(), out rtn)) 524 | { 525 | Log.Warn("Invaild input!", nameof(GetUpdatePakPath)); 526 | return GetZipCount(); 527 | } 528 | else return rtn; 529 | } 530 | 531 | // 0 -> none, 1 -> basic check (file size), 2 -> full check (size + md5) 532 | static int AskForCheck() 533 | { 534 | Log.Info("Do you want to have a check after updating?", nameof(AskForCheck)); 535 | Log.Info("If you don't want any check, type 0;", nameof(AskForCheck)); 536 | Log.Info("For a fast check (recommended, only compares file size, usually < 10s), type 1;", nameof(AskForCheck)); 537 | Log.Info("For a full check (scans files, takes a long time, usually > 5 minutes), type 2.", nameof(AskForCheck)); 538 | int rtn = 0; 539 | if (!int.TryParse(Console.ReadLine(), out rtn)) 540 | { 541 | Log.Warn("Invaild input!", nameof(AskForCheck)); 542 | return AskForCheck(); 543 | } 544 | else if (rtn < 0 || rtn > 2) 545 | { 546 | Log.Warn("Invaild input!", nameof(AskForCheck)); 547 | return AskForCheck(); 548 | } 549 | else return rtn; 550 | } 551 | #endregion 552 | 553 | #region Delete Update Zip File 554 | /// true=delete; false=reserve; null=not given, ask the user 555 | static void DeleteZipFilesReq(List zips, bool? delete = null) 556 | { 557 | if (delete == null) 558 | { 559 | Log.Info("The pre-download packages aren't needed any more.", nameof(DeleteZipFilesReq)); 560 | Log.Info("Do you want to delete them? Type 'y' to accept or 'n' to refuse.", nameof(DeleteZipFilesReq)); 561 | string? s = Console.ReadLine(); 562 | if (s == null) 563 | { 564 | Log.Warn("Invaild input!", nameof(DeleteZipFilesReq)); 565 | DeleteZipFilesReq(zips); 566 | return; 567 | } 568 | else if (s.ToLower() == "y") 569 | { 570 | foreach (var zip in zips) 571 | { 572 | try 573 | { 574 | zip.Delete(); 575 | } 576 | catch (Exception ex) 577 | { 578 | Log.Warn(ex.ToString(), nameof(DeleteZipFilesReq)); 579 | Log.Warn($"Deleting package: {zip} failed. " + 580 | $"You may close and delete it yourself.", 581 | nameof(DeleteZipFilesReq)); 582 | } 583 | } 584 | } 585 | else if (s.ToLower() == "n") 586 | { 587 | return; 588 | } 589 | else 590 | { 591 | Log.Warn("Invaild input!", nameof(DeleteZipFilesReq)); 592 | DeleteZipFilesReq(zips); 593 | return; 594 | } 595 | } 596 | else if ((bool)delete) 597 | { 598 | foreach (var zip in zips) 599 | { 600 | try 601 | { 602 | zip.Delete(); 603 | } 604 | catch (Exception ex) 605 | { 606 | Log.Warn(ex.ToString(), nameof(DeleteZipFilesReq)); 607 | Log.Warn($"Deleting package: {zip} failed. " + 608 | $"You may close and delete it yourself.", 609 | nameof(DeleteZipFilesReq)); 610 | } 611 | } 612 | } 613 | } 614 | #endregion 615 | 616 | public static string? RemoveDoubleQuotes(string? str) 617 | { 618 | if (str == null) return null; 619 | if (str.StartsWith('"') && str.EndsWith('"')) 620 | return str.Substring(1, str.Length - 2); 621 | else return str; 622 | } 623 | } 624 | } --------------------------------------------------------------------------------