Merge pull request #1737 from DGP-Studio/feat/hypapi

This commit is contained in:
DismissedLight
2024-06-17 21:33:06 +08:00
committed by GitHub
53 changed files with 377 additions and 819 deletions

View File

@@ -10,8 +10,10 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
using Snap.Hutao.Web.Response;
using System.IO;
@@ -43,7 +45,7 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
return;
}
// Backup config file, in order to prevent a incompatible launcher to delete it.
// Backup config file, recover when a incompatible launcher deleted it.
context.ServiceProvider.GetRequiredService<IGameConfigurationFileService>().Backup(gameFileSystem.GameConfigFilePath);
await context.TaskContext.SwitchToMainThreadAsync();
@@ -96,13 +98,30 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
ResourceClient resourceClient = context.ServiceProvider.GetRequiredService<ResourceClient>();
Response<GameResource> response = await resourceClient.GetResourceAsync(context.Scheme).ConfigureAwait(false);
HoyoPlayClient hoyoPlayClient = context.ServiceProvider.GetRequiredService<HoyoPlayClient>();
if (!response.TryGetDataWithoutUINotification(out GameResource? resource))
// We perform these requests before package conversion to ensure resources index is intact.
Response<GamePackagesWrapper> packagesResponse = await hoyoPlayClient.GetPackagesAsync(context.Scheme).ConfigureAwait(false);
if (!packagesResponse.TryGetDataWithoutUINotification(out GamePackagesWrapper? gamePackages))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response);
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(packagesResponse);
return false;
}
Response<GameChannelSDKsWrapper> sdkResponse = await hoyoPlayClient.GetChannelSDKAsync(context.Scheme).ConfigureAwait(false);
if (!sdkResponse.TryGetDataWithoutUINotification(out GameChannelSDKsWrapper? channelSDKs))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(sdkResponse);
return false;
}
Response<DeprecatedFileConfigurationsWrapper> deprecatedFileResponse = await hoyoPlayClient.GetDeprecatedFileConfigurationsAsync(context.Scheme).ConfigureAwait(false);
if (!deprecatedFileResponse.TryGetDataWithoutUINotification(out DeprecatedFileConfigurationsWrapper? deprecatedFileConfigs))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(deprecatedFileResponse);
return false;
}
@@ -110,7 +129,7 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
if (!context.Scheme.ExecutableMatches(gameFileName))
{
if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, resource, gameFolder, progress).ConfigureAwait(false))
if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, gamePackages.GamePackages.Single(), gameFolder, progress).ConfigureAwait(false))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError;
context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail;
@@ -124,7 +143,7 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
context.Options.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, executableName));
}
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(channelSDKs.GameChannelSDKs.SingleOrDefault(), deprecatedFileConfigs.DeprecatedFileConfigurations.SingleOrDefault(), gameFolder).ConfigureAwait(false);
return true;
}

View File

@@ -20,7 +20,7 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution
string configPath = gameFileSystem.GameConfigFilePath;
context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath);
List<IniElement> elements = default!;
List<IniElement> elements;
try
{
elements = [.. IniSerializer.DeserializeFromFile(configPath)];

View File

@@ -8,7 +8,9 @@ using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.IO.Http.Sharding;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
using System.Globalization;
using System.IO;
using System.IO.Compression;
@@ -34,9 +36,9 @@ internal sealed partial class PackageConverter
private readonly HttpClient httpClient;
private readonly ILogger<PackageConverter> logger;
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageConvertStatus> progress)
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GamePackage gamePackage, string gameFolder, IProgress<PackageConvertStatus> progress)
{
// 以 国服 => 国际 为例
// 以 国服 -> 国际 为例
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
// 获取本地对应 pkg_version 文件,转换为索引字典
//
@@ -56,7 +58,7 @@ internal sealed partial class PackageConverter
// 替换操作等于 先备份国服文件,随后新增国际服文件
// 准备下载链接
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
string scatteredFilesUrl = gamePackage.Main.Major.ResourceListUrl;
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.GetDataFolderServerCacheFolder(), gameFolder, scatteredFilesUrl);
@@ -77,7 +79,7 @@ internal sealed partial class PackageConverter
return await ReplaceGameResourceAsync(diffOperations, context, progress).ConfigureAwait(false);
}
public async ValueTask EnsureDeprecatedFilesAndSdkAsync(GameResource resource, string gameFolder)
public async ValueTask EnsureDeprecatedFilesAndSdkAsync(GameChannelSDK? channelSDK, DeprecatedFilesWrapper? deprecatedFiles, string gameFolder)
{
string sdkDllBackup = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll.backup");
string sdkDll = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll");
@@ -86,9 +88,9 @@ internal sealed partial class PackageConverter
string sdkVersion = Path.Combine(gameFolder, "sdk_pkg_version");
// Only bilibili's sdk is not null
if (resource.Sdk is not null)
if (channelSDK is not null)
{
using (Stream sdkWebStream = await httpClient.GetStreamAsync(resource.Sdk.Path).ConfigureAwait(false))
using (Stream sdkWebStream = await httpClient.GetStreamAsync(channelSDK.ChannelSdkPackage.Url).ConfigureAwait(false))
{
ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true);
}
@@ -106,9 +108,9 @@ internal sealed partial class PackageConverter
FileOperation.Move(sdkVersion, sdkVersionBackup, true);
}
if (resource.DeprecatedFiles is not null)
if (deprecatedFiles is not null)
{
foreach (NameMd5 file in resource.DeprecatedFiles)
foreach (DeprecatedFile file in deprecatedFiles.DeprecatedFiles)
{
string filePath = Path.Combine(gameFolder, file.Name);
FileOperation.Move(filePath, $"{filePath}.backup", true);

View File

@@ -30,25 +30,13 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
}
}
/// <summary>
/// 通道
/// </summary>
public ChannelType Channel { get; private protected set; }
/// <summary>
/// 子通道
/// </summary>
public SubChannelType SubChannel { get; private protected set; }
/// <summary>
/// 启动器 Id
/// </summary>
public int LauncherId { get; private protected set; }
public string LauncherId { get; private protected set; } = default!;
/// <summary>
/// API Key
/// </summary>
public string Key { get; private protected set; } = default!;
public string GameId { get; private protected set; } = default!;
/// <summary>
/// 是否为海外

View File

@@ -7,13 +7,13 @@ namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeBilibili : LaunchScheme
{
private const int SdkStaticLauncherBilibiliId = 17;
private const string SdkStaticLauncherBilibiliKey = "KAtdSsoQ";
private const string HoyoPlayLauncherBilibiliId = "umfgRO5gh5";
private const string HoyoPlayGameBilibiliId = "T2S0Gz4Dr2";
public LaunchSchemeBilibili(SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherBilibiliId;
Key = SdkStaticLauncherBilibiliKey;
LauncherId = HoyoPlayLauncherBilibiliId;
GameId = HoyoPlayGameBilibiliId;
Channel = ChannelType.Bili;
SubChannel = subChannel;
IsOversea = false;

View File

@@ -7,13 +7,13 @@ namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeChinese : LaunchScheme
{
private const int SdkStaticLauncherChineseId = 18;
private const string SdkStaticLauncherChineseKey = "eYd89JmJ";
private const string HoyoPlayLauncherChineseId = "jGHBHlcOq1";
private const string HoyoPlayGameChineseId = "1Z8W5NHUQb";
public LaunchSchemeChinese(ChannelType channel, SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherChineseId;
Key = SdkStaticLauncherChineseKey;
LauncherId = HoyoPlayLauncherChineseId;
GameId = HoyoPlayGameChineseId;
Channel = channel;
SubChannel = subChannel;
IsOversea = false;

View File

@@ -7,13 +7,13 @@ namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeOversea : LaunchScheme
{
private const int SdkStaticLauncherOverseaId = 10;
private const string SdkStaticLauncherOverseaKey = "gcStgarh";
private const string HoyoPlayLauncherOverseaId = "VYTpXlbWo8";
private const string HoyoPlayGameOverseaId = "gopR6Cufr3";
public LaunchSchemeOversea(ChannelType channel, SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherOverseaId;
Key = SdkStaticLauncherOverseaKey;
LauncherId = HoyoPlayLauncherOverseaId;
GameId = HoyoPlayGameOverseaId;
Channel = channel;
SubChannel = subChannel;
IsOversea = true;

View File

@@ -16,6 +16,7 @@
d:DataContext="{d:DesignInstance shva:AchievementViewModelSlim}"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding NavigateCommand}"
Style="{ThemeResource DefaultButtonStyle}"
mc:Ignorable="d">

View File

@@ -18,6 +18,7 @@
d:DataContext="{d:DesignInstance shvd:DailyNoteViewModelSlim}"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding NavigateCommand}"
Style="{ThemeResource DefaultButtonStyle}"
mc:Ignorable="d">

View File

@@ -17,6 +17,7 @@
d:DataContext="{d:DesignInstance shvg:GachaLogViewModelSlim}"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding NavigateCommand}"
Style="{ThemeResource DefaultButtonStyle}"
mc:Ignorable="d">

View File

@@ -18,6 +18,7 @@
d:DataContext="{d:DesignInstance shvg:LaunchGameViewModelSlim}"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding LaunchCommand}"
Style="{ThemeResource DefaultButtonStyle}"
mc:Ignorable="d">

View File

@@ -6,12 +6,14 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shwhshlr="using:Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource"
xmlns:shwhhpc="using:Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect"
xmlns:shwhhpcp="using:Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
d:DataContext="{d:DesignInstance shwhshlr:Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource.Package}"
d:DataContext="{d:DesignInstance shwhhpcp:Package}"
BorderThickness="0"
IsExpanded="True"
ItemsSource="{Binding VoicePacks}"
ItemsSource="{Binding AllPackages}"
mc:Ignorable="d">
<cwc:SettingsExpander.Resources>
@@ -24,7 +26,7 @@
</cwc:SettingsExpander.Resources>
<cwc:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="shwhshlr:VoicePackage">
<DataTemplate x:DataType="shwhhpc:PackageSegment">
<cwc:SettingsCard
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
ActionIcon="{shcm:FontIcon Glyph={StaticResource FontIconContentCopy}}"
@@ -35,70 +37,26 @@
<StackPanel
Grid.Row="1"
Margin="0,4,0,0"
Orientation="Horizontal">
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="{StaticResource CaptionTextBlockFontSize}" Glyph="{StaticResource FontIconContentZipFolder}"/>
<TextBlock
Width="80"
Margin="8,0,0,0"
HorizontalAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding PackageSize, Converter={StaticResource FileSizeToFriendlyStringConverter}}"/>
Text="{Binding Size, Converter={StaticResource FileSizeToFriendlyStringConverter}}"/>
<FontIcon FontSize="{StaticResource CaptionTextBlockFontSize}" Glyph="{StaticResource FontIconContentFolder}"/>
<TextBlock
Width="80"
Margin="8,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Size, Converter={StaticResource FileSizeToFriendlyStringConverter}}"/>
Text="{Binding DecompressedSize, Converter={StaticResource FileSizeToFriendlyStringConverter}}"/>
<FontIcon FontSize="{StaticResource CaptionTextBlockFontSize}" Glyph="{StaticResource FontIconContentAsteriskBadge12}"/>
<TextBlock
Margin="8,0,0,0"
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Md5}"/>
Text="{Binding MD5}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
</DataTemplate>
</cwc:SettingsExpander.ItemTemplate>
<cwc:SettingsExpander.ItemsHeader>
<cwc:SettingsCard
MinHeight="52"
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
ActionIcon="{shcm:FontIcon Glyph={StaticResource FontIconContentCopy}}"
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
BorderThickness="0"
Command="{Binding CopyPathCommand}"
CornerRadius="0"
Header="{Binding DisplayName}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel
Grid.Row="1"
Margin="0,4,0,0"
Orientation="Horizontal">
<FontIcon FontSize="{StaticResource CaptionTextBlockFontSize}" Glyph="{StaticResource FontIconContentZipFolder}"/>
<TextBlock
Width="80"
Margin="8,0,0,0"
HorizontalAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding PackageSize, Converter={StaticResource FileSizeToFriendlyStringConverter}}"/>
<FontIcon FontSize="{StaticResource CaptionTextBlockFontSize}" Glyph="{StaticResource FontIconContentFolder}"/>
<TextBlock
Width="80"
Margin="8,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Size, Converter={StaticResource FileSizeToFriendlyStringConverter}}"/>
<FontIcon FontSize="{StaticResource CaptionTextBlockFontSize}" Glyph="{StaticResource FontIconContentAsteriskBadge12}"/>
<TextBlock
Margin="8,0,0,0"
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Md5}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
</cwc:SettingsExpander.ItemsHeader>
</cwc:SettingsExpander>

View File

@@ -366,13 +366,13 @@
</PivotItem>
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceHeader}">
<Grid>
<ScrollViewer Visibility="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolConverter}}">
<ScrollViewer Visibility="{Binding GamePackage, Converter={StaticResource EmptyObjectToBoolConverter}}">
<StackPanel>
<Border Margin="16,16,16,0" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
<shvc:LaunchGameResourceExpander
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
DataContext="{Binding GameResource.PreDownloadGame.Latest, Mode=OneWay}"
DataContext="{Binding GamePackage.PreDownload.Major, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourcePreDownloadHeader}"
IsEnabled="{Binding FallbackValue=False, Converter={StaticResource EmptyObjectToBoolConverter}}"
IsExpanded="{Binding FallbackValue=False, Converter={StaticResource EmptyObjectToBoolConverter}, Mode=OneTime}"/>
@@ -382,22 +382,22 @@
<ItemsControl
Margin="0,0,0,0"
ItemTemplate="{StaticResource GameResourceTemplate}"
ItemsSource="{Binding GameResource.PreDownloadGame.Diffs, Mode=OneWay}"/>
ItemsSource="{Binding GamePackage.PreDownload.Patches, Mode=OneWay}"/>
<Border Margin="16,16,16,0" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
<shvc:LaunchGameResourceExpander
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
DataContext="{Binding GameResource.Game.Latest, Mode=OneWay}"
DataContext="{Binding GamePackage.Main.Major, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceLatestHeader}"/>
</Border>
</Border>
<ItemsControl
Margin="0,0,0,16"
ItemTemplate="{StaticResource GameResourceTemplate}"
ItemsSource="{Binding GameResource.Game.Diffs, Mode=OneWay}"/>
ItemsSource="{Binding GamePackage.Main.Patches, Mode=OneWay}"/>
</StackPanel>
</ScrollViewer>
<shvc:LoadingView IsLoading="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
<shvc:LoadingView IsLoading="{Binding GamePackage, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
</Grid>
</PivotItem>
</Pivot>

View File

@@ -11,26 +11,26 @@ internal static class LaunchGameLaunchExecution
{
public static async ValueTask LaunchExecutionAsync(this IViewModelSupportLaunchExecution launchExecution, LaunchScheme? targetScheme)
{
IServiceProvider root = Ioc.Default;
IInfoBarService infoBarService = root.GetRequiredService<IInfoBarService>();
ILogger<IViewModelSupportLaunchExecution> logger = root.GetRequiredService<ILogger<IViewModelSupportLaunchExecution>>();
// LaunchScheme? scheme = launchExecution.Shared.GetCurrentLaunchSchemeFromConfigFile();
try
using (IServiceScope scope = Ioc.Default.CreateScope())
{
// Root service provider is required.
LaunchExecutionContext context = new(root, launchExecution, targetScheme, launchExecution.SelectedGameAccount);
LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false);
IInfoBarService infoBarService = scope.ServiceProvider.GetRequiredService<IInfoBarService>();
ILogger<IViewModelSupportLaunchExecution> logger = scope.ServiceProvider.GetRequiredService<ILogger<IViewModelSupportLaunchExecution>>();
if (result.Kind is not LaunchExecutionResultKind.Ok)
try
{
infoBarService.Warning(result.ErrorMessage);
LaunchExecutionContext context = new(scope.ServiceProvider, launchExecution, targetScheme, launchExecution.SelectedGameAccount);
LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false);
if (result.Kind is not LaunchExecutionResultKind.Ok)
{
infoBarService.Warning(result.ErrorMessage);
}
}
catch (Exception ex)
{
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex);
}
}
catch (Exception ex)
{
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex);
}
}
}

View File

@@ -15,8 +15,8 @@ using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
using System.Collections.Immutable;
using System.IO;
@@ -39,19 +39,19 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly LaunchGameShared launchGameShared;
private readonly IInfoBarService infoBarService;
private readonly ResourceClient resourceClient;
private readonly IGameServiceFacade gameService;
private readonly RuntimeOptions runtimeOptions;
private readonly HoyoPlayClient hoyoPlayClient;
private readonly LaunchOptions launchOptions;
private readonly IUserService userService;
private readonly ITaskContext taskContext;
private readonly IGameServiceFacade gameService;
private readonly IMemoryCache memoryCache;
private readonly AppOptions appOptions;
private LaunchScheme? selectedScheme;
private AdvancedCollectionView<GameAccount>? gameAccountsView;
private GameAccount? selectedGameAccount;
private GameResource? gameResource;
private GamePackage? gamePackage;
private bool gamePathSelectedAndValid;
private ImmutableList<GamePathEntry> gamePathEntries;
private GamePathEntry? selectedGamePathEntry;
@@ -69,7 +69,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
[AlsoAsyncSets(nameof(GameResource), nameof(GameAccountsView))]
[AlsoAsyncSets(nameof(GamePackage), nameof(GameAccountsView))]
public LaunchScheme? SelectedScheme
{
get => selectedScheme;
@@ -80,7 +80,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
public GamePackage? GamePackage { get => gamePackage; set => SetProperty(ref gamePackage, value); }
public bool GamePathSelectedAndValid
{
@@ -301,10 +301,10 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
SelectedGameAccount = default;
await UpdateGameAccountsViewAsync().ConfigureAwait(false);
UpdateGameResourceAsync(value).SafeForget();
UpdateGamePackageAsync(value).SafeForget();
}
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)
async ValueTask UpdateGamePackageAsync(LaunchScheme? scheme)
{
if (scheme is null)
{
@@ -312,14 +312,14 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
}
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
Web.Response.Response<GamePackagesWrapper> response = await hoyoPlayClient
.GetPackagesAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
GamePackage = response.Data.GamePackages.Single();
}
}

View File

@@ -284,6 +284,25 @@ internal static class ApiEndpoints
#endregion
#region HoyoPlayApi
public static string HoyoPlayConnectGamePackages(LaunchScheme scheme)
{
return $"{HoyoPlayApiConnectApi}/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
}
public static string HoyoPlayConnectGameChannelSDKs(LaunchScheme scheme)
{
return $"{HoyoPlayApiConnectApi}/getGameChannelSDKs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
}
public static string HoyoPlayConnectDeprecatedFileConfigs(LaunchScheme scheme)
{
return $"{HoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
}
#endregion
#region PassportApi | PassportApiV4
/// <summary>
@@ -348,26 +367,6 @@ internal static class ApiEndpoints
}
#endregion
#region SdkStaticLauncherApi
/// <summary>
/// 启动器资源
/// </summary>
/// <param name="scheme">启动方案</param>
/// <returns>启动器资源字符串</returns>
public static string SdkStaticLauncherResource(LaunchScheme scheme)
{
return $"{SdkStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel:D}&sub_channel_id={scheme.SubChannel:D}";
}
public static string SdkStaticLauncherContent(LaunchScheme scheme, string languageCode, bool advOnly = true)
{
return advOnly
? $"{SdkStaticLauncherApi}/content?filter_adv=true&key={scheme.Key}&launcher_id={scheme.LauncherId}&language={languageCode}"
: $"{SdkStaticLauncherApi}/content?key={scheme.Key}&launcher_id={scheme.LauncherId}&language={languageCode}";
}
#endregion
#region Hosts | Queries
private const string ApiTakumi = "https://api-takumi.mihoyo.com";
private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api";
@@ -395,6 +394,9 @@ internal static class ApiEndpoints
private const string Hk4eSdk = "https://hk4e-sdk.mihoyo.com";
private const string HoyoPlayApi = "https://hyp-api.mihoyo.com";
private const string HoyoPlayApiConnectApi = $"{HoyoPlayApi}/hyp/hyp-connect/api";
private const string PassportApi = "https://passport-api.mihoyo.com";
private const string PassportApiAuthApi = $"{PassportApi}/account/auth/api";
private const string PassportApiV4 = "https://passport-api-v4.mihoyo.com";

View File

@@ -298,19 +298,23 @@ internal static class ApiOsEndpoints
}
#endregion
#region SdkOsStaticLauncherApi
#region SgHoyoPlayApi
public static string SdkOsStaticLauncherResource(LaunchScheme scheme)
public static string SgHoyoPlayConnectGamePackages(LaunchScheme scheme)
{
return $"{SdkOsStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel:D}&sub_channel_id={scheme.SubChannel:D}";
return $"{SgHoyoPlayApiConnectApi}/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
}
public static string SdkOsStaticLauncherContent(LaunchScheme scheme, string languageCode, bool advOnly = true)
public static string SgHoyoPlayConnectGameChannelSDKs(LaunchScheme scheme)
{
return advOnly
? $"{SdkOsStaticLauncherApi}/content?filter_adv=true&key={scheme.Key}&launcher_id={scheme.LauncherId}&language={languageCode}"
: $"{SdkOsStaticLauncherApi}/content?key={scheme.Key}&launcher_id={scheme.LauncherId}&language={languageCode}";
return $"{SgHoyoPlayApiConnectApi}/getGameChannelSDKs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
}
public static string SgHoyoPlayConnectDeprecatedFileConfigs(LaunchScheme scheme)
{
return $"{SgHoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
}
#endregion
#region WebApiOsAccountApi
@@ -344,6 +348,9 @@ internal static class ApiOsEndpoints
private const string SgPublicApi = "https://sg-public-api.hoyoverse.com";
private const string SgHk4eApi = "https://sg-hk4e-api.hoyoverse.com";
private const string SgHoyoPlayApi = "https://sg-hyp-api.hoyoverse.com";
private const string SgHoyoPlayApiConnectApi = $"{SgHoyoPlayApi}/hyp/hyp-connect/api";
private const string WebApiOs = "https://webapi-os.account.hoyoverse.com";
private const string WebApiOsAccountApi = $"{WebApiOs}/Api";

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
internal sealed class GameChannelSDK : GameIndexedObject
{
[JsonPropertyName("channel_sdk_pkg")]
public PackageSegment ChannelSdkPackage { get; set; } = default!;
[JsonPropertyName("pkg_version_file_name")]
public string PackageVersionFileName { get; set; } = default!;
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
internal sealed class GameChannelSDKsWrapper
{
[JsonPropertyName("game_channel_sdks")]
public List<GameChannelSDK> GameChannelSDKs { get; set; } = default!;
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
internal sealed class DeprecatedFile
{
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
internal sealed class DeprecatedFileConfigurationsWrapper
{
[JsonPropertyName("deprecated_file_configs")]
public List<DeprecatedFilesWrapper> DeprecatedFileConfigurations { get; set; } = default!;
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
internal sealed class DeprecatedFilesWrapper : GameIndexedObject
{
[JsonPropertyName("deprecated_files")]
public List<DeprecatedFile> DeprecatedFiles { get; set; } = default!;
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
internal sealed class Game
{
[JsonPropertyName("id")]
public string Id { get; set; } = default!;
[JsonPropertyName("biz")]
public string Biz { get; set; } = default!;
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
internal abstract class GameIndexedObject
{
[JsonPropertyName("game")]
public Game Game { get; set; } = default!;
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class HoyoPlayClient
{
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly ILogger<HoyoPlayClient> logger;
private readonly HttpClient httpClient;
public async ValueTask<Response<GamePackagesWrapper>> GetPackagesAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.IsOversea
? ApiOsEndpoints.SgHoyoPlayConnectGamePackages(scheme)
: ApiEndpoints.HoyoPlayConnectGamePackages(scheme);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(url)
.Get();
Response<GamePackagesWrapper>? resp = await builder
.SendAsync<Response<GamePackagesWrapper>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
public async ValueTask<Response<GameChannelSDKsWrapper>> GetChannelSDKAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.IsOversea
? ApiOsEndpoints.SgHoyoPlayConnectGameChannelSDKs(scheme)
: ApiEndpoints.HoyoPlayConnectGameChannelSDKs(scheme);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(url)
.Get();
Response<GameChannelSDKsWrapper>? resp = await builder
.SendAsync<Response<GameChannelSDKsWrapper>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
public async ValueTask<Response<DeprecatedFileConfigurationsWrapper>> GetDeprecatedFileConfigurationsAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.IsOversea
? ApiOsEndpoints.SgHoyoPlayConnectDeprecatedFileConfigs(scheme)
: ApiEndpoints.HoyoPlayConnectDeprecatedFileConfigs(scheme);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(url)
.Get();
Response<DeprecatedFileConfigurationsWrapper>? resp = await builder
.SendAsync<Response<DeprecatedFileConfigurationsWrapper>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
internal sealed class AudioPackageSegment : PackageSegment
{
[JsonPropertyName("language")]
public string Language { get; set; } = default!;
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
internal sealed class GameBranch
{
[JsonPropertyName("major")]
public Package Major { get; set; } = default!;
[JsonPropertyName("patches")]
public List<Package> Patches { get; set; } = default!;
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
internal sealed class GamePackage : GameIndexedObject
{
[JsonPropertyName("main")]
public GameBranch Main { get; set; } = default!;
[JsonPropertyName("pre_download")]
public GameBranch PreDownload { get; set; } = default!;
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
internal sealed class GamePackagesWrapper
{
[JsonPropertyName("game_packages")]
public List<GamePackage> GamePackages { get; set; } = default!;
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
internal sealed class Package
{
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
[JsonPropertyName("game_pkgs")]
public List<PackageSegment> GamePackages { get; set; } = default!;
[JsonPropertyName("audio_pkgs")]
public List<AudioPackageSegment> AudioPackages { get; set; } = default!;
[JsonPropertyName("res_list_url")]
public string ResourceListUrl { get; set; } = default!;
[JsonIgnore]
public List<PackageSegment> AllPackages
{
get => [.. GamePackages, .. AudioPackages];
}
}

View File

@@ -4,36 +4,30 @@
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
/// <summary>
/// 下载的文件
/// </summary>
[HighQuality]
internal partial class PathMd5
internal partial class PackageSegment
{
/// <summary>
/// 下载地址
/// </summary>
[JsonPropertyName("path")]
public string Path { get; set; } = default!;
[JsonPropertyName("url")]
public string Url { get; set; } = default!;
/// <summary>
/// MD5校验
/// </summary>
[JsonPropertyName("md5")]
public string Md5 { get; set; } = default!;
public string MD5 { get; set; } = default!;
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get => System.IO.Path.GetFileName(Path); }
[JsonPropertyName("size")]
public long Size { get; set; } = default!;
[JsonPropertyName("decompressed_size")]
public long DecompressedSize { get; set; } = default!;
[JsonIgnore]
public string DisplayName { get => System.IO.Path.GetFileName(Url); }
[Command("CopyPathCommand")]
private void CopyPathToClipboard()
{
IServiceProvider serviceProvider = Ioc.Default;
serviceProvider.GetRequiredService<IClipboardProvider>().SetText(Path);
serviceProvider.GetRequiredService<IClipboardProvider>().SetText(Url);
serviceProvider.GetRequiredService<IInfoBarService>().Success(SH.WebGameResourcePathCopySucceed);
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class Advertisement
{
[JsonPropertyName("background")]
public string Background { get; set; } = default!;
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
[JsonPropertyName("url")]
public string Url { get; set; } = default!;
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
[JsonPropertyName("bg_checksum")]
public string BackgroundChecksum { get; set; } = default!;
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class Banner
{
[JsonPropertyName("banner_id")]
public string BannerId { get; set; } = default!;
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
[JsonPropertyName("img")]
public string Image { get; set; } = default!;
[JsonPropertyName("url")]
public string Url { get; set; } = default!;
[JsonPropertyName("order")]
public string Order { get; set; } = default!;
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class GameContent
{
[JsonPropertyName("adv")]
public Advertisement? Advertisement { get; set; } = default!;
[JsonPropertyName("banner")]
public List<Banner> Banners { get; set; } = default!;
[JsonPropertyName("icon")]
public List<Icon> Icons { get; set; } = default!;
[JsonPropertyName("post")]
public List<Post> Posts { get; set; } = default!;
[JsonPropertyName("qq")]
public List<QQ> QQs { get; set; } = default!;
[JsonPropertyName("more")]
public More More { get; set; } = default!;
[JsonPropertyName("links")]
public Link Links { get; set; } = default!;
}

View File

@@ -1,43 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class Icon
{
[JsonPropertyName("icon_id")]
public string IconId { get; set; } = default!;
[JsonPropertyName("img")]
public string Image { get; set; } = default!;
[JsonPropertyName("url")]
public string Url { get; set; } = default!;
[JsonPropertyName("qr_img")]
public string QrImage { get; set; } = default!;
[JsonPropertyName("qr_desc")]
public string QrDescription { get; set; } = default!;
[JsonPropertyName("img_hover")]
public string ImageHover { get; set; } = default!;
[JsonPropertyName("other_links")]
public List<IconLink> OtherLinks { get; set; } = default!;
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("icon_link")]
public string IconLink { get; set; } = default!;
[JsonPropertyName("links")]
public List<IconLink> Links { get; set; } = default!;
[JsonPropertyName("enable_red_dot")]
public bool EnableRedDot { get; set; }
[JsonPropertyName("red_dot_content")]
public string RedDotContent { get; set; } = default!;
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
internal sealed class IconLink
{
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("url")]
public string Url { get; set; } = default!;
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class Link
{
[JsonPropertyName("faq")]
public string FAQ { get; set; } = default!;
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class More
{
[JsonPropertyName("activity_link")]
public string ActivityLink { get; set; } = default!;
[JsonPropertyName("announce_link")]
public string AnnounceLink { get; set; } = default!;
[JsonPropertyName("info_link")]
public string InfoLink { get; set; } = default!;
[JsonPropertyName("news_link")]
public string NewsLink { get; set; } = default!;
[JsonPropertyName("trends_link")]
public string TrendsLink { get; set; } = default!;
[JsonPropertyName("supply_link")]
public string SupplyLink { get; set; } = default!;
[JsonPropertyName("tools_link")]
public string ToolsLink { get; set; } = default!;
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class Post
{
[JsonPropertyName("post_id")]
public string PostId { get; set; } = default!;
/// <summary>
/// POST_TYPE_ACTIVITY|POST_TYPE_ANNOUNCE|POST_TYPE_INFO
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = default!;
[JsonPropertyName("url")]
public string Url { get; set; } = default!;
/// <summary>
/// MM/dd format
/// </summary>
[JsonPropertyName("show_time")]
public string ShowTime { get; set; } = default!;
[JsonPropertyName("order")]
public string Order { get; set; } = default!;
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
internal sealed class QQ
{
[JsonPropertyName("qq_id")]
public string QQId { get; set; } = default!;
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
[JsonPropertyName("number")]
public string Number { get; set; } = default!;
[JsonPropertyName("code")]
public string Code { get; set; } = default!;
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 差异文件
/// </summary>
[HighQuality]
internal sealed class DiffPackage : Package
{
/// <summary>
/// 是否为推荐更新
/// </summary>
[JsonPropertyName("is_recommended_update")]
public bool IsRecommendedUpdate { get; set; }
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 游戏
/// </summary>
[HighQuality]
internal sealed class Game
{
/// <summary>
/// 最新客户端
/// </summary>
[JsonPropertyName("latest")]
public LatestPackage Latest { get; set; } = default!;
/// <summary>
/// 相对于当前版本的之前版本的差异文件(非预下载)
/// </summary>
[JsonPropertyName("diffs")]
public List<DiffPackage> Diffs { get; set; } = default!;
}

View File

@@ -1,59 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 游戏资源
/// </summary>
[HighQuality]
internal sealed class GameResource
{
/// <summary>
/// 本体
/// </summary>
[JsonPropertyName("game")]
public Game Game { get; set; } = default!;
/// <summary>
/// 插件
/// </summary>
[JsonPropertyName("plugin")]
public Plugin Plugin { get; set; } = default!;
/// <summary>
/// 官网地址 https://ys.mihoyo.com/launcher
/// </summary>
[JsonPropertyName("web_url")]
public Uri WebUrl { get; set; } = default!;
/// <summary>
/// 强制更新文件 null
/// </summary>
[JsonPropertyName("force_update")]
public object? ForceUpdate { get; set; }
/// <summary>
/// 预下载
/// </summary>
[JsonPropertyName("pre_download_game")]
public Game? PreDownloadGame { get; set; }
/// <summary>
/// 过期更新包
/// </summary>
[JsonPropertyName("deprecated_packages")]
public List<NameMd5> DeprecatedPackages { get; set; } = default!;
/// <summary>
/// 渠道服 sdk
/// </summary>
[JsonPropertyName("sdk")]
public Sdk? Sdk { get; set; }
/// <summary>
/// 过期的单个文件
/// </summary>
[JsonPropertyName("deprecated_files")]
public List<NameMd5>? DeprecatedFiles { get; set; }
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
internal sealed class LatestPackage : Package
{
[JsonPropertyName("segments")]
public List<PackageSegment> Segments { get; set; } = default!;
public new string DisplayName { get => Name; }
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Text;
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
internal static class LatestPackageExtension
{
public static void Patch(this LatestPackage latest)
{
StringBuilder pathBuilder = new();
foreach (PackageSegment segment in latest.Segments)
{
pathBuilder.AppendLine(segment.Path);
}
latest.Path = pathBuilder.ToStringTrimEndReturn();
latest.Name = Path.GetFileName(latest.Segments[0].Path[..^4]); // .00X
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 资源文件
/// </summary>
[HighQuality]
internal class NameMd5
{
/// <summary>
/// 文件名称
/// 相对于 EXE 的路径,如 YuanShen_Data/Plugins/PCGameSDK.dll
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// Md5 校验值
/// </summary>
[JsonPropertyName("md5")]
public string Md5 { get; set; } = default!;
}

View File

@@ -1,53 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 最新客户端
/// </summary>
[HighQuality]
internal class Package : PathMd5
{
/// <summary>
/// 名称 空
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 版本
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
/// <summary>
/// 尺寸
/// </summary>
[JsonPropertyName("size")]
public long Size { get; set; } = default!;
/// <summary>
/// 主程序相对路径 YuanShen.exe
/// </summary>
public string Entry { get; set; } = default!;
/// <summary>
/// 语音包
/// </summary>
[JsonPropertyName("voice_packs")]
public List<VoicePackage> VoicePacks { get; set; } = default!;
/// <summary>
/// 松散文件
/// 用于校验完整性
/// </summary>
[JsonPropertyName("decompressed_path")]
public string DecompressedPath { get; set; } = default!;
/// <summary>
/// 包大小 bytes
/// </summary>
[JsonPropertyName("package_size")]
public long PackageSize { get; set; } = default!;
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
internal sealed class PackageSegment : PathMd5
{
[JsonPropertyName("package_size")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long PackageSize { get; set; } = default!;
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 插件
/// </summary>
[HighQuality]
internal sealed class Plugin
{
/// <summary>
/// 插件列表
/// </summary>
[JsonPropertyName("plugins")]
public List<PluginItem> Plugins { get; set; } = default!;
/// <summary>
/// 一般为 1
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 插件项
/// </summary>
[HighQuality]
internal sealed class PluginItem : NameMd5
{
/// <summary>
/// 版本 一般为空
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
/// <summary>
/// 下载地址
/// </summary>
[JsonPropertyName("path")]
public Uri Path { get; set; } = default!;
/// <summary>
/// 尺寸
/// </summary>
[JsonPropertyName("size")]
public long Size { get; set; } = default!;
/// <summary>
/// 一般为空
/// </summary>
[JsonPropertyName("entry")]
public string Entry { get; set; } = default!;
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// Sdk
/// </summary>
[HighQuality]
internal sealed class Sdk : PathMd5
{
/// <summary>
/// 版本
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
/// <summary>
/// 常量 sdk_pkg_version
/// </summary>
[JsonPropertyName("pkg_version")]
public string PackageVersion { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
[JsonPropertyName("desc")]
public string Description { get; set; } = default!;
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
/// <summary>
/// 语音包
/// </summary>
[HighQuality]
internal sealed class VoicePackage : PathMd5
{
/// <summary>
/// 语音
/// </summary>
[JsonPropertyName("language")]
public string Language { get; set; } = default!;
/// <summary>
/// 名称 一般为空
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 解压尺寸
/// </summary>
[JsonPropertyName("size")]
public long Size { get; set; }
/// <summary>
/// 包尺寸
/// </summary>
[JsonPropertyName("package_size")]
public long PackageSize { get; set; } = default!;
}

View File

@@ -1,78 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Content;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// 游戏资源客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class ResourceClient
{
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly HttpClient httpClient;
private readonly ILogger<ResourceClient> logger;
/// <summary>
/// 异步获取游戏资源
/// </summary>
/// <param name="scheme">方案</param>
/// <param name="token">取消令牌</param>
/// <returns>游戏资源</returns>
public async ValueTask<Response<GameResource>> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.IsOversea
? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
: ApiEndpoints.SdkStaticLauncherResource(scheme);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(url)
.Get();
Response<GameResource>? resp = await builder
.SendAsync<Response<GameResource>>(httpClient, logger, token)
.ConfigureAwait(false);
// 最新版完整包
if (resp is { Data.Game.Latest: LatestPackage latest })
{
latest.Patch();
}
// 预下载完整包
if (resp is { Data.PreDownloadGame.Latest: LatestPackage preDownloadLatest })
{
preDownloadLatest.Patch();
}
return Response.Response.DefaultIfNull(resp);
}
public async ValueTask<Response<GameContent>> GetContentAsync(LaunchScheme scheme, string languageCode, bool advOnly = true, CancellationToken token = default)
{
string url = scheme.IsOversea
? ApiOsEndpoints.SdkOsStaticLauncherContent(scheme, languageCode, advOnly)
: ApiEndpoints.SdkStaticLauncherContent(scheme, languageCode, advOnly);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(url)
.Get();
Response<GameContent>? resp = await builder
.SendAsync<Response<GameContent>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}