Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
97a0b0b4d7 add sophon support 2024-07-15 23:08:45 +08:00
74 changed files with 722 additions and 862 deletions

View File

@@ -3,12 +3,26 @@
namespace Snap.Hutao.Model.Calculable;
/// <summary>
/// 可计算物品选项
/// </summary>
internal readonly struct CalculableOptions
{
/// <summary>
/// 角色
/// </summary>
public readonly ICalculableAvatar? Avatar;
/// <summary>
/// 武器
/// </summary>
public readonly ICalculableWeapon? Weapon;
/// <summary>
/// 构造一个新的可计算物品选项
/// </summary>
/// <param name="avatar">角色</param>
/// <param name="weapon">武器</param>
public CalculableOptions(ICalculableAvatar? avatar, ICalculableWeapon? weapon)
{
Avatar = avatar;

View File

@@ -13,7 +13,7 @@
<Identity
Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.10.7.0" />
Version="1.10.6.0" />
<Properties>
<DisplayName>Snap Hutao</DisplayName>

View File

@@ -13,7 +13,7 @@
<Identity
Name="60568DGPStudio.SnapHutaoDev"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.10.7.0" />
Version="1.10.6.0" />
<Properties>
<DisplayName>Snap Hutao Dev</DisplayName>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>Add or update to current Enhancement Progression Plan</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>Always create new adopted target items</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>Save Method</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>Overwrite existing adopted items</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>Keep existing target items</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>Daily Commission Availability Notification</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>Operation not fully completed, added/updated {0}, skipped {1}</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>The selected level does not require ingredients</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>No plan has been created and selected</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>There is already a foster project for this item</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>Successfully added to current plan</value>
</data>
@@ -1839,10 +1818,10 @@
<value>Export failed</value>
</data>
<data name="ViewModelUIGFExportSuccess" xml:space="preserve">
<value>Exported successfully</value>
<value>Export successfully</value>
</data>
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
<value>Imported UIGF files contain UID duplicated wish items</value>
<value>Imported UIGF files contain UID duplicated prayers</value>
</data>
<data name="ViewModelUIGFImportError" xml:space="preserve">
<value>Import failed</value>
@@ -2868,7 +2847,7 @@
<value>Import</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
<value>Display Undrawn Items in "Wish Export - Characters" and "Wish Export - Weapons"</value>
<value>在「祈愿记录-角色」与「祈愿记录-武器」中显示未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
<value>Undrawn Wish Items</value>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>添加或更新到当前养成计划</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>每日委托上线提醒</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>已成功添加至当前养成计划</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>Tambahkan atau perbarui ke Rencana Dev saat ini</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>Pemberitahuan Ketersediaan Komisi Harian</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>Operasi tidak sepenuhnya selesai, ditambahkan/diperbarui {0}, dilewati {1}</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>Berhasil ditambahkan ke rencana saat ini.</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>現在の育成計画に追加または更新する</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>アイテムは常に新しいドリルを作成</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>次で保存</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>既存の育成目的アイテム</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>対象のアイテムを予約する</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>まだデイリー依頼を行っていません</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作の一部に失敗しました:追加/更新:{0}、スキップ{1}</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>選択したLvでは育成材料は必要ありません</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>まだ作成されておらず、養成計画を選択してください</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>そのアイテムは既に適用されているアイテムがあります</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>選択中の育成計画に正常に追加されました</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>添加或更新到当前养成计划</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>일일 의뢰 가능 알림</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>현재 육성 계획 추가에 성공했습니다</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>Adicionar ou atualizar ao planejamento atual</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>Notificação diária de disponibilidade de missão</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>Operação não totalmente concluída, adicionado/atualizado {0}, pulado {1}</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>Adicionado com sucesso ao planejamento</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>添加或更新到当前养成计划</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>每日委托上线提醒</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加 / 更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>已成功添加至当前养成计划</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>添加或更新到当前养成计划</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>每日委托上线提醒</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>已成功添加至当前养成计划</value>
</data>

View File

@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>添加或更新到当前养成计划</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>每日委托上线提醒</value>
</data>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>已成功添加至当前养成计划</value>
</data>

View File

@@ -557,10 +557,10 @@
<value>必須登入 米游社/ HoYoLAB 並選擇一個用戶與角色</value>
</data>
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
<value>刪除了 UID{0} 的 {1} 筆祈願錄</value>
<value>刪除了 UID{0} 的 {1} 筆祈願錄</value>
</data>
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
<value>胡桃雲儲存的祈願錄存檔數已達當前帳號上限</value>
<value>胡桃雲儲存的祈願錄存檔數已達當前帳號上限</value>
</data>
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
<value>未開通祈願紀錄上傳服務或已到期</value>
@@ -572,7 +572,7 @@
<value>數據異常,無法儲存至雲端,請勿跨帳號上傳或嘗試删除雲端數據後重試</value>
</data>
<data name="ServerGachaLogServiceUploadEntrySucceed" xml:space="preserve">
<value>上傳了 UID{0} 的 {1} 筆祈願錄,儲存了 {2} 筆</value>
<value>上傳了 UID{0} 的 {1} 筆祈願錄,儲存了 {2} 筆</value>
</data>
<data name="ServerPassportLoginRequired" xml:space="preserve">
<value>請先登入或註冊胡桃帳號</value>
@@ -623,40 +623,40 @@
<value>驗證請求過快,請 1 分鐘後再試</value>
</data>
<data name="ServerRecordBannedUid" xml:space="preserve">
<value>上傳深淵錄失敗,當前 UID 已被胡桃數據庫封禁</value>
<value>上傳深淵錄失敗,當前 UID 已被胡桃數據庫封禁</value>
</data>
<data name="ServerRecordComputingStatistics" xml:space="preserve">
<value>上傳深淵錄失敗,正在計算統計數據</value>
<value>上傳深淵錄失敗,正在計算統計數據</value>
</data>
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
<value>獲取數據失敗,正在計算統計數據</value>
</data>
<data name="ServerRecordInternalException" xml:space="preserve">
<value>上傳深淵錄失敗,伺服器異常,請盡快聯繫開發者解決</value>
<value>上傳深淵錄失敗,伺服器異常,請盡快聯繫開發者解決</value>
</data>
<data name="ServerRecordInvalidData" xml:space="preserve">
<value>上傳深淵錄失敗,存在無效的數據</value>
<value>上傳深淵錄失敗,存在無效的數據</value>
</data>
<data name="ServerRecordInvalidUid" xml:space="preserve">
<value>無效的 UID</value>
</data>
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
<value>上傳深淵錄失敗,不是本期數據</value>
<value>上傳深淵錄失敗,不是本期數據</value>
</data>
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
<value>上傳深淵錄失敗,當前 UID 的紀錄仍在處理中,請勿重複操作</value>
<value>上傳深淵錄失敗,當前 UID 的紀錄仍在處理中,請勿重複操作</value>
</data>
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
<value>上傳深淵錄成功,獲贈祈願錄上傳服務時長</value>
<value>上傳深淵錄成功,獲贈祈願錄上傳服務時長</value>
</data>
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
<value>上傳深淵錄成功,但未登入胡桃通行證,無法獲贈祈願錄上傳服務時長</value>
<value>上傳深淵錄成功,但未登入胡桃通行證,無法獲贈祈願錄上傳服務時長</value>
</data>
<data name="ServerRecordUploadSuccessButNoSuchUser" xml:space="preserve">
<value>上傳深淵錄成功,但無法找到用戶,無法獲贈祈願錄上傳服務時長</value>
<value>上傳深淵錄成功,但無法找到用戶,無法獲贈祈願錄上傳服務時長</value>
</data>
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
<value>上傳深淵錄成功,但不是本期首次提交,無法獲贈祈願錄上傳服務時長</value>
<value>上傳深淵錄成功,但不是本期首次提交,無法獲贈祈願錄上傳服務時長</value>
</data>
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
<value>新增:{0} 個成就 | 更新:{1} 個成就 | 删除:{2} 個成就</value>
@@ -878,10 +878,10 @@
<value>由 {0} 啟動</value>
</data>
<data name="ServiceGachaLogArchiveCollectionUserdataCorruptedMessage" xml:space="preserve">
<value>無法獲取祈願錄:{0}</value>
<value>無法獲取祈願錄:{0}</value>
</data>
<data name="ServiceGachaLogEndIdUserdataCorruptedMessage" xml:space="preserve">
<value>無法獲取祈願錄 End Id</value>
<value>無法獲取祈願錄 End Id</value>
</data>
<data name="ServiceGachaLogFactoryAvatarWishName" xml:space="preserve">
<value>角色活動</value>
@@ -899,7 +899,7 @@
<value>獲取雲端祈願紀錄失敗</value>
</data>
<data name="ServiceGachaLogHutaoCloudServiceNotAllowed" xml:space="preserve">
<value>祈願錄上傳服務不可用</value>
<value>祈願錄上傳服務不可用</value>
</data>
<data name="ServiceGachaLogUIGFImportItemInvalidFormat" xml:space="preserve">
<value>數據包含異常物品, Id{0}</value>
@@ -921,7 +921,7 @@
<value>提供的 URL 無效</value>
</data>
<data name="ServiceGachaLogUrlProviderStokenUnsupported" xml:space="preserve">
<value>HoYoLAB 帳號不支持使用 SToken 重新整理祈願錄</value>
<value>HoYoLAB 帳號不支持使用 SToken 重新整理祈願錄</value>
</data>
<data name="ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale" xml:space="preserve">
<value>URL 中的語言:{0} 與胡桃的語言:{1} 不對應,請切換到對應語言重試</value>
@@ -1247,18 +1247,6 @@
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
<value>添加或更新到當前養成計劃</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
<value>总是创建新的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
<value>保存方式</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
<value>覆盖存在的养成目标物品</value>
</data>
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
<value>保留存在的养成目标物品</value>
</data>
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
<value>每日委託上線提醒</value>
</data>
@@ -1287,10 +1275,10 @@
<value>即時便箋 Webhook URL</value>
</data>
<data name="ViewDialogExportUIGFSubtitle" xml:space="preserve">
<value>選擇要匯出紀錄的 UID</value>
<value>选择要导出记录的 UID</value>
</data>
<data name="ViewDialogExportUIGFTitle" xml:space="preserve">
<value>出 UIGF 文件</value>
<value>出 UIGF 文件</value>
</data>
<data name="ViewDialogFeedbackEnableLoopbackContent" xml:space="preserve">
<value>解除限制後需使用其他工具恢復限制</value>
@@ -1299,10 +1287,10 @@
<value>是否解除 Loopback 限制</value>
</data>
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
<value>匯入祈願錄</value>
<value>匯入祈願錄</value>
</data>
<data name="ViewDialogGachaLogRefreshProgressAuthkeyTimeout" xml:space="preserve">
<value>祈願錄 URL 已失效,請重新獲取</value>
<value>祈願錄 URL 已失效,請重新獲取</value>
</data>
<data name="ViewDialogGachaLogRefreshProgressDescription" xml:space="preserve">
<value>正在獲取 {0}</value>
@@ -1314,7 +1302,7 @@
<value>請輸入 URL</value>
</data>
<data name="ViewDialogGachaLogUrlTitle" xml:space="preserve">
<value>手動輸入祈願錄 URL</value>
<value>手動輸入祈願錄 URL</value>
</data>
<data name="ViewDialogGeetestCustomUrlCompositInputHint" xml:space="preserve">
<value>請輸入請求接口的 URL 複合模板</value>
@@ -1380,10 +1368,10 @@
<value>UIGF 版本</value>
</data>
<data name="ViewDialogImportUIGFSubtitle" xml:space="preserve">
<value>選擇要匯入紀錄的 UID</value>
<value>选择要导入记录的 UID</value>
</data>
<data name="ViewDialogImportUIGFTitle" xml:space="preserve">
<value>入 UIGF 檔案</value>
<value>入 UIGF 文件</value>
</data>
<data name="ViewDialogLaunchGameAccountInputPlaceholder" xml:space="preserve">
<value>在此處輸入名稱</value>
@@ -1455,7 +1443,7 @@
<value>回饋中心</value>
</data>
<data name="ViewGachaLogHeader" xml:space="preserve">
<value>祈願錄</value>
<value>祈願錄</value>
</data>
<data name="ViewGuideStaticResourceDownloadSize" xml:space="preserve">
<value>預計下載大小:{0}</value>
@@ -1610,15 +1598,6 @@
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加/更新:{0} 個,跳過 {1} 個</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
<value>选定的等级不需要养成材料</value>
</data>
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
<value>尚未创建并选择养成计划</value>
</data>
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
<value>已存在该物品的养成项目</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>已成功新增至目前養成計劃</value>
</data>
@@ -1842,19 +1821,19 @@
<value>匯出成功</value>
</data>
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
<value>入的 UIGF 檔案中包含 UID 重的祈願紀錄項</value>
<value>入的 UIGF 文件中包含 UID 重的祈愿记录项</value>
</data>
<data name="ViewModelUIGFImportError" xml:space="preserve">
<value>匯入失敗</value>
</data>
<data name="ViewModelUIGFImportNoHk4eEntry" xml:space="preserve">
<value>入的 UIGF 檔案中不包含祈願數據</value>
<value>入的 UIGF 文件中不包含祈愿数据</value>
</data>
<data name="ViewModelUIGFImportNoSelectedEntry" xml:space="preserve">
<value>請選擇至少一 UID 以匯入數據</value>
<value>请选择至少一 UID 以导入数据</value>
</data>
<data name="ViewModelUIGFImportSuccess" xml:space="preserve">
<value>入成功</value>
<value>入成功</value>
</data>
<data name="ViewModelUserAdded" xml:space="preserve">
<value>用戶 [{0}] 新增成功</value>
@@ -2214,7 +2193,7 @@
<value>手動輸入 URL</value>
</data>
<data name="ViewPageGachaLogRefreshByManualInputDescription" xml:space="preserve">
<value>使用由你提供的 URL 重新整理祈願錄</value>
<value>使用由你提供的 URL 重新整理祈願錄</value>
</data>
<data name="ViewPageGachaLogRefreshBySToken" xml:space="preserve">
<value>SToken 重新整理</value>
@@ -2307,7 +2286,7 @@
<value>數據收集統計</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewRecordTotal" xml:space="preserve">
<value>上傳錄總數</value>
<value>上傳錄總數</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewRefreshTime" xml:space="preserve">
<value>數據重新整理時間</value>
@@ -2319,16 +2298,16 @@
<value>平均戰鬥次數</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssFullStar" xml:space="preserve">
<value>滿星深淵錄</value>
<value>滿星深淵錄</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssPassed" xml:space="preserve">
<value>通關深淵錄</value>
<value>通關深淵錄</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssStarAverage" xml:space="preserve">
<value>平均獲取淵星</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssTotal" xml:space="preserve">
<value>總計深淵錄</value>
<value>總計深淵錄</value>
</data>
<data name="ViewPageHutaoDatabaseOverviewTeamAppearance" xml:space="preserve">
<value>隊伍出場</value>
@@ -2631,7 +2610,7 @@
<value>刪除遊戲内網頁快取</value>
</data>
<data name="ViewPageSettingDeleteUserDescription" xml:space="preserve">
<value>直接刪除用戶表的所有錄,用於修復特定的賬號衝突問題</value>
<value>直接刪除用戶表的所有錄,用於修復特定的賬號衝突問題</value>
</data>
<data name="ViewPageSettingDeleteUserHeader" xml:space="preserve">
<value>刪除所有用戶</value>
@@ -2673,7 +2652,7 @@
<value>前往反饋</value>
</data>
<data name="ViewPageSettingGachaLogHeader" xml:space="preserve">
<value>祈願錄</value>
<value>祈願錄</value>
</data>
<data name="ViewPageSettingGameHeader" xml:space="preserve">
<value>遊戲</value>
@@ -2859,16 +2838,16 @@
<value>匯出</value>
</data>
<data name="ViewPageSettingUIGFExportImportDescription" xml:space="preserve">
<value>出/入 UIGF 4 檔案</value>
<value>出/入 UIGF 4 文件</value>
</data>
<data name="ViewPageSettingUIGFExportImportHeader" xml:space="preserve">
<value>數據遷移</value>
<value>数据迁移</value>
</data>
<data name="ViewPageSettingUIGFImportContent" xml:space="preserve">
<value>匯入</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
<value>在祈願紀錄 - 角色」與「祈願紀錄 - 武器」中顯示未抽到的祈願物品</value>
<value>在祈願記錄頁面角色與武器頁籤顯示未抽到的祈願物品</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
<value>未抽取到的祈願物品</value>

View File

@@ -8,10 +8,12 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Achievement;
using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement;
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
internal sealed partial class AchievementService : IAchievementService
@@ -29,7 +31,9 @@ internal sealed partial class AchievementService : IAchievementService
if (archives is null)
{
await taskContext.SwitchToBackgroundAsync();
archives = new(achievementDbService.GetAchievementArchiveCollection(), serviceProvider);
ObservableCollection<AchievementArchive> source = achievementDbService.GetAchievementArchiveCollection();
await taskContext.SwitchToMainThreadAsync();
archives = new(source, serviceProvider);
}
return archives;

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.UI.Xaml.Data;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -18,6 +17,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
internal sealed partial class SummaryFactory : ISummaryFactory
{
private readonly IMetadataService metadataService;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token)
@@ -37,9 +37,11 @@ internal sealed partial class SummaryFactory : ISummaryFactory
IList<AvatarView> views = [.. avatars];
await taskContext.SwitchToMainThreadAsync();
return new()
{
Avatars = views.ToAdvancedCollectionView(),
Avatars = new(views),
};
}
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Cultivation;
internal enum ConsumptionSaveResultKind
{
NoItem,
NoProject,
Skipped,
Added,
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Cultivation;
internal enum ConsumptionSaveStrategyKind
{
PreserveExisting,
OverwriteExisting,
CreateNewEntry,
}

View File

@@ -8,6 +8,7 @@ using Snap.Hutao.Service.Inventory;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.Cultivation;
using System.Collections.ObjectModel;
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
using ModelItem = Snap.Hutao.Model.Item;
namespace Snap.Hutao.Service.Cultivation;
@@ -114,54 +115,45 @@ internal sealed partial class CultivationService : ICultivationService
}
/// <inheritdoc/>
public async ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption)
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<CalculateItem> items, LevelInformation levelInformation)
{
if (inputConsumption.Items.Count == 0)
if (items.Count == 0)
{
return ConsumptionSaveResultKind.NoItem;
return true;
}
await taskContext.SwitchToMainThreadAsync();
if (Projects.CurrentItem is null)
{
await taskContext.SwitchToMainThreadAsync();
Projects.MoveCurrentTo(Projects.SourceCollection.SelectedOrDefault());
if (Projects.CurrentItem is null)
{
return ConsumptionSaveResultKind.NoProject;
return false;
}
}
await taskContext.SwitchToBackgroundAsync();
CultivateEntry? entry = default;
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting or ConsumptionSaveStrategyKind.OverwriteExisting)
{
entry = cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, inputConsumption.ItemId);
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting && entry is not null)
{
return ConsumptionSaveResultKind.Skipped;
}
}
CultivateEntry? entry = type is CultivateType.AvatarAndSkill
? cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, itemId)
: default;
if (entry is null)
{
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, inputConsumption.Type, inputConsumption.ItemId);
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, type, itemId);
cultivationDbService.AddCultivateEntry(entry);
}
Guid entryId = entry.InnerId;
cultivationDbService.RemoveLevelInformationByEntryId(entryId);
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, inputConsumption.Type, inputConsumption.LevelInformation);
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation);
cultivationDbService.AddLevelInformation(entryLevelInformation);
cultivationDbService.RemoveCultivateItemRangeByEntryId(entryId);
IEnumerable<CultivateItem> toAdd = inputConsumption.Items.Select(item => CultivateItem.From(entryId, item));
IEnumerable<CultivateItem> toAdd = items.Select(item => CultivateItem.From(entryId, item));
cultivationDbService.AddCultivateItemRange(toAdd);
return ConsumptionSaveResultKind.Added;
return true;
}
/// <inheritdoc/>

View File

@@ -3,13 +3,22 @@
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.ViewModel.Cultivation;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Cultivation;
/// <summary>
/// 养成计算服务
/// </summary>
[HighQuality]
internal interface ICultivationService
{
/// <summary>
/// 获取用于绑定的项目集合
/// </summary>
AdvancedDbCollectionView<CultivateProject> Projects { get; }
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
@@ -17,13 +26,32 @@ internal interface ICultivationService
ValueTask<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(
CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token);
/// <summary>
/// 删除养成清单
/// </summary>
/// <param name="entryId">入口Id</param>
/// <returns>任务</returns>
ValueTask RemoveCultivateEntryAsync(Guid entryId);
/// <summary>
/// 异步移除项目
/// </summary>
/// <param name="project">项目</param>
/// <returns>任务</returns>
ValueTask RemoveProjectAsync(CultivateProject project);
ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption);
ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items, LevelInformation levelInformation);
/// <summary>
/// 保存养成物品状态
/// </summary>
/// <param name="item">养成物品</param>
void SaveCultivateItem(CultivateItemView item);
/// <summary>
/// 异步尝试添加新的项目
/// </summary>
/// <param name="project">项目</param>
/// <returns>添加操作的结果</returns>
ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project);
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Primitive;
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
namespace Snap.Hutao.Service.Cultivation;
internal sealed class InputConsumption
{
public required CultivateType Type { get; init; }
public required uint ItemId { get; init; }
public required List<CalculateItem> Items { get; init; }
public required LevelInformation LevelInformation { get; init; }
public required ConsumptionSaveStrategyKind Strategy { get; init; }
}

View File

@@ -203,7 +203,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
AsyncBarrier barrier = new(4);
List<HistoryWish> historyWishes = historyWishBuilders
.Where(b => appOptions.IsEmptyHistoryWishVisible || !b.IsEmpty)
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
.OrderByDescending(builder => builder.From)
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
.Select(builder => builder.ToHistoryWish())
@@ -212,7 +212,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
return new()
{
// history
HistoryWishes = historyWishes.ToAdvancedCollectionView(),
HistoryWishes = taskContext.InvokeOnMainThread(() => new AdvancedCollectionView<HistoryWish>(historyWishes)),
// avatars
OrangeAvatars = orangeAvatarCounter.ToStatisticsList(),

View File

@@ -47,6 +47,7 @@ internal sealed partial class GachaLogService : IGachaLogService
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
context = await metadataService.GetContextAsync<GachaLogServiceMetadataContext>(token).ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
Archives = new(gachaLogDbService.GetGachaArchiveCollection(), serviceProvider);
return true;
}

View File

@@ -21,15 +21,9 @@ internal sealed partial class GameAccountService : IGameAccountService
private ObservableReorderableDbCollection<GameAccount>? gameAccounts;
public async ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync()
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
{
if (gameAccounts is null)
{
await taskContext.SwitchToBackgroundAsync();
gameAccounts = gameDbService.GetGameAccountCollection();
}
return gameAccounts;
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
}
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType)

View File

@@ -9,14 +9,14 @@ namespace Snap.Hutao.Service.Game.Account;
internal interface IGameAccountService
{
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
GameAccount? DetectCurrentGameAccount(SchemeType schemeType);
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync();
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);

View File

@@ -11,6 +11,10 @@ using Snap.Hutao.Service.Game.PathAbstraction;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameServiceFacade))]
internal sealed partial class GameServiceFacade : IGameServiceFacade
@@ -19,46 +23,55 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
private readonly IGameAccountService gameAccountService;
private readonly IGamePathService gamePathService;
public ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync()
/// <inheritdoc/>
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
{
return gameAccountService.GetGameAccountCollectionAsync();
get => gameAccountService.GameAccountCollection;
}
/// <inheritdoc/>
public ValueTask<ValueResult<bool, string>> GetGamePathAsync()
{
return gamePathService.SilentGetGamePathAsync();
}
/// <inheritdoc/>
public ChannelOptions GetChannelOptions()
{
return gameChannelOptionsService.GetChannelOptions();
}
/// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
{
return gameAccountService.DetectGameAccountAsync(scheme);
}
/// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
{
return gameAccountService.DetectCurrentGameAccount(scheme);
}
/// <inheritdoc/>
public ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid)
{
return gameAccountService.AttachGameAccountToUidAsync(gameAccount, uid);
}
/// <inheritdoc/>
public ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
return gameAccountService.ModifyGameAccountAsync(gameAccount);
}
/// <inheritdoc/>
public ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
return gameAccountService.RemoveGameAccountAsync(gameAccount);
}
/// <inheritdoc/>
public bool IsGameRunning()
{
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);

View File

@@ -10,6 +10,8 @@ namespace Snap.Hutao.Service.Game;
internal interface IGameServiceFacade
{
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
@@ -25,6 +27,4 @@ internal interface IGameServiceFacade
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync();
}

View File

@@ -1,6 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32.Foundation;
using static Snap.Hutao.Win32.Kernel32;
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
@@ -15,34 +19,19 @@ internal static class GameFpsAddress
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext context, in RequiredRemoteModule remoteModule, in RequiredLocalModule localModule)
{
Span<byte> executableSpan = localModule.Executable.AsSpan();
int offsetToExecutable = 0;
nuint localVirtualAddress = 0;
do
{
int index = IndexOfPattern(executableSpan[offsetToExecutable..], out int patternLength);
if (index < 0)
{
break;
}
int offsetToUserAssembly = IndexOfPattern(localModule.UserAssembly.AsSpan());
HutaoException.ThrowIfNot(offsetToUserAssembly >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
offsetToExecutable += index;
nuint rip = localModule.Executable.Address + (uint)offsetToExecutable;
nuint rip = localModule.UserAssembly.Address + (uint)offsetToUserAssembly;
rip += 5U;
rip += (nuint)(*(int*)(rip + 1U) + 5);
rip += (nuint)(*(int*)(rip + 2U) + 6);
if (*(byte*)rip is ASM_JMP)
{
localVirtualAddress = rip;
break;
}
nuint remoteVirtualAddress = remoteModule.UserAssembly.Address + (rip - localModule.UserAssembly.Address);
offsetToExecutable += patternLength;
}
while (true);
nuint ptr = 0;
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(context.AllAccess, remoteVirtualAddress, out ptr) && ptr != 0);
ArgumentOutOfRangeException.ThrowIfZero(localVirtualAddress);
nuint localVirtualAddress = ptr - remoteModule.UnityPlayer.Address + localModule.UnityPlayer.Address;
while (*(byte*)localVirtualAddress is ASM_CALL or ASM_JMP)
{
@@ -50,15 +39,22 @@ internal static class GameFpsAddress
}
localVirtualAddress += *(uint*)(localVirtualAddress + 2) + 6;
nuint relativeVirtualAddress = localVirtualAddress - localModule.Executable.Address;
context.FpsAddress = remoteModule.Executable.Address + relativeVirtualAddress;
nuint relativeVirtualAddress = localVirtualAddress - localModule.UnityPlayer.Address;
context.FpsAddress = remoteModule.UnityPlayer.Address + relativeVirtualAddress;
}
private static int IndexOfPattern(in ReadOnlySpan<byte> span, out int patternLength)
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
{
// B9 3C 00 00 00 E8
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xE8];
patternLength = part.Length;
return span.IndexOf(part);
// B9 3C 00 00 00 FF 15
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
return memory.IndexOf(part);
}
private static unsafe bool UnsafeReadProcessMemory(HANDLE hProcess, nuint baseAddress, out nuint value)
{
value = 0;
bool result = ReadProcessMemory(hProcess, (void*)baseAddress, ref value, out _);
HutaoException.ThrowIfNot(result, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
return result;
}
}

View File

@@ -51,9 +51,12 @@ internal abstract class GameFpsUnlocker : IGameFpsUnlocker
private static RequiredLocalModule LoadRequiredLocalModule(GameFileSystem gameFileSystem)
{
string gameFoler = gameFileSystem.GameDirectory;
string dataFoler = gameFileSystem.DataDirectory;
LOAD_LIBRARY_FLAGS flags = LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_IMAGE_RESOURCE;
HMODULE executaleAddress = LoadLibraryExW(gameFileSystem.GameFilePath, default, flags);
HMODULE unityPlayerAddress = LoadLibraryExW(System.IO.Path.Combine(gameFoler, "UnityPlayer.dll"), default, flags);
HMODULE userAssemblyAddress = LoadLibraryExW(System.IO.Path.Combine(dataFoler, "Native", "UserAssembly.dll"), default, flags);
return new(executaleAddress);
return new(unityPlayerAddress, userAssemblyAddress);
}
}

View File

@@ -42,15 +42,16 @@ internal static class GameProcessModule
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out RequiredRemoteModule info)
{
FindModuleResult result = UnsafeFindModule(hProcess, GameConstants.YuanShenFileName, GameConstants.GenshinImpactFileName, out Module executable);
FindModuleResult unityPlayerResult = UnsafeFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
FindModuleResult userAssemblyResult = UnsafeFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
if (result is FindModuleResult.Ok)
if (unityPlayerResult is FindModuleResult.Ok && userAssemblyResult is FindModuleResult.Ok)
{
info = new(executable);
info = new(unityPlayer, userAssembly);
return FindModuleResult.Ok;
}
if (result is FindModuleResult.NoModuleFound)
if (unityPlayerResult is FindModuleResult.NoModuleFound && userAssemblyResult is FindModuleResult.NoModuleFound)
{
info = default;
return FindModuleResult.NoModuleFound;
@@ -60,7 +61,7 @@ internal static class GameProcessModule
return FindModuleResult.ModuleNotLoaded;
}
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName1, in ReadOnlySpan<char> moduleName2, out Module module)
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, out Module module)
{
HMODULE[] buffer = new HMODULE[128];
if (!K32EnumProcessModules(hProcess, buffer, out uint actualSize))
@@ -85,8 +86,7 @@ internal static class GameProcessModule
fixed (char* lpBaseName = baseName)
{
ReadOnlySpan<char> baseNameSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
if (!(moduleName1.SequenceEqual(baseNameSpan) || moduleName2.SequenceEqual(baseNameSpan)))
if (!moduleName.SequenceEqual(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName)))
{
continue;
}

View File

@@ -12,24 +12,30 @@ namespace Snap.Hutao.Service.Game.Unlocker;
internal readonly struct RequiredLocalModule : IDisposable
{
public readonly bool HasValue = false;
public readonly Module Executable;
public readonly Module UnityPlayer;
public readonly Module UserAssembly;
private readonly HMODULE hModuleExecutable;
private readonly HMODULE hModuleUnityPlayer;
private readonly HMODULE hModuleUserAssembly;
public RequiredLocalModule(HMODULE executable)
public RequiredLocalModule(HMODULE unityPlayer, HMODULE userAssembly)
{
hModuleExecutable = executable;
hModuleUnityPlayer = unityPlayer;
hModuleUserAssembly = userAssembly;
// Align the pointer
nint executableMappedView = (nint)(executable & ~0x3L);
nint unityPlayerMappedView = (nint)(unityPlayer & ~0x3L);
nint userAssemblyMappedView = (nint)(userAssembly & ~0x3L);
Executable = new((nuint)executableMappedView, GetImageSize(executableMappedView));
HasValue = true;
UnityPlayer = new((nuint)unityPlayerMappedView, GetImageSize(unityPlayerMappedView));
UserAssembly = new((nuint)userAssemblyMappedView, GetImageSize(userAssemblyMappedView));
}
public void Dispose()
{
FreeLibrary(hModuleExecutable);
FreeLibrary(hModuleUnityPlayer);
FreeLibrary(hModuleUserAssembly);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -6,11 +6,13 @@ namespace Snap.Hutao.Service.Game.Unlocker;
internal readonly struct RequiredRemoteModule
{
public readonly bool HasValue = false;
public readonly Module Executable;
public readonly Module UnityPlayer;
public readonly Module UserAssembly;
public RequiredRemoteModule(in Module executable)
public RequiredRemoteModule(in Module unityPlayer, in Module userAssembly)
{
HasValue = true;
Executable = executable;
UnityPlayer = unityPlayer;
UserAssembly = userAssembly;
}
}

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Model.Entity.Extension;
using Snap.Hutao.UI.Xaml.Data;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Passport;
@@ -30,7 +29,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
await taskContext.SwitchToMainThreadAsync();
user.UserGameRoles = new([]);
user.UserGameRoles = [];
}
return user;
@@ -214,7 +213,8 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
if (userGameRolesResponse.IsOk())
{
user.UserGameRoles = userGameRolesResponse.Data.List.ToAdvancedCollectionView();
await taskContext.SwitchToMainThreadAsync();
user.UserGameRoles = new(userGameRolesResponse.Data.List);
return user.UserGameRoles.Count > 0;
}
else

View File

@@ -99,12 +99,8 @@ internal sealed class UniformStaggeredLayoutState
}
internal void Clear()
{
if (items.Count > 0)
{
RecycleElements();
}
ClearColumns();
ClearItems();
}

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.WinUI.Collections;
using CommunityToolkit.WinUI.Helpers;
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@@ -27,6 +28,11 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private int deferCounter;
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView()
: this([])
{
}
public AdvancedCollectionView(IList<T> source)
{
view = [];
@@ -622,13 +628,14 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
if (i < -1 || i >= view.Count)
{
// view is empty, i is 0, current pos is -1
Debugger.Break(); // Figure out how this will hit.
OnPropertyChanged(nameof(CurrentItem));
return false;
}
OnCurrentChanging(out bool cancel);
if (cancel)
CurrentChangingEventArgs e = new();
OnCurrentChanging(e);
if (e.Cancel)
{
return false;
}
@@ -638,17 +645,14 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return true;
}
private void OnCurrentChanging(out bool cancel)
private void OnCurrentChanging(CurrentChangingEventArgs e)
{
if (!created || deferCounter > 0)
{
cancel = false;
return;
}
CurrentChangingEventArgs e = new();
CurrentChanging?.Invoke(this, e);
cancel = e.Cancel;
}
private void OnCurrentChanged()

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.UI.Xaml.Data;
internal static class AdvancedCollectionViewExtension
@@ -15,11 +13,4 @@ internal static class AdvancedCollectionViewExtension
view.MoveCurrentTo(default);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AdvancedCollectionView<T> ToAdvancedCollectionView<T>(this IList<T> source)
where T : class, IAdvancedCollectionViewItem
{
return new AdvancedCollectionView<T>(source);
}
}

View File

@@ -60,7 +60,7 @@
</cwc:HeaderedContentControl>
</cwc:UniformGrid>
<RadioButtons
x:Name="ImportModeSelector"
Name="ImportModeSelector"
Grid.Row="1"
Margin="0,16,0,0"
Header="{shuxm:ResourceString Name=ViewDialogAchievementArchiveImportStrategy}"

View File

@@ -8,11 +8,20 @@ using Snap.Hutao.Service.Achievement;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
/// <summary>
/// 成就对话框
/// </summary>
[HighQuality]
[DependencyProperty("UIAF", typeof(UIAF))]
internal sealed partial class AchievementImportDialog : ContentDialog
{
private readonly ITaskContext taskContext;
/// <summary>
/// 构造一个新的成就对话框
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="uiaf">uiaf数据</param>
public AchievementImportDialog(IServiceProvider serviceProvider, UIAF uiaf)
{
InitializeComponent();
@@ -21,6 +30,10 @@ internal sealed partial class AchievementImportDialog : ContentDialog
UIAF = uiaf;
}
/// <summary>
/// 异步获取导入选项
/// </summary>
/// <returns>导入选项</returns>
public async ValueTask<ValueResult<bool, ImportStrategyKind>> GetImportStrategyAsync()
{
await taskContext.SwitchToMainThreadAsync();

View File

@@ -73,15 +73,5 @@
SpinButtonPlacementMode="Inline"
Value="{x:Bind PromotionDelta.Weapon.LevelTarget, Mode=TwoWay}"/>
</cwc:SettingsCard>
<RadioButtons
x:Name="SaveModeSelector"
Margin="0,13,0,0"
Header="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyHeader}"
SelectedIndex="0">
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyPreserveExisting}"/>
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
</RadioButtons>
</StackPanel>
</ContentDialog>

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
@@ -22,7 +21,7 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
PromotionDelta = AvatarPromotionDelta.CreateForBaseline();
}
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaBaselineAsync()
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaBaselineAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
@@ -51,6 +50,6 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
LocalSetting.Set(SettingKeys.CultivationWeapon90LevelTarget, weapon.LevelTarget);
}
return new(true, new(PromotionDelta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
return new(true, PromotionDelta);
}
}

View File

@@ -19,7 +19,7 @@
<DataTemplate x:Key="SkillTemplate">
<Border Margin="0,2,0,0" Style="{StaticResource BorderCardStyle}">
<Grid Margin="8" ColumnSpacing="6">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="160"/>
@@ -64,17 +64,18 @@
</Border>
</DataTemplate>
</ContentDialog.Resources>
<Grid Margin="0,8,0,0" RowSpacing="6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<StackPanel
Grid.Row="0"
Margin="0,8,0,0"
Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<Grid
Padding="8"
ColumnSpacing="6"
DataContext="{x:Bind Avatar}"
Style="{ThemeResource GridCardStyle}">
<Grid.ColumnDefinitions>
@@ -122,10 +123,12 @@
<ItemsControl ItemTemplate="{StaticResource SkillTemplate}" ItemsSource="{x:Bind Avatar.Skills}"/>
</StackPanel>
<StackPanel Grid.Row="1" Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<StackPanel
Grid.Row="1"
Margin="0,6,0,0"
Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<Grid
Padding="8"
ColumnSpacing="6"
DataContext="{x:Bind Weapon}"
Style="{ThemeResource GridCardStyle}">
<Grid.ColumnDefinitions>
@@ -171,15 +174,5 @@
</Grid>
</StackPanel>
<RadioButtons
x:Name="SaveModeSelector"
Grid.Row="2"
Margin="0,10,0,0"
Header="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyHeader}"
SelectedIndex="0">
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyPreserveExisting}"/>
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
</RadioButtons>
</Grid>
</ContentDialog>

View File

@@ -4,17 +4,25 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
/// <summary>
/// 养成计算对话框
/// </summary>
[HighQuality]
[DependencyProperty("Avatar", typeof(ICalculableAvatar))]
[DependencyProperty("Weapon", typeof(ICalculableWeapon))]
internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
{
private readonly ITaskContext taskContext;
/// <summary>
/// 构造一个新的养成计算对话框
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="options">选项</param>
public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options)
{
InitializeComponent();
@@ -25,7 +33,11 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
Weapon = options.Weapon;
}
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaAsync()
/// <summary>
/// 异步获取提升差异
/// </summary>
/// <returns>提升差异</returns>
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
@@ -54,6 +66,6 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
},
};
return new(true, new(delta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
return new(true, delta);
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
internal sealed class CultivatePromotionDeltaOptions
{
public CultivatePromotionDeltaOptions(AvatarPromotionDelta delta, ConsumptionSaveStrategyKind strategy)
{
Delta = delta;
Strategy = strategy;
}
public AvatarPromotionDelta Delta { get; set; }
public ConsumptionSaveStrategyKind Strategy { get; set; }
}

View File

@@ -107,16 +107,14 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
/// <inheritdoc/>
public async ValueTask<bool> ReceiveAsync(INavigationData data)
{
if (!await Initialization.Task.ConfigureAwait(false))
if (await Initialization.Task.ConfigureAwait(false))
{
return false;
}
if (data.Data is AppActivation.ImportUIAFFromClipboard)
{
await ImportUIAFFromClipboardAsync().ConfigureAwait(false);
return true;
}
}
return false;
}
@@ -128,7 +126,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
return false;
}
AdvancedCollectionView<AchievementGoalView> sortedGoals;
List<AchievementGoalView> sortedGoals;
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
@@ -136,13 +134,13 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
.GetAchievementGoalListAsync(CancellationToken)
.ConfigureAwait(false);
sortedGoals = goals.SortBy(goal => goal.Order).SelectList(AchievementGoalView.From).ToAdvancedCollectionView();
sortedGoals = goals.SortBy(goal => goal.Order).SelectList(AchievementGoalView.From);
}
IAdvancedDbCollectionView<EntityArchive> archives = await scopeContext.AchievementService.GetArchivesAsync(CancellationToken).ConfigureAwait(false);
await scopeContext.TaskContext.SwitchToMainThreadAsync();
AchievementGoals = sortedGoals;
AchievementGoals = new(sortedGoals);
Archives = archives;
Archives.MoveCurrentTo(Archives.SourceCollection.SelectedOrDefault());
return true;
@@ -208,7 +206,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("RemoveArchiveCommand")]
private async Task RemoveArchiveAsync()
{
if (Archives?.CurrentItem is not { } current)
if (Archives is null || !(Archives.CurrentItem is { } current))
{
return;
}
@@ -285,9 +283,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
private async ValueTask UpdateAchievementsAsync(EntityArchive? archive)
{
await scopeContext.TaskContext.SwitchToMainThreadAsync();
Achievements = default;
// TODO: immediately clear values
if (archive is null)
{
return;
@@ -297,29 +293,29 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
.GetContextAsync<AchievementServiceMetadataContext>(CancellationToken)
.ConfigureAwait(false);
if (!TryGetAchievements(archive, context, out AdvancedCollectionView<AchievementView>? combined))
if (!TryGetAchievements(archive, context, out List<AchievementView>? combined))
{
return;
}
await scopeContext.TaskContext.SwitchToMainThreadAsync();
Achievements = combined;
Achievements = new(combined);
AchievementFinishPercent.Update(this);
UpdateAchievementsFilterByGoal(AchievementGoals?.CurrentItem);
UpdateAchievementsSort();
}
private bool TryGetAchievements(EntityArchive archive, AchievementServiceMetadataContext context, [NotNullWhen(true)] out AdvancedCollectionView<AchievementView>? view)
private bool TryGetAchievements(EntityArchive archive, AchievementServiceMetadataContext context, [NotNullWhen(true)] out List<AchievementView>? combined)
{
try
{
view = scopeContext.AchievementService.GetAchievementViewList(archive, context).ToAdvancedCollectionView();
combined = scopeContext.AchievementService.GetAchievementViewList(archive, context);
return true;
}
catch (HutaoException ex)
{
scopeContext.InfoBarService.Error(ex);
view = default;
combined = default;
return false;
}
}
@@ -332,10 +328,6 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
return;
}
using (Achievements.DeferRefresh())
{
using (AchievementGoals.DeferRefresh())
{
Achievements.SortDescriptions.Clear();
AchievementGoals.SortDescriptions.Clear();
@@ -349,16 +341,11 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
Achievements.SortDescriptions.Add(achievementDefaultSortDescription);
AchievementGoals.SortDescriptions.Add(achievementGoalDefaultSortDescription);
}
}
}
private void UpdateAchievementsFilterByGoal(AchievementGoalView? goal)
{
if (Achievements is null)
if (Achievements is not null)
{
return;
}
if (goal is null)
{
Achievements.Filter = default!;
@@ -369,6 +356,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
Achievements.Filter = (AchievementView view) => view.Inner.Goal == goalId;
}
}
}
[Command("SearchAchievementCommand")]
private void UpdateAchievementsFilterBySearch(string? search)
@@ -408,12 +396,10 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("SaveAchievementCommand")]
private void SaveAchievement(AchievementView? achievement)
{
if (achievement is null)
if (achievement is not null)
{
return;
}
scopeContext.AchievementService.SaveAchievement(achievement);
AchievementFinishPercent.Update(this);
}
}
}

View File

@@ -3,7 +3,9 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.DataTransfer;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.AvatarInfo;
@@ -18,7 +20,9 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Response;
using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
using CalculatorClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
using CalculatorItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
namespace Snap.Hutao.ViewModel.AvatarProperty;
@@ -27,7 +31,14 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
[Injection(InjectAs.Scoped)]
internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient<UserAndUidChangedMessage>
{
private readonly AvatarPropertyViewModelScopeContext scopeContext;
private readonly IContentDialogFactory contentDialogFactory;
private readonly ICultivationService cultivationService;
private readonly IAvatarInfoService avatarInfoService;
private readonly IClipboardProvider clipboardProvider;
private readonly CalculatorClient calculatorClient;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private readonly IUserService userService;
private Summary? summary;
@@ -50,7 +61,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.None, CancellationToken).ConfigureAwait(false);
return true;
@@ -62,7 +73,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("RefreshFromEnkaApiCommand")]
private async Task RefreshByEnkaApiAsync()
{
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromEnkaAPI, CancellationToken).ConfigureAwait(false);
}
@@ -71,7 +82,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("RefreshFromHoyolabGameRecordCommand")]
private async Task RefreshByHoyolabGameRecordAsync()
{
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabGameRecord, CancellationToken).ConfigureAwait(false);
}
@@ -80,7 +91,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("RefreshFromHoyolabCalculateCommand")]
private async Task RefreshByHoyolabCalculateAsync()
{
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabCalculate, CancellationToken).ConfigureAwait(false);
}
@@ -90,19 +101,18 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
{
try
{
await scopeContext.TaskContext.SwitchToMainThreadAsync();
await taskContext.SwitchToMainThreadAsync();
IsInitialized = false;
ValueResult<RefreshResultKind, Summary?> summaryResult;
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
ContentDialog dialog = await scopeContext.ContentDialogFactory
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch)
.ConfigureAwait(false);
using (await dialog.BlockAsync(scopeContext.TaskContext).ConfigureAwait(false))
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
summaryResult = await scopeContext.AvatarInfoService
summaryResult = await avatarInfoService
.GetSummaryAsync(userAndUid, option, token)
.ConfigureAwait(false);
}
@@ -111,7 +121,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
(RefreshResultKind result, Summary? summary) = summaryResult;
if (result is RefreshResultKind.Ok)
{
await scopeContext.TaskContext.SwitchToMainThreadAsync();
await taskContext.SwitchToMainThreadAsync();
Summary = summary;
Summary?.Avatars.MoveCurrentToFirstOrDefault();
}
@@ -120,16 +130,16 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
switch (result)
{
case RefreshResultKind.APIUnavailable:
scopeContext.InfoBarService.Warning(SH.ViewModelAvatarPropertyEnkaApiUnavailable);
infoBarService.Warning(SH.ViewModelAvatarPropertyEnkaApiUnavailable);
break;
case RefreshResultKind.StatusCodeNotSucceed:
ArgumentNullException.ThrowIfNull(summary);
scopeContext.InfoBarService.Warning(summary.Message);
infoBarService.Warning(summary.Message);
break;
case RefreshResultKind.ShowcaseNotOpen:
scopeContext.InfoBarService.Warning(SH.ViewModelAvatarPropertyShowcaseNotOpen);
infoBarService.Warning(SH.ViewModelAvatarPropertyShowcaseNotOpen);
break;
}
}
@@ -139,7 +149,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
}
finally
{
await scopeContext.TaskContext.SwitchToMainThreadAsync();
await taskContext.SwitchToMainThreadAsync();
IsInitialized = true;
}
}
@@ -152,47 +162,41 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
return;
}
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
scopeContext.InfoBarService.Warning(SH.MustSelectUserAndUid);
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
if (avatar.Weapon is null)
{
scopeContext.InfoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull);
infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull);
return;
}
CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable());
CultivatePromotionDeltaDialog dialog = await scopeContext.ContentDialogFactory
.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
Response<CalculatorBatchConsumption> response;
using (IServiceScope scope = scopeContext.ServiceScopeFactory.CreateScope())
{
CalculateClient calculatorClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
response = await calculatorClient.BatchComputeAsync(userAndUid, deltaOptions.Delta).ConfigureAwait(false);
}
Response<CalculatorBatchConsumption> response = await calculatorClient.BatchComputeAsync(userAndUid, delta).ConfigureAwait(false);
if (!response.IsOk())
{
return;
}
if (!await SaveCultivationAsync(response.Data.Items.Single(), deltaOptions).ConfigureAwait(false))
if (!await SaveCultivationAsync(response.Data.Items.Single(), delta).ConfigureAwait(false))
{
scopeContext.InfoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
return;
}
scopeContext.InfoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
}
[Command("BatchCultivateCommand")]
@@ -203,35 +207,34 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
return;
}
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
scopeContext.InfoBarService.Warning(SH.MustSelectUserAndUid);
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
CultivatePromotionDeltaBatchDialog dialog = await scopeContext.ContentDialogFactory
.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>().ConfigureAwait(false);
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false);
CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>().ConfigureAwait(false);
(bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
ArgumentNullException.ThrowIfNull(deltaOptions.Delta.SkillList);
ArgumentNullException.ThrowIfNull(deltaOptions.Delta.Weapon);
ArgumentNullException.ThrowIfNull(baseline.SkillList);
ArgumentNullException.ThrowIfNull(baseline.Weapon);
ContentDialog progressDialog = await scopeContext.ContentDialogFactory
ContentDialog progressDialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle)
.ConfigureAwait(false);
BatchCultivateResult result = default;
using (await progressDialog.BlockAsync(scopeContext.TaskContext).ConfigureAwait(false))
using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false))
{
List<CalculatorAvatarPromotionDelta> deltas = [];
foreach (AvatarView avatar in avatars)
{
if (!deltaOptions.Delta.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
if (!baseline.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
{
++result.SkippedCount;
continue;
@@ -240,12 +243,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
deltas.Add(copy);
}
Response<CalculatorBatchConsumption> response;
using (IServiceScope scope = scopeContext.ServiceScopeFactory.CreateScope())
{
CalculateClient calculatorClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
response = await calculatorClient.BatchComputeAsync(userAndUid, deltas).ConfigureAwait(false);
}
Response<CalculatorBatchConsumption> response = await calculatorClient.BatchComputeAsync(userAndUid, deltas).ConfigureAwait(false);
if (!response.IsOk())
{
@@ -254,8 +252,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
foreach ((CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta) in response.Data.Items.Zip(deltas))
{
if (!await SaveCultivationAsync(consumption, new(delta, deltaOptions.Strategy)).ConfigureAwait(false))
if (!await SaveCultivationAsync(consumption, delta).ConfigureAwait(false))
{
result.Interrupted = true;
break;
}
@@ -263,70 +262,42 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
}
}
if (result.SkippedCount > 0)
if (result.Interrupted)
{
scopeContext.InfoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount));
infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount));
}
else
{
scopeContext.InfoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount));
infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount));
}
}
/// <returns><see langword="true"/> if we can continue saving consumptions, otherwise <see langword="false"/>.</returns>
private async ValueTask<bool> SaveCultivationAsync(CalculatorConsumption consumption, CultivatePromotionDeltaOptions options)
private async ValueTask<bool> SaveCultivationAsync(CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta)
{
LevelInformation levelInformation = LevelInformation.From(options.Delta);
LevelInformation levelInformation = LevelInformation.From(delta);
InputConsumption avatarInput = new()
{
Type = CultivateType.AvatarAndSkill,
ItemId = options.Delta.AvatarId,
Items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume),
LevelInformation = levelInformation,
Strategy = options.Strategy,
};
ConsumptionSaveResultKind avatarSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
switch (avatarSaveKind)
{
case ConsumptionSaveResultKind.NoProject:
scopeContext.InfoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
return false;
case ConsumptionSaveResultKind.Skipped:
scopeContext.InfoBarService.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint);
break;
case ConsumptionSaveResultKind.NoItem:
scopeContext.InfoBarService.Information(SH.ViewModelCultivationConsumptionSaveNoItemHint);
break;
case ConsumptionSaveResultKind.Added:
break;
}
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, delta.AvatarId, items, levelInformation)
.ConfigureAwait(false);
try
{
ArgumentNullException.ThrowIfNull(options.Delta.Weapon);
ArgumentNullException.ThrowIfNull(delta.Weapon);
InputConsumption weaponInput = new()
{
Type = CultivateType.Weapon,
ItemId = options.Delta.Weapon.Id,
Items = consumption.WeaponConsume.EmptyIfNull(),
LevelInformation = levelInformation,
Strategy = options.Strategy,
};
// Take a hot path if avatar is not saved.
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, delta.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation)
.ConfigureAwait(false);
ConsumptionSaveResultKind weaponSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
return weaponSaveKind is not ConsumptionSaveResultKind.NoProject;
return avatarAndWeaponSaved;
}
catch (HutaoException ex)
{
scopeContext.InfoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
return false;
return true;
}
[Command("ExportToTextCommand")]
@@ -337,13 +308,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
return;
}
if (scopeContext.ClipboardProvider.SetText(AvatarViewTextTemplating.GetTemplatedText(avatar)))
if (clipboardProvider.SetText(AvatarViewTextTemplating.GetTemplatedText(avatar)))
{
scopeContext.InfoBarService.Success(SH.ViewModelAvatatPropertyExportTextSuccess);
infoBarService.Success(SH.ViewModelAvatatPropertyExportTextSuccess);
}
else
{
scopeContext.InfoBarService.Warning(SH.ViewModelAvatatPropertyExportTextError);
infoBarService.Warning(SH.ViewModelAvatatPropertyExportTextError);
}
}
}

View File

@@ -1,41 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DataTransfer;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Service.AvatarInfo;
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
namespace Snap.Hutao.ViewModel.AvatarProperty;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class AvatarPropertyViewModelScopeContext
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ICultivationService cultivationService;
private readonly IAvatarInfoService avatarInfoService;
private readonly IClipboardProvider clipboardProvider;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private readonly IUserService userService;
public IContentDialogFactory ContentDialogFactory { get => contentDialogFactory; }
public IServiceScopeFactory ServiceScopeFactory { get => serviceScopeFactory; }
public ICultivationService CultivationService { get => cultivationService; }
public IAvatarInfoService AvatarInfoService { get => avatarInfoService; }
public IClipboardProvider ClipboardProvider { get => clipboardProvider; }
public IInfoBarService InfoBarService { get => infoBarService; }
public ITaskContext TaskContext { get => taskContext; }
public IUserService UserService { get => userService; }
}

View File

@@ -7,4 +7,5 @@ internal struct BatchCultivateResult
{
public int SucceedCount;
public int SkippedCount;
public bool Interrupted;
}

View File

@@ -325,11 +325,13 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
async ValueTask UpdateGameAccountsViewAsync()
{
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
ObservableReorderableDbCollection<GameAccount> accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false);
AdvancedCollectionView<GameAccount> accountsView = new(accounts) { Filter = gameAccountFilter.Filter };
ObservableReorderableDbCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = accountsView;
GameAccountsView = new(accounts)
{
Filter = gameAccountFilter.Filter,
};
}
}
}

View File

@@ -43,14 +43,13 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
protected override async Task LoadAsync()
{
LaunchScheme? scheme = launchGameShared.GetCurrentLaunchSchemeFromConfigFile();
ObservableCollection<GameAccount> accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false);
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
try
{
if (scheme is not null)
{
// Try set to the current account.
await taskContext.SwitchToMainThreadAsync();
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(scheme);
}
}
@@ -60,10 +59,12 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
}
gameAccountFilter = new(scheme?.GetSchemeType());
AdvancedCollectionView<GameAccount> accountsView = new(accounts) { Filter = gameAccountFilter.Filter };
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = accountsView;
GameAccountsView = new(accounts)
{
Filter = gameAccountFilter.Filter,
};
}
[Command("LaunchCommand")]

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.IO;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog;

View File

@@ -85,10 +85,8 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
.ConfigureAwait(false);
}
AdvancedCollectionView<SpiralAbyssView> spiralAbyssEntries = collection.ToAdvancedCollectionView();
await taskContext.SwitchToMainThreadAsync();
SpiralAbyssEntries = spiralAbyssEntries;
SpiralAbyssEntries = new(collection);
SpiralAbyssEntries.MoveCurrentTo(SpiralAbyssEntries.SourceCollection.FirstOrDefault(s => s.Engaged));
}
catch (OperationCanceledException)

View File

@@ -72,7 +72,6 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
{
if (LocalSetting.Get(SettingKeys.AlwaysIsFirstRunAfterUpdate, false) || XamlApplicationLifetime.IsFirstRunAfterUpdate)
{
XamlApplicationLifetime.IsFirstRunAfterUpdate = false;
new ShowWebView2WindowAction()
{
ContentProvider = new UpdateLogContentProvider(),

View File

@@ -24,6 +24,7 @@ using Snap.Hutao.Web.Response;
using System.Collections.Frozen;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
@@ -42,8 +43,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
private readonly IMetadataService metadataService;
private readonly ITaskContext taskContext;
private readonly IHutaoSpiralAbyssStatisticsCache hutaoCache;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IInfoBarService infoBarService;
private readonly CalculateClient calculateClient;
private readonly IUserService userService;
private AdvancedCollectionView<Avatar>? avatars;
@@ -108,12 +109,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
AdvancedCollectionView<Avatar> avatarsView = list.ToAdvancedCollectionView();
await taskContext.SwitchToMainThreadAsync();
Avatars = avatarsView;
// TODO: use CurrentItem
Avatars = new(list);
Selected = Avatars.View.ElementAtOrDefault(0);
}
@@ -172,19 +169,16 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
CalculableOptions options = new(avatar.ToCalculable(), null);
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
Response<CalculateBatchConsumption> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
CalculateClient calculateClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
response = await calculateClient.BatchComputeAsync(userAndUid, deltaOptions.Delta).ConfigureAwait(false);
}
Response<CalculateBatchConsumption> response = await calculateClient
.BatchComputeAsync(userAndUid, delta)
.ConfigureAwait(false);
if (!response.IsOk())
{
@@ -192,32 +186,20 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
}
CalculateBatchConsumption batchConsumption = response.Data;
LevelInformation levelInformation = LevelInformation.From(deltaOptions.Delta);
LevelInformation levelInformation = LevelInformation.From(delta);
try
{
InputConsumption input = new()
{
Type = CultivateType.AvatarAndSkill,
ItemId = avatar.Id,
Items = batchConsumption.OverallConsume,
LevelInformation = levelInformation,
Strategy = deltaOptions.Strategy,
};
bool saved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, batchConsumption.OverallConsume, levelInformation)
.ConfigureAwait(false);
switch (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false))
if (saved)
{
case ConsumptionSaveResultKind.NoProject:
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
break;
case ConsumptionSaveResultKind.Skipped:
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint);
break;
case ConsumptionSaveResultKind.NoItem:
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveNoItemHint);
break;
case ConsumptionSaveResultKind.Added:
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
break;
}
else
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
}
}
catch (HutaoException ex)

View File

@@ -68,12 +68,8 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
AdvancedCollectionView<Monster> monstersView = ordered.ToAdvancedCollectionView();
await taskContext.SwitchToMainThreadAsync();
Monsters = monstersView;
// TODO: use CurrentItem
Monsters = new(ordered);
Selected = Monsters.View.ElementAtOrDefault(0);
}

View File

@@ -24,6 +24,7 @@ using Snap.Hutao.Web.Response;
using System.Collections.Frozen;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
@@ -37,11 +38,11 @@ namespace Snap.Hutao.ViewModel.Wiki;
internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly CalculateClient calculateClient;
private readonly ICultivationService cultivationService;
private readonly ITaskContext taskContext;
private readonly IMetadataService metadataService;
private readonly IHutaoSpiralAbyssStatisticsCache hutaoCache;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IInfoBarService infoBarService;
private readonly IUserService userService;
@@ -107,13 +108,9 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
AdvancedCollectionView<Weapon> weaponsView = list.ToAdvancedCollectionView();
await taskContext.SwitchToMainThreadAsync();
Weapons = weaponsView;
// TODO: use CurrentItem
Weapons = new(list);
Selected = Weapons.View.ElementAtOrDefault(0);
}
@@ -167,19 +164,16 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
CalculableOptions options = new(null, weapon.ToCalculable());
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
Response<CalculateBatchConsumption> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
CalculateClient calculateClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
response = await calculateClient.BatchComputeAsync(userAndUid, deltaOptions.Delta).ConfigureAwait(false);
}
Response<CalculateBatchConsumption> response = await calculateClient
.BatchComputeAsync(userAndUid, delta)
.ConfigureAwait(false);
if (!response.IsOk())
{
@@ -187,32 +181,20 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
}
CalculateBatchConsumption batchConsumption = response.Data;
LevelInformation levelInformation = LevelInformation.From(deltaOptions.Delta);
LevelInformation levelInformation = LevelInformation.From(delta);
try
{
InputConsumption input = new()
{
Type = CultivateType.Weapon,
ItemId = weapon.Id,
Items = batchConsumption.OverallConsume,
LevelInformation = levelInformation,
Strategy = deltaOptions.Strategy,
};
bool saved = await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, batchConsumption.OverallConsume, levelInformation)
.ConfigureAwait(false);
switch (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false))
if (saved)
{
case ConsumptionSaveResultKind.NoProject:
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
break;
case ConsumptionSaveResultKind.Skipped:
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint);
break;
case ConsumptionSaveResultKind.NoItem:
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveNoItemHint);
break;
case ConsumptionSaveResultKind.Added:
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
break;
}
else
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
}
}
catch (HutaoException ex)

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
namespace Snap.Hutao.Web;
@@ -157,6 +158,15 @@ internal static class ApiEndpoints
public const string GameRecordRoleCombatPath = $"{ApiTakumiRecordApi}/role_combat";
#endregion
#region ApiTakumiDownloaderApi
public static string SophonChunkGetBuild(BranchWrapper branch)
{
return $"{ApiTakumiDownloaderApi}/sophon_chunk/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password}";
}
#endregion
#region ApiTakumiEventCalculate
#region V1
@@ -307,6 +317,11 @@ internal static class ApiEndpoints
return $"{HoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
}
public static string HoyoPlayConnectGameBranches(LaunchScheme scheme)
{
return $"{HoyoPlayApiConnectApi}/getGameBranches?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
}
#endregion
#region PassportApi | PassportApiV4
@@ -381,6 +396,8 @@ internal static class ApiEndpoints
private const string ApiTakumiCardApi = $"{ApiTakumiRecord}/game_record/app/card/api";
private const string ApiTakumiCardWApi = $"{ApiTakumiRecord}/game_record/app/card/wapi";
private const string ApiTakumiDownloaderApi = $"{ApiTakumi}/downloader";
private const string ApiTakumiEvent = $"{ApiTakumi}/event";
private const string ApiTakumiEventCalculate = $"{ApiTakumiEvent}/e20200928calculate";
private const string ApiTakumiEventLuna = $"{ApiTakumiEvent}/luna";

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
namespace Snap.Hutao.Web;
@@ -254,6 +255,11 @@ internal static class ApiOsEndpoints
/// </summary>
public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
public static string SophonChunkGetBuild(BranchWrapper branch)
{
return $"{SgPublicApi}/sophon_chunk/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password}";
}
#endregion
#region SgHk4eApi
@@ -316,6 +322,11 @@ internal static class ApiOsEndpoints
return $"{SgHoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
}
public static string HoyoPlayConnectGameBranches(LaunchScheme scheme)
{
return $"{SgHoyoPlayApiConnectApi}/getGameBranches?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
}
#endregion
#region WebApiOsAccountApi

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
internal sealed class BranchWrapper
{
[JsonPropertyName("package_id")]
public string PackageId { get; set; } = default!;
[JsonPropertyName("branch")]
public string Branch { get; set; } = default!;
[JsonPropertyName("password")]
public string Password { get; set; } = default!;
[JsonPropertyName("tag")]
public string Tag { 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.Branch;
internal sealed class GameBranch : GameIndexedObject
{
[JsonPropertyName("main")]
public BranchWrapper Main { get; set; } = default!;
[JsonPropertyName("pre_download")]
public BranchWrapper 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.Branch;
internal sealed class GameBranchesWrapper
{
[JsonPropertyName("game_branches")]
public List<GameBranch> GameBranches { get; set; } = default!;
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
@@ -71,4 +72,21 @@ internal sealed partial class HoyoPlayClient
return Response.Response.DefaultIfNull(resp);
}
public async ValueTask<Response<GameBranchesWrapper>> GetBranchesAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.IsOversea
? ApiOsEndpoints.HoyoPlayConnectGameBranches(scheme)
: ApiEndpoints.HoyoPlayConnectGameBranches(scheme);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(url)
.Get();
Response<GameBranchesWrapper>? resp = await builder
.SendAsync<Response<GameBranchesWrapper>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
internal interface ISophonClient
{
ValueTask<Response<SophonBuild>> GetBuildAsync(BranchWrapper branch, CancellationToken token = default);
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
internal sealed class Manifest
{
[JsonPropertyName("id")]
public string Id { get; set; } = default!;
[JsonPropertyName("checksum")]
public string Checksum { get; set; } = default!;
[JsonPropertyName("compressed_size")]
public string CompressedSize { get; set; } = default!;
[JsonPropertyName("uncompressed_size")]
public string UncompressedSize { get; set; } = default!;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
internal sealed class ManifestDownloadInfo
{
[JsonPropertyName("encryption")]
public uint Encryption { get; set; }
[JsonPropertyName("password")]
public string Password { get; set; } = default!;
[JsonPropertyName("compression")]
public uint Compression { get; set; }
[JsonPropertyName("url_prefix")]
public string UrlPrefix { get; set; } = default!;
[JsonPropertyName("url_suffix")]
public string UrlSuffix { get; set; } = default!;
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
internal sealed class ManifestStats
{
[JsonPropertyName("compressed_size")]
public string CompressedSize { get; set; } = default!;
[JsonPropertyName("uncompressed_size")]
public string UncompressedSize { get; set; } = default!;
[JsonPropertyName("file_count")]
public string FileCount { get; set; } = default!;
[JsonPropertyName("chunk_count")]
public string ChunkCount { get; set; } = default!;
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
internal sealed class SophonBuild
{
[JsonPropertyName("build_id")]
public string BuildId { get; set; } = default!;
[JsonPropertyName("tag")]
public string Tag { get; set; } = default!;
[JsonPropertyName("manifests")]
public List<SophonManifest> Manifests { get; set; } = default!;
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
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.Takumi.Downloader;
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class SophonClient : ISophonClient
{
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly ILogger<SophonClient> logger;
private readonly HttpClient httpClient;
public async ValueTask<Response<SophonBuild>> GetBuildAsync(BranchWrapper branch, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.SophonChunkGetBuild(branch))
.Get();
Response<SophonBuild>? resp = await builder
.SendAsync<Response<SophonBuild>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
[Injection(InjectAs.Transient, typeof(IOverseaSupportFactory<ISophonClient>))]
[ConstructorGenerated(CallBaseConstructor = true)]
internal sealed partial class SophonClientFactory : OverseaSupportFactory<ISophonClient, SophonClient, SophonClientOversea>
{
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
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.Takumi.Downloader;
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class SophonClientOversea : ISophonClient
{
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly ILogger<SophonClientOversea> logger;
private readonly HttpClient httpClient;
public async ValueTask<Response<SophonBuild>> GetBuildAsync(BranchWrapper branch, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiOsEndpoints.SophonChunkGetBuild(branch))
.Get();
Response<SophonBuild>? resp = await builder
.SendAsync<Response<SophonBuild>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
internal sealed class SophonManifest
{
[JsonPropertyName("category_id")]
public string CategoryId { get; set; } = default!;
[JsonPropertyName("category_name")]
public string CategoryName { get; set; } = default!;
[JsonPropertyName("manifest")]
public Manifest Manifest { get; set; } = default!;
[JsonPropertyName("chunk_download")]
public ManifestDownloadInfo ChunkDownload { get; set; } = default!;
[JsonPropertyName("manifest_download")]
public ManifestDownloadInfo ManifestDownload { get; set; } = default!;
[JsonPropertyName("matching_field")]
public string MatchingField { get; set; } = default!;
[JsonPropertyName("stats")]
public ManifestStats Stats { get; set; } = default!;
[JsonPropertyName("deduplicated_stats")]
public ManifestStats DeduplicatedStats { get; set; } = default!;
}

View File

@@ -228,7 +228,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient
.ConfigureAwait(false);
// We have a verification procedure to handle
if (resp?.ReturnCode is (int)KnownReturnCode.CODE1034)
if (resp?.ReturnCode == (int)KnownReturnCode.CODE1034)
{
// Replace message
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;

View File

@@ -8,7 +8,7 @@ using System.Net.Http.Headers;
namespace Snap.Hutao.Web.Request.Builder;
internal sealed class HttpRequestMessageBuilder :
internal class HttpRequestMessageBuilder :
IBuilder,
IHttpRequestMessageBuilder,
IHttpHeadersBuilder<HttpHeaders>,
@@ -22,14 +22,12 @@ internal sealed class HttpRequestMessageBuilder :
IHttpMethodBuilder
{
private readonly HttpContentSerializer httpContentSerializer;
private readonly IServiceProvider serviceProvider;
private HttpRequestMessage httpRequestMessage;
public HttpRequestMessageBuilder(IServiceProvider serviceProvider, HttpContentSerializer httpContentSerializer, HttpRequestMessage? httpRequestMessage = default)
public HttpRequestMessageBuilder(HttpContentSerializer httpContentSerializer, HttpRequestMessage? httpRequestMessage = default)
{
this.serviceProvider = serviceProvider;
this.httpContentSerializer = httpContentSerializer;
this.httpRequestMessage = httpRequestMessage ?? new();
this.httpRequestMessage = httpRequestMessage ?? new HttpRequestMessage();
}
public HttpRequestMessage HttpRequestMessage
@@ -42,8 +40,6 @@ internal sealed class HttpRequestMessageBuilder :
}
}
public IServiceProvider ServiceProvider { get => serviceProvider; }
public HttpContentSerializer HttpContentSerializer { get => httpContentSerializer; }
HttpContentHeaders IHttpHeadersBuilder<HttpContentHeaders>.Headers

View File

@@ -1,14 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Web.Hutao.Response;
using Snap.Hutao.Web.Response;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
namespace Snap.Hutao.Web.Request.Builder;
@@ -25,10 +22,6 @@ internal static class HttpRequestMessageBuilderExtension
internal static async ValueTask<TResult?> SendAsync<TResult>(this HttpRequestMessageBuilder builder, HttpClient httpClient, ILogger logger, CancellationToken token)
where TResult : class
{
StringBuilder messageBuilder = new();
messageBuilder.AppendLine(System.Globalization.CultureInfo.CurrentCulture, $"Host: {builder.RequestUri?.Host}");
bool showInfo = true;
try
{
using (builder.HttpRequestMessage)
@@ -36,51 +29,52 @@ internal static class HttpRequestMessageBuilderExtension
using (HttpResponseMessage message = await httpClient.SendAsync(builder.HttpRequestMessage, token).ConfigureAwait(false))
{
message.EnsureSuccessStatusCode();
showInfo = false;
return await builder.HttpContentSerializer.DeserializeAsync<TResult>(message.Content, token).ConfigureAwait(false);
}
}
}
catch (HttpRequestException ex)
{
if (TryHandleHttp502HutaoResponseSpecialCase(ex, out TResult? result))
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
if (ex.StatusCode is HttpStatusCode.BadGateway)
{
return result;
Type resultType = typeof(TResult);
if (resultType == typeof(HutaoResponse))
{
return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default) as TResult;
}
ProcessException(messageBuilder, ex);
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
}
catch (IOException ex)
if (resultType.IsConstructedGenericType && resultType.GetGenericTypeDefinition() == typeof(HutaoResponse<>))
{
ProcessException(messageBuilder, ex);
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
}
catch (JsonException ex)
{
ProcessException(messageBuilder, ex);
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
}
catch (HttpContentSerializationException ex)
{
ProcessException(messageBuilder, ex);
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
}
catch (SocketException ex)
{
ProcessException(messageBuilder, ex);
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
}
finally
{
if (showInfo)
{
builder.ServiceProvider.GetRequiredService<IInfoBarService>().Error(messageBuilder.ToString());
return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default, default) as TResult;
}
}
return default;
}
catch (IOException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
return default;
}
catch (JsonException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
return default;
}
catch (HttpContentSerializationException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
return default;
}
catch (SocketException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
return default;
}
}
internal static void Send(this HttpRequestMessageBuilder builder, HttpClient httpClient, ILogger logger)
{
@@ -92,95 +86,23 @@ internal static class HttpRequestMessageBuilderExtension
}
catch (HttpRequestException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
}
catch (IOException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
}
catch (JsonException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
}
catch (HttpContentSerializationException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
}
catch (SocketException ex)
{
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
}
}
private static bool TryHandleHttp502HutaoResponseSpecialCase<TResult>(HttpRequestException ex, out TResult? result)
where TResult : class
{
result = default;
if (ex.StatusCode is HttpStatusCode.BadGateway)
{
Type resultType = typeof(TResult);
if (resultType == typeof(HutaoResponse))
{
// HutaoResponse(int returnCode, string message, string? localizationKey)
result = Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default) as TResult;
return true;
}
if (resultType.IsConstructedGenericType && resultType.GetGenericTypeDefinition() == typeof(HutaoResponse<>))
{
// HutaoResponse<TData>(int returnCode, string message, TData? data, string? localizationKey)
result = Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default, default) as TResult;
return true;
}
}
return false;
}
[SuppressMessage("", "CA1305")]
private static void ProcessException(StringBuilder builder, Exception exception)
{
if (exception is HttpRequestException hre)
{
builder
.AppendLine($"{nameof(HttpRequestException)}: Status Code: {hre.StatusCode} Error: {hre.HttpRequestError}")
.AppendLine(hre.Message);
}
if (exception is IOException ioe)
{
builder
.AppendLine($"{nameof(IOException)}: 0x{ioe.HResult:X8}")
.AppendLine(ioe.Message);
}
if (exception is JsonException je)
{
builder
.AppendLine($"{nameof(JsonException)}: Path: {je.Path} at Line: {je.LineNumber} Position: {je.BytePositionInLine}")
.AppendLine(je.Message);
}
if (exception is HttpContentSerializationException hcse)
{
builder
.AppendLine($"{nameof(HttpContentSerializationException)}:")
.AppendLine(hcse.Message);
}
if (exception is SocketException se)
{
builder
.AppendLine($"{nameof(SocketException)}: Error: {se.SocketErrorCode}")
.AppendLine(se.Message);
}
if (exception.InnerException is { } inner)
{
builder.AppendLine(new string('-', 40));
ProcessException(builder, inner);
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
}
}
}

View File

@@ -10,10 +10,9 @@ namespace Snap.Hutao.Web.Request.Builder;
internal sealed partial class HttpRequestMessageBuilderFactory : IHttpRequestMessageBuilderFactory
{
private readonly JsonHttpContentSerializer jsonHttpContentSerializer;
private readonly IServiceProvider serviceProvider;
public HttpRequestMessageBuilder Create()
{
return new(serviceProvider, jsonHttpContentSerializer);
return new(jsonHttpContentSerializer);
}
}