Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 19 additions & 47 deletions CollapseLauncher/Classes/CachesManagement/Honkai/Fetch.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CollapseLauncher.Helper;
using CollapseLauncher.Helper.Metadata;
using CollapseLauncher.Interfaces;
using CollapseLauncher.RepairManagement;
using Hi3Helper;
using Hi3Helper.EncTool;
using Hi3Helper.EncTool.Parser.KianaDispatch;
Expand All @@ -19,6 +20,7 @@
// ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault
// ReSharper disable CommentTypo
// ReSharper disable StringLiteralTypo
#pragma warning disable IDE0130

namespace CollapseLauncher
{
Expand All @@ -36,6 +38,11 @@ private async Task<List<CacheAsset>> Fetch(CancellationToken token)
.SetAllowedDecompression(DecompressionMethods.None)
.Create();

#if DEBUG
// Assign Dispatcher Logger for Debug
KianaDispatch.DebugLogger = ILoggerHelper.GetILogger("KianaDispatch");
#endif

// Build _gameRepoURL from loading Dispatcher and Gateway
await BuildGameRepoURL(httpClientNew, token);

Expand Down Expand Up @@ -96,13 +103,14 @@ private async Task BuildGameRepoURL(HttpClient client, CancellationToken token)
string key = GameVersionManager.GamePreset.DispatcherKey;

// Try assign dispatcher
dispatch = await KianaDispatch.GetDispatch(client,
baseURL,
GameVersionManager.GamePreset.GameDispatchURLTemplate,
GameVersionManager.GamePreset.GameDispatchChannelName,
key,
GameVersion.VersionArray,
token);
if (GameVersionManager.GamePreset is { GameDispatchURLTemplate: not null, GameDispatchChannelName: not null })
dispatch = await KianaDispatch.GetDispatch(client,
baseURL,
GameVersionManager.GamePreset.GameDispatchURLTemplate,
GameVersionManager.GamePreset.GameDispatchChannelName,
key,
GameVersion.VersionArray,
token);
lastException = null;
break;
}
Expand All @@ -116,12 +124,11 @@ private async Task BuildGameRepoURL(HttpClient client, CancellationToken token)
if (lastException != null) throw lastException;

// Get gatewayURl and fetch the gateway
GameGateway =
await KianaDispatch.GetGameserver(client, dispatch!, GameVersionManager.GamePreset.GameGatewayDefault!, token);
GameGateway = await KianaDispatch.GetGameserver(client, dispatch!, GameVersionManager.GamePreset.GameGatewayDefault!, token);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A NullReferenceException will occur when calling GetGameserver with dispatch! because dispatch can remain null if GameDispatchURLTemplate or GameDispatchChannelName is null.
Severity: CRITICAL

Suggested Fix

Ensure the dispatch variable is handled correctly when it is null. Either throw a more informative exception immediately after the conditional check if dispatch is null, or adjust the logic to prevent the call to KianaDispatch.GetGameserver from executing with a null dispatch variable.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: CollapseLauncher/Classes/CachesManagement/Honkai/Fetch.cs#L127

Potential issue: The `dispatch` variable is initialized within a conditional block that
checks if `GameDispatchURLTemplate` and `GameDispatchChannelName` are non-null. If
either of these properties is null, the condition is false, and `dispatch` remains
uninitialized (i.e., null). However, the code later calls `KianaDispatch.GetGameserver`
using `dispatch!` with the null-forgiving operator. This will cause a
`NullReferenceException` at runtime if the initial condition was not met, crashing the
application during the cache fetching process.

GameRepoURL = BuildAssetBundleURL(GameGateway);
}

private static string BuildAssetBundleURL(KianaDispatch gateway) => CombineURLFromString(gateway!.AssetBundleUrls![0], "/{0}/editor_compressed/");
private string BuildAssetBundleURL(KianaDispatch gateway) => CombineURLFromString(this.GetRandomCacheBaseUrl(gateway), "/{0}/editor_compressed/");

private async Task<(int, long)> FetchByType(CacheAssetType type, HttpClient client, List<CacheAsset> assetIndex, CancellationToken token)
{
Expand All @@ -132,7 +139,7 @@ private async Task BuildGameRepoURL(HttpClient client, CancellationToken token)
UpdateStatus();

// Get the asset index properties
string baseURL = string.Format(GameRepoURL!, type.ToString().ToLowerInvariant());
string baseURL = string.Format(BuildAssetBundleURL(GameGateway), type.ToString().ToLowerInvariant());
string assetIndexURL = string.Format(CombineURLFromString(baseURL, "{0}Version.unity3d"),
type == CacheAssetType.Data ? "Data" : "Resource");

Expand All @@ -150,25 +157,6 @@ private async Task BuildGameRepoURL(HttpClient client, CancellationToken token)
return returnValue;
}

/*
private void BuildDataPatchConfig(MemoryStream stream, List<CacheAsset> assetIndex)
{
// Reset position
stream.Position = 0;

// Initialize manifest file
CachePatchManifest manifest = new CachePatchManifest(stream, true);

for (int i = 0; i < assetIndex.Count; i++)
{
if (assetIndex[i].DataType == CacheAssetType.Data)
{
CachePatchInfo info = manifest.PatchAsset.Where(x => IsArrayMatch(x.NewHashSHA1, assetIndex[i].CRCArray)).FirstOrDefault();
}
}
}
*/

private IEnumerable<CacheAsset> EnumerateCacheTextAsset(CacheAssetType type, IEnumerable<string> enumerator, string baseUrl)
{
// Set isFirst flag as true if type is Data and
Expand Down Expand Up @@ -287,7 +275,7 @@ await Parallel.ForEachAsync(EnumerateCacheTextAsset(type, dataTextAsset.GetStrin
UpdateStatus();

// Check for the URL availability and is not available, then skip.
var urlStatus = await client.GetURLStatusCode(content.ConcatURL, cancellationToken);
UrlStatus urlStatus = await client.GetURLStatusCode(content.ConcatURL, cancellationToken);
LogWriteLine($"The Cache {type} asset: {content.N} " + (urlStatus.IsSuccessStatusCode ? "is" : "is not") + $" available (Status code: {urlStatus.StatusCode})", LogType.Default, true);

if (!urlStatus.IsSuccessStatusCode) return;
Expand Down Expand Up @@ -342,21 +330,5 @@ private static bool IsValidRegionFile(string input, string lang)
}

public KianaDispatch GetCurrentGateway() => GameGateway;

public async Task<(List<CacheAsset>, string, string, int)> GetCacheAssetList(HttpClient client, CacheAssetType type, CancellationToken token)
{
// Initialize asset index for the return
List<CacheAsset> returnAsset = [];

// Build _gameRepoURL from loading Dispatcher and Gateway
await BuildGameRepoURL(client, token);

// Fetch the progress
_ = await FetchByType(type, client, returnAsset, token);

// Return the list and base asset bundle repo URL
return (returnAsset, GameGateway!.ExternalAssetUrls!.FirstOrDefault(), BuildAssetBundleURL(GameGateway),
LuckyNumber);
}
}
}
26 changes: 26 additions & 0 deletions CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Hashing;
using System.Linq;
Expand Down Expand Up @@ -1359,6 +1360,31 @@ await downloadClient.DownloadAsync(assetURL,
goto StartOver;
}
}

[return: NotNullIfNotNull(nameof(url))]
internal string? GetHttpsOrHttpOverrideUrl(string? url)
{
const string schemeStart = "://";

if (string.IsNullOrEmpty(url))
{
return url;
}

string scheme = IsForceHttpOverride ? "http://" : "https://";
if (url.StartsWith(scheme, StringComparison.OrdinalIgnoreCase))
{
return url;
}

string urlNoScheme = url;
int indexOfSchemeMark = url.IndexOf(schemeStart, StringComparison.OrdinalIgnoreCase);
if (indexOfSchemeMark < 0) return scheme + urlNoScheme;

indexOfSchemeMark += schemeStart.Length;
urlNoScheme = url[indexOfSchemeMark..];
return scheme + urlNoScheme;
}
#endregion

#region Stream and Archive Tools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,88 +57,74 @@ internal static async Task<List<FilePropertiesRemote>>

progressibleInstance.UpdateStatus();

bool isUseHttpRepairOverride = progressibleInstance.IsForceHttpOverride;
AudioLanguageType gameLanguageType = GetCurrentGameAudioLanguage(presetConfig);

Exception? lastException = null;
foreach (string baseAsbUrl in gameServerInfo.ExternalAssetUrls)
AudioLanguageType gameLanguageType = GetCurrentGameAudioLanguage(presetConfig);
string baseUrl = progressibleInstance.GetRandomAsbBaseUrl(gameServerInfo);
string baseAudioUrl =
baseUrl.CombineURLFromString($"Audio/Windows/{gameVersion.Major}_{gameVersion.Minor}/{gameServerInfo
.Manifest
.ManifestAudio
.ManifestAudioRevision}");

await using Stream manifestStream = audioFileIdentifier.fileStream ?? throw new NullReferenceException("Senadina Audio Identifier Stream cannot be null!");
KianaAudioManifest manifestData =
new(manifestStream, gameVersion.VersionArrayManifest);

List<FilePropertiesRemote> assetList = [];
await Parallel.ForEachAsync(manifestData.AudioAssets,
new ParallelOptions
{
CancellationToken = token,
MaxDegreeOfParallelism = parallelThread
},
ImplCheckAndAdd);

return assetList;

async ValueTask ImplCheckAndAdd(ManifestAssetInfo audioAsset, CancellationToken innerToken)
{
try
// Eliminate removed audio assets or not matching language.
if ((audioAsset.Language != gameLanguageType &&
audioAsset.Language != AudioLanguageType.Common) ||
ignoredAudioHashset.Contains(audioAsset.PckType))
{
string baseAudioAssetUrl = ((isUseHttpRepairOverride ? "http://" : "https://") + baseAsbUrl)
.CombineURLFromString($"Audio/Windows/{gameVersion.Major}_{gameVersion.Minor}/{gameServerInfo
.Manifest
.ManifestAudio
.ManifestAudioRevision}");

await using Stream manifestStream = audioFileIdentifier.fileStream ?? throw new NullReferenceException("Senadina Audio Identifier Stream cannot be null!");
KianaAudioManifest manifestData =
new(manifestStream, gameVersion.VersionArrayManifest);

List<FilePropertiesRemote> assetList = [];
await Parallel.ForEachAsync(manifestData.AudioAssets,
new ParallelOptions
{
CancellationToken = token,
MaxDegreeOfParallelism = parallelThread
},
ImplCheckAndAdd);

return assetList;

async ValueTask ImplCheckAndAdd(ManifestAssetInfo audioAsset, CancellationToken innerToken)
{
// Eliminate removed audio assets or not matching language.
if ((audioAsset.Language != gameLanguageType &&
audioAsset.Language != AudioLanguageType.Common) ||
ignoredAudioHashset.Contains(audioAsset.PckType))
{
return;
}

if (audioAsset.NeedMap)
{
goto AddAsset; // I love goto. Dun ask me why :>
}

progressibleInstance.Status.ActivityStatus = string.Format(Locale.Current.Lang?._GameRepairPage?.Status15 ?? "", audioAsset.Path);
progressibleInstance.Status.IsProgressAllIndetermined = true;
progressibleInstance.Status.IsProgressPerFileIndetermined = true;
progressibleInstance.UpdateStatus();

string assetUrl = baseAudioAssetUrl.CombineURLFromString(audioAsset.Path);
UrlStatus urlStatus = await assetBundleHttpClient.GetURLStatusCode(assetUrl, innerToken);
Logger.LogWriteLine($"The audio asset: {audioAsset.Path} " + (urlStatus.IsSuccessStatusCode ? "is" : "is not") + $" available (Status code: {urlStatus.StatusCode})", LogType.Default, true);

if (!urlStatus.IsSuccessStatusCode)
{
return;
}

AddAsset:
lock (assetList)
{
assetList.Add(new FilePropertiesRemote
{
IsPatchApplicable = audioAsset.IsHasPatch,
AssociatedObject = audioAsset,
AudioPatchInfo = audioAsset.PatchInfo,
CRC = audioAsset.HashString,
FT = FileType.Audio,
RN = baseAudioAssetUrl.CombineURLFromString(audioAsset.Path),
N = audioAsset.Name + ".pck",
S = audioAsset.Size
});
}
}
return;
}
catch (Exception e)

if (audioAsset.NeedMap)
{
lastException = e;
goto AddAsset; // I love goto. Dun ask me why :>
}
}

throw lastException ?? new HttpRequestException("No Asset bundle URLs were reachable");
progressibleInstance.Status.ActivityStatus = string.Format(Locale.Current.Lang?._GameRepairPage?.Status15 ?? "", audioAsset.Path);
progressibleInstance.Status.IsProgressAllIndetermined = true;
progressibleInstance.Status.IsProgressPerFileIndetermined = true;
progressibleInstance.UpdateStatus();

string assetUrl = baseAudioUrl.CombineURLFromString(audioAsset.Path);
UrlStatus urlStatus = await assetBundleHttpClient.GetURLStatusCode(assetUrl, innerToken);
Logger.LogWriteLine($"The audio asset: {audioAsset.Path} " + (urlStatus.IsSuccessStatusCode ? "is" : "is not") + $" available (Status code: {urlStatus.StatusCode})", LogType.Default, true);

if (!urlStatus.IsSuccessStatusCode)
{
throw new HttpRequestException("No Asset bundle URLs were reachable");
}

AddAsset:
lock (assetList)
{
assetList.Add(new FilePropertiesRemote
{
IsPatchApplicable = audioAsset.IsHasPatch,
AssociatedObject = audioAsset,
AudioPatchInfo = audioAsset.PatchInfo,
CRC = audioAsset.HashString,
FT = FileType.Audio,
RN = baseAudioUrl.CombineURLFromString(audioAsset.Path),
N = audioAsset.Name + ".pck",
S = audioAsset.Size
});
}
}
}

internal static bool GetAudioPatchUrlProperty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ internal static async Task<List<FilePropertiesRemote>>
progressibleInstance.Status.IsIncludePerFileIndicator = false;
progressibleInstance.UpdateStatus();

bool isUseHttpRepairOverride = progressibleInstance.IsForceHttpOverride;

await using Stream xmfMetaCurrentFileStream = senadinaResults.XmfMeta?.fileStream ?? throw new NullReferenceException("Senadina BlockMeta Identifier Stream cannot be null!");
await using Stream xmfPatchCurrentFileStream = senadinaResults.XmfPatch?.fileStream ?? throw new NullReferenceException("Senadina BlockPatch Identifier Stream cannot be null!");

Expand Down Expand Up @@ -106,15 +104,18 @@ internal static async Task<List<FilePropertiesRemote>>
ref BlockPatchInfo patchInfoRef =
ref CollectionsMarshal.GetValueRefOrNullRef(patchInfos, xmfBlock.BlockName);

string asbBaseUrl = progressibleInstance.GetRandomAsbBaseUrl(gameServerInfo);
string assetUrl =
asbBaseUrl.CombineURLFromString($"StreamingAsb/{baseUrlVersionPrefix}/pc/HD/asb", xmfBlock.BlockName);

FilePropertiesRemote asset = new()
{
AssociatedObject = xmfBlock,
CRC = Path.GetFileNameWithoutExtension(xmfBlock.BlockName),
FT = FileType.Block,
RN = GetRandomBaseUrl(gameServerInfo)
.CombineURLFromString($"StreamingAsb/{baseUrlVersionPrefix}/pc/HD/asb", xmfBlock.BlockName),
N = xmfBlock.BlockName,
S = xmfBlock.Size
CRC = Path.GetFileNameWithoutExtension(xmfBlock.BlockName),
FT = FileType.Block,
RN = assetUrl,
N = xmfBlock.BlockName,
S = xmfBlock.Size
};
assetList.Add(asset);

Expand All @@ -128,12 +129,20 @@ internal static async Task<List<FilePropertiesRemote>>
}

return assetList;
}

string GetRandomBaseUrl(KianaDispatch kianaDispatch)
{
string selectedUrl = kianaDispatch.ExternalAssetUrls.RandomSelectSingle();
return isUseHttpRepairOverride ? "http://" : "https://" + selectedUrl;
}
internal static string GetRandomAsbBaseUrl<T>(this ProgressBase<T> instance, KianaDispatch kianaDispatch)
where T : IAssetIndexSummary
{
string selectedUrl = kianaDispatch.ExternalAssetUrls.RandomSelectSingle();
return instance.GetHttpsOrHttpOverrideUrl(selectedUrl);
}

internal static string GetRandomCacheBaseUrl<T>(this ProgressBase<T> instance, KianaDispatch kianaDispatch)
where T : IAssetIndexSummary
{
string selectedUrl = kianaDispatch.AssetBundleUrls.RandomSelectSingle();
return instance.GetHttpsOrHttpOverrideUrl(selectedUrl);
}

internal static bool GetBlockPatchUrlProperty(
Expand Down
Loading
Loading