1- using CollapseLauncher . Helper . Metadata ;
1+ using CollapseLauncher . GameVersioning ;
2+ using CollapseLauncher . Helper . Metadata ;
23using CollapseLauncher . Helper . StreamUtility ;
4+ using CollapseLauncher . InstallManager . Base ;
35using CollapseLauncher . Interfaces ;
6+ using CollapseLauncher . Statics ;
47using Hi3Helper ;
58using Hi3Helper . EncTool . Parser . AssetIndex ;
69using Hi3Helper . Http ;
@@ -21,7 +24,117 @@ namespace CollapseLauncher.InstallManager.Genshin;
2124#nullable enable
2225internal sealed partial class GenshinInstall
2326{
24- protected override async Task < string > DownloadPkgVersion ( DownloadClient downloadClient , RegionResourceVersion ? packageLatestBase )
27+ protected override async Task < string > DownloadPkgVersion ( DownloadClient downloadClient ,
28+ RegionResourceVersion ? _ )
29+ {
30+ // First, build the fake pkg_version from Sophon
31+ string pkgVersionPath = await DownloadPkgVersionStatic ( downloadClient . GetHttpClient ( ) ,
32+ GameVersionManager ,
33+ GamePath ,
34+ _gameAudioLangListPathStatic ,
35+ token : Token . Token ) ;
36+
37+ // Second, build persistent manifest from game_res dispatcher.
38+ // If game repair is not available, then skip.
39+ GamePresetProperty presetProperty = GamePropertyVault . GetCurrentGameProperty ( ) ;
40+ if ( presetProperty . GameRepair is not GenshinRepair genshinRepairInstance )
41+ {
42+ return pkgVersionPath ;
43+ }
44+
45+ // Call and borrow persistent manifest method from GenshinRepair instance
46+ await genshinRepairInstance . BuildPersistentManifest ( downloadClient ,
47+ null ,
48+ [ ] ,
49+ [ ] ,
50+ Token . Token ) ;
51+
52+ return pkgVersionPath ;
53+ }
54+
55+ protected override async ValueTask ParsePkgVersions2FileInfo ( List < LocalFileInfo > pkgFileInfo ,
56+ HashSet < string > pkgFileInfoHashSet ,
57+ CancellationToken token )
58+ {
59+ // Parse primary pkg_version as main manifest
60+ await base . ParsePkgVersions2FileInfo ( pkgFileInfo ,
61+ pkgFileInfoHashSet ,
62+ token ) ;
63+
64+ string ? execPrefix = Path . GetFileNameWithoutExtension ( GameVersionManager . GamePreset . GameExecutableName ) ;
65+ if ( string . IsNullOrEmpty ( execPrefix ) )
66+ {
67+ return ;
68+ }
69+
70+ string basePersistentPath = $ "{ execPrefix } _Data\\ Persistent";
71+ string baseStreamingAssetsPath = $ "{ execPrefix } _Data\\ StreamingAssets";
72+ string persistentFolder = Path . Combine ( GamePath , basePersistentPath ) ;
73+
74+ List < string > ? audioLangList = ( GameVersionManager as GameTypeGenshinVersion ) ? . AudioVoiceLanguageList ;
75+ string audioLangListPath = Path . Combine ( GamePath , basePersistentPath , "audio_lang_14" ) ;
76+
77+ // Then add additional parsing to persistent manifests (provided by dispatcher)
78+ string persistentResVersions = Path . Combine ( persistentFolder , "res_versions_persist" ) ;
79+ if ( File . Exists ( persistentResVersions ) )
80+ {
81+ await ParsePersistentManifest2FileInfo ( GamePath ,
82+ asset => Path . Combine ( baseStreamingAssetsPath ,
83+ GenshinRepair . GetParentFromAssetRelativePath ( asset . RelativePath ,
84+ out _ ) ) ,
85+ persistentResVersions ,
86+ pkgFileInfo ,
87+ pkgFileInfoHashSet ,
88+ token ) ;
89+ }
90+
91+ // Filter unnecessary files
92+ if ( audioLangList != null )
93+ {
94+ GenshinRepair . EliminateUnnecessaryAssetIndex ( audioLangListPath , audioLangList , pkgFileInfo , '\\ ' , x => x . FullPath ) ;
95+ }
96+ }
97+
98+ private static async Task ParsePersistentManifest2FileInfo ( string baseGamePath ,
99+ Func < LocalFileInfo , string ? > ? assetGetMiddlePath ,
100+ string pkgFilePath ,
101+ List < LocalFileInfo > pkgFileInfo ,
102+ HashSet < string > pkgFileInfoHashSet ,
103+ CancellationToken token )
104+ {
105+ using StreamReader reader = File . OpenText ( pkgFilePath ) ;
106+ while ( await reader . ReadLineAsync ( token ) is { } line )
107+ {
108+ LocalFileInfo ? localFileInfo = line . Deserialize ( LocalFileInfoJsonContext . Default . LocalFileInfo ) ;
109+
110+ // If null, then go to next line
111+ if ( localFileInfo == null )
112+ continue ;
113+
114+ string ? middlePath = assetGetMiddlePath ? . Invoke ( localFileInfo ) ;
115+ string fullBasePath = string . IsNullOrEmpty ( middlePath ) ? baseGamePath : Path . Combine ( baseGamePath , middlePath ) ;
116+
117+ localFileInfo . FullPath = Path . Combine ( fullBasePath , localFileInfo . RelativePath ) ;
118+ localFileInfo . FileName = Path . GetFileName ( localFileInfo . RelativePath ) ;
119+ localFileInfo . IsFileExist = File . Exists ( localFileInfo . FullPath ) ;
120+
121+ string relativePathNoBase = Path . Combine ( middlePath ?? "" , localFileInfo . RelativePath ) ;
122+
123+ // Add it to the list and hashset (if it's not registered yet)
124+ if ( pkgFileInfoHashSet . Add ( relativePathNoBase ) )
125+ {
126+ pkgFileInfo . Add ( localFileInfo ) ;
127+ }
128+ }
129+ }
130+
131+ public static async Task < string > DownloadPkgVersionStatic ( HttpClient client ,
132+ IGameVersion gameVersionManager ,
133+ string gameBasePath ,
134+ string gameAudioListPath ,
135+ SophonChunkManifestInfoPair ? manifestInfoPair = null ,
136+ List < SophonAsset > ? outputAssetList = null ,
137+ CancellationToken token = default )
25138 {
26139 const string errorRaiseMsg = "Please raise this issue to our Github Repo or Official Discord" ;
27140
@@ -31,8 +144,7 @@ protected override async Task<string> DownloadPkgVersion(DownloadClient download
31144 // by making a fake JSON response of pkg_version by fetching references from Sophon manifest.
32145
33146 // Get GameVersionManager and GamePreset
34- IGameVersion gameVersion = GameVersionManager ;
35- PresetConfig gamePreset = gameVersion . GamePreset ;
147+ PresetConfig gamePreset = gameVersionManager . GamePreset ;
36148 SophonChunkUrls ? branchResources = gamePreset . LauncherResourceChunksURL ;
37149
38150 // If branchResources is null, throw
@@ -42,25 +154,34 @@ protected override async Task<string> DownloadPkgVersion(DownloadClient download
42154 }
43155
44156 // Try to get client and manifest info pair
45- HttpClient client = downloadClient . GetHttpClient ( ) ;
46- SophonChunkManifestInfoPair manifestInfoPair = await SophonManifest
157+ manifestInfoPair ??= await SophonManifest
47158 . CreateSophonChunkManifestInfoPair ( client ,
48159 branchResources . MainUrl ,
49160 branchResources . MainBranchMatchingField ,
50- Token . Token ) ;
161+ token ) ;
51162
52163 // Throw if main manifest pair is not found
53164 if ( ! manifestInfoPair . IsFound )
54165 {
55- throw new InvalidOperationException ( $ "Sophon main manifest info pair is not found! " + errorRaiseMsg ) ;
166+ throw new InvalidOperationException ( "Sophon main manifest info pair is not found! " + errorRaiseMsg ) ;
56167 }
57168
169+ // Create the main pkg_version
170+ await CreateFakePkgVersionFromSophon ( client ,
171+ gameBasePath ,
172+ manifestInfoPair ,
173+ outputAssetList ,
174+ "pkg_version" ,
175+ token ) ;
176+
58177 // Get the existing voice-over matching fields
59178 List < string > availableVaMatchingFields = [ ] ;
60- await GetVoiceOverPkgVersionMatchingFields ( availableVaMatchingFields ) ;
179+ await GetVoiceOverPkgVersionMatchingFields ( availableVaMatchingFields ,
180+ gameAudioListPath ,
181+ token ) ;
61182
62- // If any, then create them
63- if ( availableVaMatchingFields . Count != 0 )
183+ // ReSharper disable once InvertIf
184+ if ( availableVaMatchingFields . Count != 0 ) // If any, then create them
64185 {
65186 foreach ( var field in availableVaMatchingFields )
66187 {
@@ -70,31 +191,29 @@ protected override async Task<string> DownloadPkgVersion(DownloadClient download
70191 throw new InvalidOperationException ( $ "Sophon voice-over manifest info pair for field: { field } is not found! " + errorRaiseMsg ) ;
71192 }
72193
73- string languageString = GetLanguageStringByLocaleCode ( field ) ;
194+ string languageString = GetLanguageStringByLocaleCodeStatic ( field ) ;
74195 await CreateFakePkgVersionFromSophon ( client ,
196+ gameBasePath ,
75197 voManifestInfoPair ,
198+ outputAssetList ,
76199 $ "Audio_{ languageString } _pkg_version",
77- Token . Token ) ;
200+ token ) ;
78201 }
79202 }
80203
81- // Create the main pkg_version
82- await CreateFakePkgVersionFromSophon ( client ,
83- manifestInfoPair ,
84- "pkg_version" ,
85- Token . Token ) ;
86-
87204 // Return the main pkg_version path
88- return Path . Combine ( GamePath , "pkg_version" ) ;
205+ return Path . Combine ( gameBasePath , "pkg_version" ) ;
89206 }
90207
91- private async Task CreateFakePkgVersionFromSophon ( HttpClient client ,
92- SophonChunkManifestInfoPair manifestPair ,
93- string pkgVersionFilename ,
94- CancellationToken token )
208+ private static async Task CreateFakePkgVersionFromSophon ( HttpClient client ,
209+ string gamePath ,
210+ SophonChunkManifestInfoPair manifestPair ,
211+ List < SophonAsset > ? outputAssetList ,
212+ string pkgVersionFilename ,
213+ CancellationToken token )
95214 {
96215 // Ensure and try to create examine FileInfo
97- string filePath = Path . Combine ( GamePath , pkgVersionFilename ) ;
216+ string filePath = Path . Combine ( gamePath , pkgVersionFilename ) ;
98217 FileInfo fileInfo = new FileInfo ( filePath )
99218 . EnsureCreationOfDirectory ( )
100219 . EnsureNoReadOnly ( ) ;
@@ -126,13 +245,16 @@ private async Task CreateFakePkgVersionFromSophon(HttpClient cl
126245 // Serialize and write to stream
127246 await pkgVersionEntry . SerializeAsync ( stream , CoreLibraryJsonContext . Default . PkgVersionProperties , token ) ;
128247 await stream . WriteAsync ( newLineBytes , token ) ;
248+
249+ outputAssetList ? . Add ( assetInfo ) ;
129250 }
130251 }
131252
132- private async Task GetVoiceOverPkgVersionMatchingFields ( List < string > matchingFields )
253+ private static async Task GetVoiceOverPkgVersionMatchingFields ( List < string > matchingFields ,
254+ string audioLangListPath ,
255+ CancellationToken token )
133256 {
134257 // Try to get audio lang list file and if null, ignore
135- string audioLangListPath = _gameAudioLangListPathStatic ;
136258 if ( string . IsNullOrEmpty ( audioLangListPath ) )
137259 {
138260 return ;
@@ -148,10 +270,10 @@ private async Task GetVoiceOverPkgVersionMatchingFields(List<string> matchingFie
148270 // Open the file and read the content
149271 await using FileStream audioLangFileStream = audioLangFile . OpenRead ( ) ;
150272 using StreamReader reader = new StreamReader ( audioLangFileStream ) ;
151- while ( await reader . ReadLineAsync ( Token . Token ) is { } line )
273+ while ( await reader . ReadLineAsync ( token ) is { } line )
152274 {
153275 // Get locale code to be used as Sophon Manifest field later
154- string ? currentLocaleId = GetLanguageLocaleCodeByLanguageString ( line
276+ string ? currentLocaleId = GetLanguageLocaleCodeByLanguageStringStatic ( line
155277#if ! DEBUG
156278 , false
157279#endif
0 commit comments