Compare commits

..

12 Commits

Author SHA1 Message Date
qhy040404
59a7d6746f fix #1208 2023-12-31 23:36:29 +08:00
Lightczx
b49cd924d0 add source link 2023-12-29 13:51:27 +08:00
Lightczx
49db3003c9 fix launch game window 2023-12-29 11:56:44 +08:00
Lightczx
314c771020 clear selected game account after scheme changed 2023-12-29 11:39:01 +08:00
Lightczx
967f6f76f0 refuse convert for game in Program Files folder 2023-12-29 11:11:53 +08:00
Lightczx
5d05c31af5 fix startup crash 2023-12-29 10:05:03 +08:00
Lightczx
64998453a1 Update LaunchGameViewModel.cs 2023-12-28 17:07:15 +08:00
Lightczx
9fdedd78d0 refactor Launch Game Pipeline 2023-12-28 17:06:45 +08:00
Lightczx
58e4d1b90e fix ci 2023-12-28 10:30:10 +08:00
Lightczx
e0d11bf9a0 impl #1199 2023-12-28 10:13:41 +08:00
Lightczx
51be2c76aa remove unused strings 2023-12-27 13:44:20 +08:00
DismissedLight
686d2378de Merge pull request #1232 from DGP-Studio/feat/elevate_restart 2023-12-27 13:34:48 +08:00
52 changed files with 750 additions and 624 deletions

View File

@@ -18,11 +18,11 @@ internal sealed class CommandLineBuilder
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
public CommandLineBuilder AppendIf(bool condition, string name, object? value = null)
{
return condition ? Append(name, value) : this;
}
@@ -35,7 +35,7 @@ internal sealed class CommandLineBuilder
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
{
return AppendIf(name, value is not null, value);
return AppendIf(value is not null, name, value);
}
/// <summary>

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoAsyncSetsAttribute : Attribute
{
public AlsoAsyncSetsAttribute(string propertyName)
{
}
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoAsyncSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoSetsAttribute : Attribute
{
public AlsoSetsAttribute(string propertyName)
{
}
public AlsoSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
public HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal enum HutaoExceptionKind
{
None,
}

View File

@@ -49,6 +49,13 @@ internal static class ThrowHelper
throw new NotSupportedException();
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static NotSupportedException NotSupported(string message)
{
throw new NotSupportedException(message);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)

View File

@@ -34,7 +34,7 @@
<ListView
Grid.Row="1"
ItemsSource="{Binding GameAccounts}"
ItemsSource="{Binding GameAccountsView}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("game_accounts")]
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string>
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>
{
/// <summary>
/// 内部Id
@@ -40,21 +40,17 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
/// <summary>
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
/// [MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810]
/// </summary>
public string MihoyoSDK { get; set; } = default!;
/// <summary>
/// 构造一个新的游戏内账号
/// </summary>
/// <param name="name">名称</param>
/// <param name="sdk">sdk</param>
/// <returns>游戏内账号</returns>
public static GameAccount From(string name, string sdk)
public static GameAccount From(string name, string sdk, SchemeType type)
{
return new()
{
Name = name,
MihoyoSDK = sdk,
Type = type,
};
}

View File

@@ -9,18 +9,18 @@ namespace Snap.Hutao.Model.Entity.Primitive;
[HighQuality]
internal enum SchemeType
{
/// <summary>
/// 国际服
/// </summary>
Hoyoverse,
/// <summary>
/// 国服官服
/// </summary>
Official,
ChineseOfficial,
/// <summary>
/// 国际服
/// </summary>
Oversea,
/// <summary>
/// 渠道服
/// </summary>
Bilibili,
ChineseBilibili,
}

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Import</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Select PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Welcome to Snap Hutao, Traveler ~</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Unable to set registry key without enabling long path</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell installation directory not found</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Unable to read game config file {0}, file may be not exist</value>
</data>
@@ -2432,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>When setting the game path, please select the game program (Yuanshen.exe or GenshinImpact.exe) instead of the game launcher (launcher.exe)</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>Snap Hutao uses PowerShell to modify information in registry to change game accounts</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell Path</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell Experience</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Impor</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Pilih PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Selamat Datang di Snap Hutao, Traveler ~</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Tidak dapat mengatur kunci registri tanpa mengaktifkan path panjang</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>Direktori instalasi PowerShell tidak ditemukan</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Tidak dapat membaca file konfigurasi game {0}, file mungkin tidak ada</value>
</data>
@@ -2432,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>Saat mengatur jalur permainan, pilih program permainan (Yuanshen.exe atau GenshinImpact.exe) bukan peluncur permainan (launcher.exe)</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>Snap Hutao menggunakan PowerShell untuk memodifikasi informasi di registri untuk mengubah akun Game</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>Path PowerShell</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Pengalaman Shell</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>インポート</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>PowerShellを選択</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>胡桃へようこそ</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>長いパスのサポートがオフになっているため、レジストリキーを編集できません。</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShellのインストールディレクトリが見つかりません</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>ゲーム設定ファイル {0} の読み込みに失敗しました。ファイルが存在していない可能性があります。</value>
</data>
@@ -2432,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>ゲームのパスを設定する際、本体YuanShen.exe または GenshinImpact.exeを選んでください。ランチャーlauncher.exeではありません</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃のゲームランチャーはPowershellを介してレジストリを変更し、ゲームで使用するアカウントを変更します。</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell パス</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell エクスペリエンス</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>가져오기</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>选择 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>欢迎使用胡桃</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>긴 경로 기능이 켜지지 않아 레지스트리 키 값을 설정할 수 없습니다</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell 설치 경로를 찾을 수 없습니다</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data>
@@ -2432,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value>
</data>

View File

@@ -1553,6 +1553,9 @@
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
<value>切换账号失败</value>
</data>
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
</data>
<data name="ViewModelSettingActionComplete" xml:space="preserve">
<value>操作完成</value>
</data>
@@ -1577,12 +1580,6 @@
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
<value>无感验证复合 Url 配置成功</value>
</data>
<data name="ViewModelSettingNotRunningInElevatedMode" xml:space="preserve">
<value>当前以用户身份运行</value>
</data>
<data name="ViewModelSettingRunningInElevatedMode" xml:space="preserve">
<value>当前以管理员身份运行</value>
</data>
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
<value>设置数据目录成功,重启以应用更改</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Импорт</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Выберите PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Добро пожаловать в Snap Hutao, путешественник ~</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未开启长路径功能,无法设置注册表键值</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安装目录</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data>
@@ -2432,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>匯入</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>選擇 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>歡迎使用胡桃</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未開啓長路徑功能,無法設定注冊表鍵值</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安裝目錄</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>無法讀取遊戲設定檔 {0},可能是檔案不存在</value>
</data>
@@ -2432,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>設置游戲路徑時請選擇游戲本體YuanShen.exe 或 GenshinImpact.exe 而不是啓動器launcher.exe</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改註冊表中的信息以修改遊戲內賬號</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路徑</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 體驗</value>
</data>

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.View.Dialog;
using System.Collections.ObjectModel;
@@ -24,67 +25,51 @@ internal sealed partial class GameAccountService : IGameAccountService
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
}
public async ValueTask<GameAccount?> DetectGameAccountAsync()
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType)
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
string? registrySdk = RegistryInterop.Get(schemeType);
if (string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = null;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
if (account is null)
{
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
return default;
}
return default;
GameAccount? account = SingleGameAccountOrDefault(gameAccounts, registrySdk);
if (account is null)
{
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk, schemeType);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
}
public GameAccount? DetectCurrentGameAccount()
public GameAccount? DetectCurrentGameAccount(SchemeType schemeType)
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
string? registrySdk = RegistryInterop.Get(schemeType);
if (!string.IsNullOrEmpty(registrySdk))
if (string.IsNullOrEmpty(registrySdk))
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
return default;
}
return null;
return SingleGameAccountOrDefault(gameAccounts, registrySdk);
}
public bool SetGameAccount(GameAccount account)
@@ -100,12 +85,12 @@ internal sealed partial class GameAccountService : IGameAccountService
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
await taskContext.SwitchToMainThreadAsync();
gameAccount.UpdateName(name);
// sync database
@@ -116,11 +101,24 @@ internal sealed partial class GameAccountService : IGameAccountService
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(gameAccounts);
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync();
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
}
private static GameAccount? SingleGameAccountOrDefault(ObservableCollection<GameAccount> gameAccounts, string registrySdk)
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
throw ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game.Account;
@@ -12,9 +13,9 @@ internal interface IGameAccountService
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
GameAccount? DetectCurrentGameAccount();
GameAccount? DetectCurrentGameAccount(SchemeType schemeType);
ValueTask<GameAccount?> DetectGameAccountAsync();
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);

View File

@@ -2,7 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using System.Runtime.InteropServices;
using System.Text;
@@ -13,9 +15,10 @@ namespace Snap.Hutao.Service.Game.Account;
/// </summary>
internal static class RegistryInterop
{
private const string GenshinPath = @"Software\miHoYo\原神";
private const string GenshinKey = $@"HKEY_CURRENT_USER\{GenshinPath}";
private const string SdkChineseKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string ChineseKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
private const string OverseaKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\Genshin Impact";
private const string SdkChineseValueName = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string SdkOverseaValueName = "MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810";
public static bool Set(GameAccount? account)
{
@@ -23,10 +26,10 @@ internal static class RegistryInterop
{
// 存回注册表的字节需要 '\0' 结尾
byte[] target = [.. Encoding.UTF8.GetBytes(account.MihoyoSDK), 0];
Registry.SetValue(GenshinKey, SdkChineseKey, target);
(string keyName, string valueName) = GetKeyValueName(account.Type);
Registry.SetValue(keyName, valueName, target);
string? get = Get();
if (get == account.MihoyoSDK)
if (Get(account.Type) == account.MihoyoSDK)
{
return true;
}
@@ -35,20 +38,31 @@ internal static class RegistryInterop
return false;
}
public static unsafe string? Get()
public static unsafe string? Get(SchemeType scheme)
{
object? sdk = Registry.GetValue(GenshinKey, SdkChineseKey, Array.Empty<byte>());
(string keyName, string valueName) = GetKeyValueName(scheme);
object? sdk = Registry.GetValue(keyName, valueName, Array.Empty<byte>());
if (sdk is byte[] bytes)
if (sdk is not byte[] bytes)
{
fixed (byte* pByte = bytes)
{
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
return null;
}
return null;
fixed (byte* pByte = bytes)
{
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
}
private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme)
{
return scheme switch
{
SchemeType.ChineseOfficial => (ChineseKeyName, SdkChineseValueName),
SchemeType.Oversea => (OverseaKeyName, SdkOverseaValueName),
_ => throw ThrowHelper.NotSupported($"Invalid account SchemeType: {scheme}"),
};
}
}

View File

@@ -34,21 +34,6 @@ internal readonly struct ChannelOptions
/// </summary>
public readonly string? ConfigFilePath;
/// <summary>
/// 构造一个新的多通道
/// </summary>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="configFilePath">配置文件路径</param>
public ChannelOptions(string? channel, string? subChannel, bool isOversea, string? configFilePath = null)
{
_ = Enum.TryParse(channel, out Channel);
_ = Enum.TryParse(subChannel, out SubChannel);
IsOversea = isOversea;
ConfigFilePath = configFilePath;
}
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
{
Channel = channel;
@@ -56,24 +41,33 @@ internal readonly struct ChannelOptions
IsOversea = isOversea;
}
/// <summary>
/// 配置文件未找到
/// </summary>
/// <param name="isOversea">是否为国际服</param>
/// <param name="configFilePath">配置文件期望路径</param>
/// <returns>选项</returns>
public ChannelOptions(string? channel, string? subChannel, bool isOversea)
{
_ = Enum.TryParse(channel, out Channel);
_ = Enum.TryParse(subChannel, out SubChannel);
IsOversea = isOversea;
}
private ChannelOptions(bool isOversea, string? configFilePath)
{
IsOversea = isOversea;
ConfigFilePath = configFilePath;
}
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
{
return new(null, null, isOversea, configFilePath);
return new(isOversea, configFilePath);
}
/// <inheritdoc/>
public override string ToString()
{
return $"[ChannelType:{Channel}] [SubChannel:{SubChannel}] [IsOversea: {IsOversea}]";
return $$"""
{ ChannelType: {{Channel}}, SubChannel: {{SubChannel}}, IsOversea: {{IsOversea}}}
""";
}
// DO NOT DELETE used in HashSet
// DO NOT DELETE, used in HashSet
public override int GetHashCode()
{
return HashCode.Combine(Channel, SubChannel, IsOversea);

View File

@@ -17,9 +17,12 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public ChannelOptions GetChannelOptions()
{
string gamePath = launchOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
{
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}");
}
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
if (!File.Exists(configPath))
{
@@ -38,10 +41,10 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public bool SetChannelOptions(LaunchScheme scheme)
{
string gamePath = launchOptions.GamePath;
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
{
return false;
}
List<IniElement> elements = default!;
try
@@ -70,14 +73,16 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
{
if (element is IniParameter parameter)
{
if (parameter.Key == "channel")
if (parameter.Key is ChannelOptions.ChannelName)
{
changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
continue;
}
if (parameter.Key == "sub_channel")
if (parameter.Key is ChannelOptions.SubChannelName)
{
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
continue;
}
}
}

View File

@@ -9,38 +9,13 @@ namespace Snap.Hutao.Service.Game;
[HighQuality]
internal static class GameConstants
{
/// <summary>
/// 设置文件
/// </summary>
public const string ConfigFileName = "config.ini";
/// <summary>
/// 国服文件名
/// </summary>
public const string YuanShenFileName = "YuanShen.exe";
/// <summary>
/// 外服文件名
/// </summary>
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
public const string GenshinImpactFileName = "GenshinImpact.exe";
/// <summary>
/// 国服数据文件夹
/// </summary>
public const string GenshinImpactFileNameUpper = "GENSHINIMPACT.EXE";
public const string YuanShenData = "YuanShen_Data";
/// <summary>
/// 国际服数据文件夹
/// </summary>
public const string GenshinImpactData = "GenshinImpact_Data";
/// <summary>
/// 国服进程名
/// </summary>
public const string YuanShenProcessName = "YuanShen";
/// <summary>
/// 外服进程名
/// </summary>
public const string GenshinImpactProcessName = "GenshinImpact";
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
@@ -51,15 +52,15 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
}
/// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync()
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
{
return gameAccountService.DetectGameAccountAsync();
return gameAccountService.DetectGameAccountAsync(scheme);
}
/// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount()
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
{
return gameAccountService.DetectCurrentGameAccount();
return gameAccountService.DetectCurrentGameAccount(scheme);
}
/// <inheritdoc/>

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Scheme;
namespace Snap.Hutao.Service.Game;
internal static class GameServiceFacadeExtension
{
public static GameAccount? DetectCurrentGameAccount(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
{
return gameServiceFacade.DetectCurrentGameAccount(scheme.GetSchemeType());
}
public static ValueTask<GameAccount?> DetectGameAccountAsync(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
{
return gameServiceFacade.DetectGameAccountAsync(scheme.GetSchemeType());
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Scheme;
@@ -28,7 +29,7 @@ internal interface IGameServiceFacade
/// <param name="uid">uid</param>
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
ValueTask<GameAccount?> DetectGameAccountAsync();
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
/// <summary>
/// 异步获取游戏路径
@@ -86,9 +87,5 @@ internal interface IGameServiceFacade
/// <returns>是否更改了ini文件</returns>
bool SetChannelOptions(LaunchScheme scheme);
/// <summary>
/// 检测账号
/// </summary>
/// <returns>账号</returns>
GameAccount? DetectCurrentGameAccount();
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
}

View File

@@ -9,12 +9,25 @@ namespace Snap.Hutao.Service.Game;
internal static class LaunchOptionsExtension
{
public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory)
{
gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
return false;
}
return true;
}
public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = options.GamePath;
gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameFolder))
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
gameFileName = default;
return false;
@@ -42,6 +55,18 @@ internal static class LaunchOptionsExtension
return true;
}
public static bool TryGetGamePathAndFilePathByName(this LaunchOptions options, string fileName, out string gamePath, [NotNullWhen(true)] out string? filePath)
{
if (options.TryGetGamePathAndGameDirectory(out gamePath, out string? gameDirectory))
{
filePath = Path.Combine(gameDirectory, fileName);
return true;
}
filePath = default;
return false;
}
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
{
string gamePath = options.GamePath;

View File

@@ -4,20 +4,13 @@
namespace Snap.Hutao.Service.Game.Locator;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
[Injection(InjectAs.Singleton, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider;
public IGameLocator Create(GameLocationSource source)
{
return source switch
{
GameLocationSource.Registry => serviceProvider.GetRequiredService<RegistryLauncherLocator>(),
GameLocationSource.UnityLog => serviceProvider.GetRequiredService<UnityLogGameLocator>(),
GameLocationSource.Manual => serviceProvider.GetRequiredService<ManualGameLocator>(),
_ => throw Must.NeverHappen(),
};
return serviceProvider.GetRequiredKeyedService<IGameLocator>(source);
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Locator;
internal static class GameLocatorFactoryExtensions
{
public static ValueTask<ValueResult<bool, string>> LocateAsync(this IGameLocatorFactory factory, GameLocationSource source)
{
return factory.Create(source).LocateGamePathAsync();
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Manual)]
internal sealed partial class ManualGameLocator : IGameLocator
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
@@ -26,7 +26,7 @@ internal sealed partial class ManualGameLocator : IGameLocator
if (isPickerOk)
{
string fileName = System.IO.Path.GetFileName(file);
if (fileName is GameConstants.YuanShenFileName or GameConstants.GenshinImpactFileName)
if (fileName.ToUpperInvariant() is GameConstants.YuanShenFileNameUpper or GameConstants.GenshinImpactFileNameUpper)
{
return ValueTask.FromResult<ValueResult<bool, string>>(new(true, file));
}

View File

@@ -13,9 +13,10 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Registry)]
internal sealed partial class RegistryLauncherLocator : IGameLocator
{
private const string RegistryKeyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神";
private readonly ITaskContext taskContext;
/// <inheritdoc/>
@@ -29,50 +30,37 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
{
return result;
}
else
{
string? path = Path.GetDirectoryName(result.Value);
ArgumentException.ThrowIfNullOrEmpty(path);
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
string? escapedPath;
using (FileStream stream = File.OpenRead(configPath))
{
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
escapedPath = elements
.OfType<IniParameter>()
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
}
if (escapedPath is not null)
{
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
return new(true, gamePath);
}
string? path = Path.GetDirectoryName(result.Value);
ArgumentException.ThrowIfNullOrEmpty(path);
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
string? escapedPath;
using (FileStream stream = File.OpenRead(configPath))
{
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
escapedPath = elements
.OfType<IniParameter>()
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
}
if (!string.IsNullOrEmpty(escapedPath))
{
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
return new(true, gamePath);
}
return new(false, string.Empty);
}
private static ValueResult<bool, string> LocateInternal(string key)
private static ValueResult<bool, string> LocateInternal(string valueName)
{
using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"))
if (Registry.GetValue(RegistryKeyName, valueName, null) is string path)
{
if (uninstallKey is not null)
{
if (uninstallKey.GetValue(key) is string path)
{
return new(true, path);
}
else
{
return new(false, default!);
}
}
else
{
return new(false, default!);
}
return new(true, path);
}
return new(false, default!);
}
private static string Unescape(string str)

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)]
internal sealed partial class UnityLogGameLocator : IGameLocator
{
private readonly ITaskContext taskContext;

View File

@@ -21,7 +21,7 @@ internal sealed partial class GamePackageService : IGamePackageService
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
{
return false;
}
@@ -47,8 +47,7 @@ internal sealed partial class GamePackageService : IGamePackageService
if (!launchScheme.ExecutableMatches(gameFileName))
{
// We can't start the game
// when we failed to convert game
// We can't start the game when we failed to convert game
if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false))
{
return false;
@@ -67,6 +66,13 @@ internal sealed partial class GamePackageService : IGamePackageService
private static bool CheckDirectoryPermissions(string folder)
{
// Program Files has special permissions limitation.
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
if (folder.StartsWith(programFiles, StringComparison.OrdinalIgnoreCase))
{
return false;
}
try
{
string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");

View File

@@ -15,6 +15,7 @@ using System.IO.Compression;
using System.Net.Http;
using System.Text.RegularExpressions;
using static Snap.Hutao.Service.Game.GameConstants;
using RelativePathVersionItemDictionary = System.Collections.Generic.Dictionary<string, Snap.Hutao.Service.Game.Package.VersionItem>;
namespace Snap.Hutao.Service.Game.Package;
@@ -58,15 +59,15 @@ internal sealed partial class PackageConverter
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
// Step 1
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
// Step 2
List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
List<PackageItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
diffOperations.SortBy(i => i.Type);
// Step 3
@@ -116,16 +117,16 @@ internal sealed partial class PackageConverter
}
}
private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
private static IEnumerable<PackageItemOperationInfo> GetItemOperationInfos(RelativePathVersionItemDictionary remote, RelativePathVersionItemDictionary local)
{
foreach ((string remoteName, VersionItem remoteItem) in remote)
{
if (local.TryGetValue(remoteName, out VersionItem? localItem))
{
if (!remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))
if (!(remoteItem.FileSize == localItem.FileSize && remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase)))
{
// 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项
yield return new(ItemOperationType.Replace, remoteItem, localItem);
yield return new(PackageItemOperationType.Replace, remoteItem, localItem);
}
// 同名同MD5跳过
@@ -134,22 +135,22 @@ internal sealed partial class PackageConverter
else
{
// 本地没有发现同名项
yield return new(ItemOperationType.Add, remoteItem, remoteItem);
yield return new(PackageItemOperationType.Add, remoteItem, remoteItem);
}
}
foreach ((_, VersionItem localItem) in local)
{
yield return new(ItemOperationType.Backup, localItem, localItem);
yield return new(PackageItemOperationType.Backup, localItem, localItem);
}
}
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
private static partial Regex DataFolderRegex();
private async ValueTask<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
private async ValueTask<RelativePathVersionItemDictionary> GetVersionItemsAsync(Stream stream)
{
Dictionary<string, VersionItem> results = [];
RelativePathVersionItemDictionary results = [];
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row)
@@ -164,7 +165,7 @@ internal sealed partial class PackageConverter
return results;
}
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
private async ValueTask<RelativePathVersionItemDictionary> GetRemoteItemsAsync(string pkgVersionUrl)
{
try
{
@@ -179,7 +180,7 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder)
private async ValueTask<RelativePathVersionItemDictionary> GetLocalItemsAsync(string gameFolder)
{
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
{
@@ -187,23 +188,23 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask PrepareCacheFilesAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
{
foreach (ItemOperationInfo info in operations)
foreach (PackageItemOperationInfo info in operations)
{
switch (info.Type)
{
case ItemOperationType.Backup:
case PackageItemOperationType.Backup:
continue;
case ItemOperationType.Replace:
case ItemOperationType.Add:
case PackageItemOperationType.Replace:
case PackageItemOperationType.Add:
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
break;
}
}
}
private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
{
// 还原正确的远程地址
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
@@ -257,16 +258,16 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask<bool> ReplaceGameResourceAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
{
// 执行下载与移动操作
foreach (ItemOperationInfo info in operations)
foreach (PackageItemOperationInfo info in operations)
{
(bool moveToBackup, bool moveToTarget) = info.Type switch
{
ItemOperationType.Backup => (true, false),
ItemOperationType.Replace => (true, true),
ItemOperationType.Add => (false, true),
PackageItemOperationType.Backup => (true, false),
PackageItemOperationType.Replace => (true, true),
PackageItemOperationType.Add => (false, true),
_ => (false, false),
};
@@ -321,7 +322,7 @@ internal sealed partial class PackageConverter
return true;
}
private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context)
private async ValueTask ReplacePackageVersionFilesAsync(PackageConverterFileSystemContext context)
{
foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version"))
{

View File

@@ -6,7 +6,7 @@ using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
internal readonly struct PackageConvertContext
internal readonly struct PackageConverterFileSystemContext
{
public readonly string GameFolder;
public readonly string ServerCacheFolder;
@@ -22,7 +22,7 @@ internal readonly struct PackageConvertContext
public readonly string ScatteredFilesUrl;
public readonly string PkgVersionUrl;
public PackageConvertContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
public PackageConverterFileSystemContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
{
GameFolder = gameFolder;
ServerCacheFolder = Path.Combine(dataFolder, "ServerCache");
@@ -37,7 +37,8 @@ internal readonly struct PackageConvertContext
? (YuanShenData, GenshinImpactData)
: (GenshinImpactData, YuanShenData);
(FromDataFolder, ToDataFolder) = (Path.Combine(GameFolder, FromDataFolderName), Path.Combine(GameFolder, ToDataFolderName));
FromDataFolder = Path.Combine(GameFolder, FromDataFolderName);
ToDataFolder = Path.Combine(GameFolder, ToDataFolderName);
ScatteredFilesUrl = scatteredFilesUrl;
PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version";

View File

@@ -10,12 +10,12 @@ namespace Snap.Hutao.Service.Game.Package;
/// </summary>
[HighQuality]
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
internal readonly struct ItemOperationInfo
internal readonly struct PackageItemOperationInfo
{
/// <summary>
/// 操作的类型
/// </summary>
public readonly ItemOperationType Type;
public readonly PackageItemOperationType Type;
/// <summary>
/// 目标文件
@@ -33,7 +33,7 @@ internal readonly struct ItemOperationInfo
/// <param name="type">操作类型</param>
/// <param name="remote">远程</param>
/// <param name="local">本地</param>
public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
public PackageItemOperationInfo(PackageItemOperationType type, VersionItem remote, VersionItem local)
{
Type = type;
Remote = remote;

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Game.Package;
/// 包文件操作的类型
/// </summary>
[HighQuality]
internal enum ItemOperationType
internal enum PackageItemOperationType
{
/// <summary>
/// 需要备份

View File

@@ -2,14 +2,13 @@
// Licensed under the MIT license.
using CommunityToolkit.Common;
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包更新状态
/// </summary>
internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
internal sealed class PackageReplaceStatus
{
/// <summary>
/// 构造一个新的包更新状态
@@ -34,10 +33,6 @@ internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
}
private PackageReplaceStatus()
{
}
public string Name { get; set; } = default!;
/// <summary>
@@ -54,19 +49,4 @@ internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
/// 是否有进度
/// </summary>
public bool IsIndeterminate { get => Percent < 0; }
/// <summary>
/// 克隆
/// </summary>
/// <returns>克隆的实例</returns>
public PackageReplaceStatus Clone()
{
// 进度需要在主线程上创建
return new()
{
Name = Name,
Description = Description,
Percent = Percent,
};
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.Game.PathAbstraction;
[Injection(InjectAs.Singleton, typeof(IGamePathService))]
internal sealed partial class GamePathService : IGamePathService
{
private readonly IServiceProvider serviceProvider;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly LaunchOptions launchOptions;
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
@@ -17,24 +17,16 @@ internal sealed partial class GamePathService : IGamePathService
// Cannot find in setting
if (string.IsNullOrEmpty(launchOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
bool isOk;
string path;
// Try locate by unity log
(isOk, path) = await locatorFactory
.Create(GameLocationSource.UnityLog)
.LocateGamePathAsync()
.ConfigureAwait(false);
(isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.UnityLog).ConfigureAwait(false);
if (!isOk)
{
// Try locate by registry
(isOk, path) = await locatorFactory
.Create(GameLocationSource.Registry)
.LocateGamePathAsync()
.ConfigureAwait(false);
(isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Registry).ConfigureAwait(false);
}
if (isOk)
@@ -48,13 +40,11 @@ internal sealed partial class GamePathService : IGamePathService
}
}
if (!string.IsNullOrEmpty(launchOptions.GamePath))
{
return new(true, launchOptions.GamePath);
}
else
if (string.IsNullOrEmpty(launchOptions.GamePath))
{
return new(false, default!);
}
return new(true, launchOptions.GamePath);
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker;
@@ -18,6 +19,7 @@ namespace Snap.Hutao.Service.Game.Process;
internal sealed partial class GameProcessService : IGameProcessService
{
private readonly IServiceProvider serviceProvider;
private readonly IProgressFactory progressFactory;
private readonly IDiscordService discordService;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
@@ -109,13 +111,13 @@ internal sealed partial class GameProcessService : IGameProcessService
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", launchOptions.IsBorderless)
.AppendIf("-window-mode", launchOptions.IsExclusive, "exclusive")
.AppendIf(launchOptions.IsBorderless, "-popupwindow")
.AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive")
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
.AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
.AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
.AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
.AppendIf("-platform_type CLOUD_THIRD_PARTY_MOBILE", launchOptions.IsUseCloudThirdPartyMobile)
.AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth)
.AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight)
.AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value)
.AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE")
.ToString();
}
@@ -138,7 +140,7 @@ internal sealed partial class GameProcessService : IGameProcessService
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
#pragma warning restore CA1859
UnlockTimingOptions options = new(100, 20000, 3000);
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
IProgress<UnlockerStatus> lockerProgress = progressFactory.CreateForMainThread<UnlockerStatus>(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, lockerProgress, token);
}

View File

@@ -59,11 +59,11 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
public static bool ExecutableIsOversea(string gameFileName)
{
return gameFileName switch
return gameFileName.ToUpperInvariant() switch
{
GameConstants.GenshinImpactFileName => true,
GameConstants.YuanShenFileName => false,
_ => throw Requires.Fail("无效的游戏可执行文件名称{0}", gameFileName),
GameConstants.GenshinImpactFileNameUpper => true,
GameConstants.YuanShenFileNameUpper => false,
_ => throw Requires.Fail("Invalid game executable file name{0}", gameFileName),
};
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal static class LaunchSchemeExtension
{
public static SchemeType GetSchemeType(this LaunchScheme scheme)
{
return (scheme.Channel, scheme.IsOversea) switch
{
(ChannelType.Bili, false) => SchemeType.ChineseBilibili,
(_, false) => SchemeType.ChineseOfficial,
(_, true) => SchemeType.Oversea,
};
}
}

View File

@@ -228,7 +228,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
rip += 5U;
rip += (nuint)(*(int*)(rip + 2) + 6);
rip += (nuint)(*(int*)(rip + 2U) + 6);
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
@@ -236,6 +236,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
// CALL or JMP
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
{
rip += (nuint)(*(int*)(rip + 1) + 5);

View File

@@ -303,12 +303,16 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.5.0">
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.9.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -67,7 +67,7 @@
VerticalAlignment="Bottom">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding GameAccounts}"
ItemsSource="{Binding GameAccountsView}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>

View File

@@ -76,16 +76,15 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
private async ValueTask InitializeAsync()
{
if (isInitializingOrInitialized)
if (!isInitializingOrInitialized)
{
return;
isInitializingOrInitialized = true;
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
}
isInitializingOrInitialized = true;
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
RefreshWebview2Content();
}
@@ -128,6 +127,9 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
string source = SourceProvider.GetSource(userAndUid);
if (!string.IsNullOrEmpty(source))
{
CoreWebView2Navigator navigator = new(coreWebView2);
await navigator.NavigateAsync("about:blank").ConfigureAwait(true);
try
{
await coreWebView2.Profile.ClearBrowsingDataAsync();
@@ -138,9 +140,6 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
await coreWebView2.DeleteCookiesAsync(userAndUid.IsOversea).ConfigureAwait(true);
}
CoreWebView2Navigator navigator = new(coreWebView2);
await navigator.NavigateAsync("about:blank").ConfigureAwait(true);
coreWebView2
.SetCookie(user.CookieToken, user.LToken, userAndUid.IsOversea)
.SetMobileUserAgent(userAndUid.IsOversea);

View File

@@ -198,7 +198,7 @@
<Border Style="{StaticResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccounts}"
ItemsSource="{Binding GameAccountsView}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border>

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.ViewModel;

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
namespace Snap.Hutao.ViewModel.Game;
internal sealed class GameAccountFilter
{
private readonly SchemeType? type;
public GameAccountFilter(SchemeType? type)
{
this.type = type;
}
public bool Filter(object? item)
{
if (type is null)
{
return true;
}
return item is GameAccount account && account.Type == type;
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.ViewModel.Game;
internal static class LaunchGameShared
{
public static LaunchScheme? GetCurrentLaunchSchemeFromConfigFile(IGameServiceFacade gameService, IInfoBarService infoBarService)
{
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
{
try
{
return KnownLaunchSchemes.Get().Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
throw ThrowHelper.NotSupported($"不支持的 MultiChannel: {options}");
}
}
}
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
}
return default;
}
}

View File

@@ -1,17 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using CommunityToolkit.WinUI.Collections;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Diagnostics.CodeAnalysis;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.PathAbstraction;
@@ -56,46 +56,13 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly AppOptions appOptions;
private LaunchScheme? selectedScheme;
private ObservableCollection<GameAccount>? gameAccounts;
private AdvancedCollectionView? gameAccountsView;
private GameAccount? selectedGameAccount;
private GameResource? gameResource;
private bool gamePathSelectedAndValid;
private ImmutableList<GamePathEntry> gamePathEntries;
private GamePathEntry? selectedGamePathEntry;
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
public LaunchScheme? SelectedScheme
{
get => selectedScheme;
set
{
SetProperty(ref selectedScheme, value, UpdateGameResourceAsync);
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)
{
if (scheme is null)
{
return;
}
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
}
}
}
}
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
private GameAccountFilter? gameAccountFilter;
public LaunchOptions LaunchOptions { get => launchOptions; }
@@ -105,8 +72,22 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
public AppOptions AppOptions { get => appOptions; }
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
[AlsoAsyncSets(nameof(GameResource), nameof(GameAccountsView))]
public LaunchScheme? SelectedScheme
{
get => selectedScheme;
set => SetSelectedSchemeAsync(value).SafeForget();
}
public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); }
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
[AlsoAsyncSets(nameof(SelectedScheme), nameof(GameAccountsView))]
public bool GamePathSelectedAndValid
{
get => gamePathSelectedAndValid;
@@ -114,112 +95,100 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
if (SetProperty(ref gamePathSelectedAndValid, value) && value)
{
InitializeUICoreAsync().SafeForget();
RefreshUIAsync().SafeForget();
}
async ValueTask RefreshUIAsync()
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService);
await taskContext.SwitchToMainThreadAsync();
await SetSelectedSchemeAsync(scheme).ConfigureAwait(true);
TrySetGameAccountByDesiredUid();
// Try set to the current account.
if (SelectedScheme is not null)
{
// The GameAccount is gaurenteed to be in the view, bacause the scheme is synced
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(SelectedScheme);
}
else
{
infoBarService.Warning(SH.ViewModelLaunchGameSchemeNotSelected);
}
}
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
}
void TrySetGameAccountByDesiredUid()
{
// Sync uid, almost never hit, so we are not so care about performance
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
{
ArgumentNullException.ThrowIfNull(GameAccountsView);
// Exists in the source collection
if (GameAccountsView.SourceCollection.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount)
{
SelectedGameAccount = GameAccountsView.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid);
// But not exists in the view for current scheme
if (SelectedGameAccount is null)
{
infoBarService.Warning(SH.FormatViewModelLaunchGameUnableToSwitchUidAttachedGameAccount(uid, sourceAccount.Name));
}
}
}
}
}
}
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
[AlsoSets(nameof(GamePathSelectedAndValid))]
public GamePathEntry? SelectedGamePathEntry
{
get => selectedGamePathEntry;
set => UpdateSelectedGamePathEntry(value, true);
set
{
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)))
{
if (IsViewDisposed)
{
return;
}
launchOptions.GamePath = value?.Path ?? string.Empty;
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
}
}
}
protected override ValueTask<bool> InitializeUIAsync()
{
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SelectedGamePathEntry = entry;
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions();
return ValueTask.FromResult(true);
}
private async ValueTask InitializeUICoreAsync()
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
{
try
{
SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
throw new NotSupportedException($"不支持的 MultiChannel: {options}");
}
}
}
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
}
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
// Sync uid
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
{
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
}
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
catch (OperationCanceledException)
{
}
}
private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack)
{
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)) && setBack)
{
if (IsViewDisposed)
{
return;
}
launchOptions.GamePath = value?.Path ?? string.Empty;
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
}
}
[Command("SetGamePathCommand")]
private async Task SetGamePathAsync()
{
IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
(bool isOk, string path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Manual).ConfigureAwait(false);
if (!isOk)
{
return;
}
await taskContext.SwitchToMainThreadAsync();
try
{
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
}
catch (SqliteException ex)
{
// 文件夹权限不足,无法写入数据库
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
}
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
}
[Command("ResetGamePathCommand")]
@@ -256,24 +225,20 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
// Always ensure game resources
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
{
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail, dialog.State.Name);
return;
}
else
{
await taskContext.SwitchToMainThreadAsync();
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
UpdateSelectedGamePathEntry(entry, true);
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions();
}
}
if (SelectedGameAccount is not null)
if (SelectedGameAccount is not null && !gameService.SetGameAccount(SelectedGameAccount))
{
if (!gameService.SetGameAccount(SelectedGameAccount))
{
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
return;
}
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
return;
}
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
@@ -297,11 +262,14 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
try
{
GameAccount? account = await gameService.DetectGameAccountAsync().ConfigureAwait(false);
if (SelectedScheme is null)
{
infoBarService.Error(SH.ViewModelLaunchGameSchemeNotSelected);
return;
}
// If user canceled the operation, the return is null,
// and thus we should not set SelectedAccount
if (account is not null)
// If user canceled the operation, the return is null
if (await gameService.DetectGameAccountAsync(SelectedScheme).ConfigureAwait(false) is { } account)
{
await taskContext.SwitchToMainThreadAsync();
SelectedGameAccount = account;
@@ -359,4 +327,54 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
await Windows.System.Launcher.LaunchFolderPathAsync(screenshot);
}
}
private async ValueTask SetSelectedSchemeAsync(LaunchScheme? value)
{
if (SetProperty(ref selectedScheme, value, nameof(SelectedScheme)))
{
UpdateGameResourceAsync(value).SafeForget();
await UpdateGameAccountsViewAsync().ConfigureAwait(false);
// Clear the selected game account to prevent setting
// incorrect CN/OS account when scheme not match
SelectedGameAccount = default;
}
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)
{
if (scheme is null)
{
return;
}
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
}
}
async ValueTask UpdateGameAccountsViewAsync()
{
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
}
}
private void SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions()
{
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SelectedGamePathEntry = entry;
}
}

View File

@@ -1,9 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification;
using System.Collections.ObjectModel;
using Windows.Win32.Foundation;
@@ -17,17 +20,17 @@ namespace Snap.Hutao.ViewModel.Game;
[ConstructorGenerated(CallBaseConstructor = true)]
internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>
{
private readonly LaunchStatusOptions launchStatusOptions;
private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService;
private readonly IGameServiceFacade gameService;
private readonly ITaskContext taskContext;
private readonly IInfoBarService infoBarService;
private ObservableCollection<GameAccount>? gameAccounts;
private AdvancedCollectionView? gameAccountsView;
private GameAccount? selectedGameAccount;
private GameAccountFilter? gameAccountFilter;
/// <summary>
/// 游戏账号集合
/// </summary>
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); }
/// <summary>
/// 选中的账号
@@ -37,19 +40,29 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
/// <inheritdoc/>
protected override async Task OpenUIAsync()
{
LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService);
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
try
{
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
if (scheme is not null)
{
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(scheme);
}
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
gameAccountFilter = new(scheme?.GetSchemeType());
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
}
[Command("LaunchCommand")]
@@ -68,7 +81,8 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
}
}
await gameService.LaunchAsync(new Progress<LaunchStatus>()).ConfigureAwait(false);
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
await gameService.LaunchAsync(launchProgress).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -15,7 +15,6 @@ using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
@@ -47,7 +46,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private readonly HutaoInfrastructureClient hutaoInfrastructureClient;
private readonly HutaoPassportViewModel hutaoPassportViewModel;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly INavigationService navigationService;
private readonly IClipboardProvider clipboardInterop;
private readonly IShellLinkInterop shellLinkInterop;