Skip to content

Commit

Permalink
Merge pull request #2026 from DGP-Studio/feat/extract_blks
Browse files Browse the repository at this point in the history
  • Loading branch information
Lightczx authored Oct 8, 2024
2 parents 7cb8d50 + b747dea commit 9fae387
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal static class SettingKeys
public const string PhysicalDriverIsAlwaysSolidState = "PhysicalDriverIsAlwaysSolidState";
public const string AlwaysIsFirstRunAfterUpdate = "AlwaysIsFirstRunAfterUpdate";
public const string AlphaBuildUseCNPatchEndpoint = "AlphaBuildUseCNPatchEndpoint";
public const string AllowExtractGameBlks = "AllowExtractGameBlks";
#endregion

#region Obsolete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ protected static async ValueTask DeleteAssetsAsync(GamePackageServiceContext con

foreach (AssetProperty asset in assets)
{
string assetPath = Path.Combine(context.Operation.GameFileSystem.GameDirectory, asset.AssetName);
string assetPath = Path.Combine(context.Operation.ExtractOrGameDirectory, asset.AssetName);

if (asset.AssetType is 64)
{
Expand Down Expand Up @@ -287,7 +287,7 @@ private async ValueTask MergeDiffAssetAsync(GamePackageServiceContext context, S
}
}

if (!context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName))
if (context.Operation.Kind is GamePackageOperationKind.Update && !context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName))
{
FileOperation.Delete(chunkPath);
}
Expand Down Expand Up @@ -320,8 +320,8 @@ private async ValueTask MergeDiffAssetAsync(GamePackageServiceContext context, S
}
}

string newAssetPath = Path.Combine(context.Operation.GameFileSystem.GameDirectory, asset.NewAsset.AssetName);
using (FileStream newAssetFileStream = File.Create(newAssetPath))
string path = context.EnsureAssetTargetDirectoryExists(asset.NewAsset.AssetName);
using (FileStream newAssetFileStream = File.Create(path))
{
newAssetStream.Position = 0;
await newAssetStream.CopyToAsync(newAssetFileStream, token).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ protected override async ValueTask MergeNewAssetAsync(GamePackageServiceContext
{
CancellationToken token = context.CancellationToken;

string path = Path.Combine(context.Operation.GameFileSystem.GameDirectory, assetProperty.AssetName);
string? directory = Path.GetDirectoryName(path);
ArgumentNullException.ThrowIfNull(directory);
Directory.CreateDirectory(directory);

string path = context.EnsureAssetTargetDirectoryExists(assetProperty.AssetName);
using (SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.None, preallocationSize: 32 * 1024))
{
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(81920))
Expand Down Expand Up @@ -135,7 +131,7 @@ protected override async ValueTask MergeNewAssetAsync(GamePackageServiceContext
}
}

if (!context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName))
if (context.Operation.Kind is GamePackageOperationKind.Update && !context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName))
{
FileOperation.Delete(chunkPath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,7 @@ protected override async ValueTask DownloadChunksAsync(GamePackageServiceContext

protected override async ValueTask MergeNewAssetAsync(GamePackageServiceContext context, AssetProperty assetProperty)
{
string path = Path.Combine(context.Operation.GameFileSystem.GameDirectory, assetProperty.AssetName);
string? directory = Path.GetDirectoryName(path);
ArgumentNullException.ThrowIfNull(directory);
Directory.CreateDirectory(directory);

string path = context.EnsureAssetTargetDirectoryExists(assetProperty.AssetName);
using (SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.None, preallocationSize: assetProperty.AssetSize))
{
await Parallel.ForEachAsync(assetProperty.AssetChunks, context.ParallelOptions, (chunk, token) => MergeChunkIntoAssetAsync(context, fileHandle, chunk)).ConfigureAwait(false);
Expand Down Expand Up @@ -118,7 +114,7 @@ private static async ValueTask MergeChunkIntoAssetAsync(GamePackageServiceContex
}
}

if (!context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName))
if (context.Operation.Kind is GamePackageOperationKind.Update && !context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName))
{
FileOperation.Delete(chunkPath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal readonly struct GamePackageOperationContext
public readonly BranchWrapper LocalBranch;
public readonly BranchWrapper RemoteBranch;
public readonly GameChannelSDK? GameChannelSDK;
public readonly string ExtractOrGameDirectory;
public readonly string ProxiedChunksDirectory;

public GamePackageOperationContext(
Expand All @@ -24,14 +25,16 @@ public GamePackageOperationContext(
GameFileSystem gameFileSystem,
BranchWrapper localBranch,
BranchWrapper remoteBranch,
GameChannelSDK? gameChannelSDK)
GameChannelSDK? gameChannelSDK,
string? extractDirectory)
{
Kind = kind;
Asset = serviceProvider.GetRequiredService<IDriverMediaTypeAwareFactory<IGameAssetOperation>>().Create(gameFileSystem.GameDirectory);
GameFileSystem = gameFileSystem;
LocalBranch = localBranch;
RemoteBranch = remoteBranch;
GameChannelSDK = gameChannelSDK;
ExtractOrGameDirectory = extractDirectory ?? gameFileSystem.GameDirectory;

ProxiedChunksDirectory = kind is GamePackageOperationKind.Verify
? Path.Combine(gameFileSystem.ChunksDirectory, "repair")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ internal enum GamePackageOperationKind
Install,
Verify,
Update,
Extract,
Predownload,
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.RateLimiting;

namespace Snap.Hutao.Service.Game.Package.Advanced;
Expand Down Expand Up @@ -71,6 +72,7 @@ public async ValueTask<bool> StartOperationAsync(GamePackageOperationContext ope
GamePackageOperationKind.Install => InstallAsync,
GamePackageOperationKind.Verify => VerifyAndRepairAsync,
GamePackageOperationKind.Update => UpdateAsync,
GamePackageOperationKind.Extract => ExtractAsync,
GamePackageOperationKind.Predownload => PredownloadAsync,
_ => context => ValueTask.FromException(HutaoException.NotSupported()),
};
Expand Down Expand Up @@ -259,6 +261,9 @@ private static long GetTotalBytes(List<SophonAssetOperation> assets)
return totalBytes;
}

[GeneratedRegex(@"AssetBundles.*\.blk$", RegexOptions.IgnoreCase)]
private static partial Regex AssetBundlesBlock();

private async ValueTask VerifyAndRepairAsync(GamePackageServiceContext context)
{
if (await DecodeManifestsAsync(context, context.Operation.LocalBranch).ConfigureAwait(false) is not { } localBuild)
Expand Down Expand Up @@ -338,6 +343,58 @@ await DecodeManifestsAsync(context, context.Operation.RemoteBranch).ConfigureAwa
}
}

private async ValueTask ExtractAsync(GamePackageServiceContext context)
{
if (await DecodeManifestsAsync(context, context.Operation.LocalBranch).ConfigureAwait(false) is not { } localBuild ||
await DecodeManifestsAsync(context, context.Operation.RemoteBranch).ConfigureAwait(false) is not { } remoteBuild)
{
context.Progress.Report(new GamePackageOperationReport.Reset(SH.ServiceGamePackageAdvancedDecodeManifestFailed));
return;
}

localBuild = ExtractGameAssetBundles(localBuild);
remoteBuild = ExtractGameAssetBundles(remoteBuild);

List<SophonAssetOperation> diffAssets = GetDiffOperations(localBuild, remoteBuild).ToList();
diffAssets.SortBy(a => a.Kind);

int downloadTotalChunks = GetTotalBlocks(diffAssets);
int installTotalChunks = diffAssets.Sum(a => a.NewAsset.AssetChunks.Count);
long totalBytes = GetTotalBytes(diffAssets);

if (!context.EnsureAvailableFreeSpace(totalBytes))
{
return;
}

InitializeDuplicatedChunkNames(context, diffAssets.SelectMany(a => a.DiffChunks.Select(c => c.AssetChunk)));

context.Progress.Report(new GamePackageOperationReport.Reset("Copying", 0, localBuild.TotalChunks, localBuild.TotalBytes));
string oldBlksDirectory = Path.Combine(context.Operation.GameFileSystem.DataDirectory, @"StreamingAssets\AssetBundles\blocks");
foreach (string file in Directory.GetFiles(oldBlksDirectory, "*.blk", SearchOption.AllDirectories))
{
string fileName = Path.GetFileName(file);
string newFilePath = Path.Combine(context.Operation.ExtractOrGameDirectory, fileName);
File.Copy(file, newFilePath, true);
AssetProperty asset = localBuild.Manifests.First().ManifestProto.Assets.First(a =>
a.AssetName.Contains(fileName, StringComparison.OrdinalIgnoreCase));
context.Progress.Report(new GamePackageOperationReport.Install(asset.AssetSize, asset.AssetChunks.Count));
}

context.Progress.Report(new GamePackageOperationReport.Reset("Extracting", downloadTotalChunks, installTotalChunks, totalBytes));
await context.Operation.Asset.UpdateDiffAssetsAsync(context, diffAssets).ConfigureAwait(false);

context.Progress.Report(new GamePackageOperationReport.Finish(context.Operation.Kind));

SophonDecodedBuild ExtractGameAssetBundles(SophonDecodedBuild decodedBuild)
{
SophonDecodedManifest manifest = decodedBuild.Manifests.First();
SophonManifestProto proto = new();
proto.Assets.AddRange(manifest.ManifestProto.Assets.Where(asset => AssetBundlesBlock().IsMatch(asset.AssetName)));
return new(decodedBuild.TotalBytes, [new(manifest.UrlPrefix, proto)]);
}
}

private async ValueTask PredownloadAsync(GamePackageServiceContext context)
{
if (await DecodeManifestsAsync(context, context.Operation.LocalBranch).ConfigureAwait(false) is not { } localBuild ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using CommunityToolkit.Common;
using Snap.Hutao.Core.IO;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.RateLimiting;
Expand Down Expand Up @@ -34,7 +35,7 @@ public GamePackageServiceContext(GamePackageOperationContext operation, IProgres

public readonly bool EnsureAvailableFreeSpace(long totalBytes)
{
long availableBytes = LogicalDriver.GetAvailableFreeSpace(Operation.GameFileSystem.GameDirectory);
long availableBytes = LogicalDriver.GetAvailableFreeSpace(Operation.ExtractOrGameDirectory);

if (totalBytes > availableBytes)
{
Expand All @@ -48,6 +49,22 @@ public readonly bool EnsureAvailableFreeSpace(long totalBytes)
return true;
}

public readonly string EnsureAssetTargetDirectoryExists(string assetName)
{
if (Operation.Kind is GamePackageOperationKind.Extract)
{
assetName = Path.GetFileName(assetName);
}

string targetPath = Path.Combine(Operation.ExtractOrGameDirectory, assetName);

string? directory = Path.GetDirectoryName(targetPath);
ArgumentNullException.ThrowIfNull(directory);
Directory.CreateDirectory(directory);

return targetPath;
}

[SuppressMessage("", "SH003")]
public readonly Task<AsyncKeyedLock<string>.Releaser> ExclusiveProcessChunkAsync(string chunkName, CancellationToken token)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@
IsHitTestVisible="False"
Visibility="{Binding GamePackageViewModel.IsPredownloadFinished, Converter={StaticResource BoolToVisibilityConverter}}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
Command="{Binding GamePackageViewModel.StartCommand}"
CommandParameter="Extract"
Header="Extract Blks"
IsClickEnabled="True"
Visibility="{Binding GamePackageViewModel.IsExtractAvailable, Converter={StaticResource BoolToVisibilityConverter}}"/>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>

Expand Down
4 changes: 4 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/TestPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@
<ToggleSwitch IsOn="{Binding AlphaBuildUseCNPatchEndpoint, Mode=TwoWay}"/>
</cwc:SettingsCard>

<cwc:SettingsCard Header="Allow Extrack Game Blks">
<ToggleSwitch IsOn="{Binding AllowExtractGameBlks, Mode=TwoWay}"/>
</cwc:SettingsCard>

<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="Gacha Service"/>
<cwc:SettingsCard
Command="{Binding CompensationGachaLogServiceTimeCommand}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ private async Task StartAsync()
gameFileSystem,
default!,
branch.Main,
gameChannelSDK);
gameChannelSDK,
default);

gameFileSystem.GenerateConfigurationFile(branch.Main.Tag, launchScheme);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ private void FinishProgress(GamePackageOperationReport.Finish finish)
GamePackageOperationKind.Install => SH.ViewModelGamePakcageOperationCompleteInstall,
GamePackageOperationKind.Verify => finish.Repaired ? SH.ViewModelGamePakcageOperationCompleteRepair : SH.ViewModelGamePakcageOperationSkipRepair,
GamePackageOperationKind.Update => SH.ViewModelGamePakcageOperationCompleteUpdate,
GamePackageOperationKind.Extract => "Extracted",
GamePackageOperationKind.Predownload => SH.ViewModelGamePakcageOperationCompletePredownload,
_ => throw HutaoException.NotSupported(),
};
Expand Down
23 changes: 21 additions & 2 deletions src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GamePackageViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

using Snap.Hutao.Core.Setting;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Package.Advanced;
using Snap.Hutao.Service.Game.Scheme;
Expand All @@ -16,6 +18,7 @@ namespace Snap.Hutao.ViewModel.Game;
[Injection(InjectAs.Singleton)]
internal sealed partial class GamePackageViewModel : Abstraction.ViewModel
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly IGamePackageService gamePackageService;
private readonly LaunchGameShared launchGameShared;
private readonly IServiceProvider serviceProvider;
Expand Down Expand Up @@ -121,6 +124,11 @@ public bool IsPredownloadFinished
}
}

public bool IsExtractAvailable
{
get => PreVersion is not null && LocalSetting.Get(SettingKeys.AllowExtractGameBlks, false);
}

protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (launchGameShared.GetCurrentLaunchSchemeFromConfigFile() is not { } launchScheme)
Expand Down Expand Up @@ -205,13 +213,24 @@ private async Task StartAsync(string state)

GameChannelSDK? gameChannelSDK = sdkResp.Data.GameChannelSDKs.FirstOrDefault(sdk => sdk.Game.Id == targetLaunchScheme.GameId);

string? extractDirectory = default;
if (operationKind is GamePackageOperationKind.Extract)
{
(bool isOk, extractDirectory) = fileSystemPickerInteraction.PickFolder("Select directory to extract the game blks");
if (!isOk)
{
return;
}
}

GamePackageOperationContext context = new(
serviceProvider,
operationKind,
gameFileSystem,
GameBranch.Main.GetTaggedCopy(LocalVersion.ToString()),
operationKind is GamePackageOperationKind.Predownload ? GameBranch.PreDownload : GameBranch.Main,
gameChannelSDK);
operationKind is GamePackageOperationKind.Predownload or GamePackageOperationKind.Extract ? GameBranch.PreDownload : GameBranch.Main,
gameChannelSDK,
extractDirectory);

if (!await gamePackageService.StartOperationAsync(context).ConfigureAwait(false))
{
Expand Down
14 changes: 14 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ public bool AlphaBuildUseCNPatchEndpoint
}
}

public bool AllowExtractGameBlks
{
get => LocalSetting.Get(SettingKeys.AllowExtractGameBlks, false);
set
{
if (IsViewDisposed)
{
return;
}

LocalSetting.Set(SettingKeys.AllowExtractGameBlks, value);
}
}

[Command("ResetGuideStateCommand")]
private static void ResetGuideState()
{
Expand Down

0 comments on commit 9fae387

Please sign in to comment.