mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-06-18 00:34:50 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9da2c2eb8b | ||
|
|
ed3f2270b3 | ||
|
|
0eb8b58711 | ||
|
|
0777180df5 | ||
|
|
f29ddd62a7 | ||
|
|
8588f7736a | ||
|
|
f66d712a1b | ||
|
|
a02d7e533f | ||
|
|
57461a06dd | ||
|
|
82f1820ca9 | ||
|
|
9ea2bb1a6e | ||
|
|
7a3f125846 | ||
|
|
94592c8498 | ||
|
|
ed4626a74c | ||
|
|
c6a2212caa | ||
|
|
18afbffbcc | ||
|
|
64628b50b5 | ||
|
|
11a5efa488 | ||
|
|
726c4203d2 | ||
|
|
ace17fcc7b | ||
|
|
8f5532819a | ||
|
|
70c0970e2e | ||
|
|
a988a65190 | ||
|
|
dead0166a1 | ||
|
|
f19dbf78a0 | ||
|
|
a41b7e4a01 | ||
|
|
8bb177d08f | ||
|
|
1e600db869 | ||
|
|
be9f3f6a25 | ||
|
|
d3865b7992 | ||
|
|
66263a82f2 | ||
|
|
a2ddeadd0a | ||
|
|
246109227d | ||
|
|
515fa004bb | ||
|
|
7c706f8dc7 | ||
|
|
9de07754c7 | ||
|
|
8e86ccc560 | ||
|
|
e1df07ac21 | ||
|
|
1dabff07a1 | ||
|
|
640686a837 |
2
.github/workflows/PublishDistribution.yml
vendored
2
.github/workflows/PublishDistribution.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
|||||||
-Body $r2Data `
|
-Body $r2Data `
|
||||||
-ContentType "application/json"
|
-ContentType "application/json"
|
||||||
Write-Output $response2.Content
|
Write-Output $response2.Content
|
||||||
- uses: benc-uk/workflow-dispatch@v1.2.4
|
- uses: benc-uk/workflow-dispatch@v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: Build
|
workflow: Build
|
||||||
repo: DGP-Studio/hutao-installer
|
repo: DGP-Studio/hutao-installer
|
||||||
|
|||||||
2
.github/workflows/alpha.yml
vendored
2
.github/workflows/alpha.yml
vendored
@@ -113,7 +113,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload signed msix
|
- name: Upload signed msix
|
||||||
if: success() && github.event_name != 'pull_request'
|
if: success() && github.event_name != 'pull_request'
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
||||||
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||||
|
|||||||
2
.github/workflows/canary.yml
vendored
2
.github/workflows/canary.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload signed msix
|
- name: Upload signed msix
|
||||||
if: ${{ success() && steps.merge.outputs.continue == 'true' }}
|
if: ${{ success() && steps.merge.outputs.continue == 'true' }}
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Snap.Hutao.Canary-${{ steps.cake.outputs.version }}
|
name: Snap.Hutao.Canary-${{ steps.cake.outputs.version }}
|
||||||
path: ${{ github.workspace }}/src/output/Snap.Hutao.Canary-${{ steps.cake.outputs.version }}.msix
|
path: ${{ github.workspace }}/src/output/Snap.Hutao.Canary-${{ steps.cake.outputs.version }}.msix
|
||||||
|
|||||||
2
.github/workflows/msi-build.yml
vendored
2
.github/workflows/msi-build.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release
|
run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release
|
||||||
|
|
||||||
- name: Upload MSI Artifact
|
- name: Upload MSI Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Snap.Hutao-MSI
|
name: Snap.Hutao-MSI
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
自带的注入功能只有FPS调整,只保证FPS调整长期可用,你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
|
自带的注入功能只有FPS调整,只保证FPS调整长期可用,你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
|
||||||
|
|
||||||
官网:https://htserver.wdg.cloudns.ch/
|
官网:https://htserver.wdg12.work/
|
||||||
|
|
||||||
**该版本的特点:**
|
**该版本的特点:**
|
||||||
- 尽量保留原版功能,少重写功能,稳定性强
|
- 尽量保留原版功能,少重写功能,稳定性强
|
||||||
@@ -42,7 +42,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
|||||||
|
|
||||||
**目前元数据的编写进度:**
|
**目前元数据的编写进度:**
|
||||||
|
|
||||||
| 项目(V6.3) | 是否完成 |
|
| 项目(V6.4) | 是否完成 |
|
||||||
| ----------- | ----------- |
|
| ----------- | ----------- |
|
||||||
| 总体数据 | ✔️ |
|
| 总体数据 | ✔️ |
|
||||||
|
|
||||||
@@ -67,6 +67,14 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
|
|||||||
- 服务端:[Snap.Server](https://github.com/wangdage12/Snap.Server)
|
- 服务端:[Snap.Server](https://github.com/wangdage12/Snap.Server)
|
||||||
- Web管理后台和官网:[Snap.Server.Web](https://github.com/wangdage12/Snap.Server.Web)
|
- Web管理后台和官网:[Snap.Server.Web](https://github.com/wangdage12/Snap.Server.Web)
|
||||||
|
|
||||||
|
**第三方工具**
|
||||||
|
|
||||||
|
如果你想要添加你自己开发的工具到第三方工具列表中,请确保:
|
||||||
|
1. 工具应该提供源码或者开源,并且可以成功编译
|
||||||
|
2. 工具不应提供任何可能影响游戏公平性的功能
|
||||||
|
|
||||||
|
工具不限于注入功能,若满足以上条件,请提 issue,或者在 discord 服务器中联系管理员
|
||||||
|
|
||||||
## 打包测试
|
## 打包测试
|
||||||
|
|
||||||
由于采用了 wix 进行打包程序,VS 需要安装 **HeatWave for VS2022**(2026兼容)。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录:Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
|
由于采用了 wix 进行打包程序,VS 需要安装 **HeatWave for VS2022**(2026兼容)。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录:Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
|
||||||
@@ -85,9 +93,7 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
|
|||||||
|
|
||||||
我们将使用[UptimeRobot](https://uptimerobot.com)赞助的监控服务作为新的服务器状态页面,它有更多的功能
|
我们将使用[UptimeRobot](https://uptimerobot.com)赞助的监控服务作为新的服务器状态页面,它有更多的功能
|
||||||
|
|
||||||
[新服务器状态页面](https://stats.uptimerobot.com/fHxWxdxK61)
|
[服务器状态页面](https://stats.uptimerobot.com/fHxWxdxK61)
|
||||||
|
|
||||||
[旧服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -95,7 +101,6 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
|
|||||||
https://github.com/wangdage12/Snap.Metadata
|
https://github.com/wangdage12/Snap.Metadata
|
||||||
|
|
||||||
仓库镜像:
|
仓库镜像:
|
||||||

|
|
||||||
|
|
||||||
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
||||||
|
|
||||||
@@ -105,10 +110,16 @@ http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
https://htserver.wdg.cloudns.ch/api/
|
https://htserver.wdg12.work/api/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**图片资源站:**
|
**图片资源站:**
|
||||||
|
|
||||||
https://htserver.wdg.cloudns.ch/
|
https://htserver.wdg12.work/
|
||||||
|
|
||||||
|
# 赞助
|
||||||
|
|
||||||
|
如果你想要为我分摊经济压力,可以在下方链接中为我赞助(支持多个预设方案,你也可以在页面下方自定义金额)
|
||||||
|
赞助的资金将全部用于服务器、域名等,若有剩余资金将升级CDN或者服务器来提升使用体验,我们的服务是完全免费的,该赞助并不会解锁额外特权,但是>=10元时将在官网新的“赞助者页面”上添加你的信息
|
||||||
|
https://ifdian.net/a/wdg12
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<Package
|
<Package
|
||||||
Name="Snap.Hutao"
|
Name="Snap.Hutao"
|
||||||
Manufacturer="Millennium Science Technology R-D Inst"
|
Manufacturer="Millennium Science Technology R-D Inst"
|
||||||
Version="1.18.4.0"
|
Version="1.18.7.0"
|
||||||
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
||||||
Language="2052"
|
Language="2052"
|
||||||
Scope="perMachine">
|
Scope="perMachine">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="WixToolset.Sdk/6.0.2">
|
<Project Sdk="WixToolset.Sdk/6.0.2">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SuppressIces>ICE03;ICE60</SuppressIces>
|
<SuppressIces>ICE03;ICE60</SuppressIces>
|
||||||
<Platform>x64</Platform>
|
<Platform>x64</Platform>
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Snap.Hutao\Snap.Hutao.csproj" />
|
<ProjectReference Include="..\Snap.Hutao\Snap.Hutao.csproj" SetPlatform="Platform=x64" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<HarvestDirectory Include="..\Snap.Hutao\bin\Release\net10.0-windows10.0.26100.0\win-x64">
|
<HarvestDirectory Include="..\Snap.Hutao\bin\x64\Release\net10.0-windows10.0.26100.0\win-x64">
|
||||||
<ComponentGroupName>MainAppComponents</ComponentGroupName>
|
<ComponentGroupName>MainAppComponents</ComponentGroupName>
|
||||||
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
|
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
|
||||||
<SuppressCom>true</SuppressCom>
|
<SuppressCom>true</SuppressCom>
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ public static partial class Bootstrap
|
|||||||
private static readonly ApplicationInitializationCallback AppInitializationCallback = InitializeApp;
|
private static readonly ApplicationInitializationCallback AppInitializationCallback = InitializeApp;
|
||||||
private static Mutex? mutex;
|
private static Mutex? mutex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether console output is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ConsoleEnabled { get; private set; }
|
||||||
|
|
||||||
internal static void UseNamedPipeRedirection()
|
internal static void UseNamedPipeRedirection()
|
||||||
{
|
{
|
||||||
Debug.Assert(mutex is not null);
|
Debug.Assert(mutex is not null);
|
||||||
@@ -31,6 +36,19 @@ public static partial class Bootstrap
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// Check for console flag
|
||||||
|
ConsoleEnabled = args.Contains("--console") || args.Contains("--debug") ||
|
||||||
|
Environment.GetEnvironmentVariable("HUTAO_DEBUG") == "1";
|
||||||
|
|
||||||
|
if (ConsoleEnabled)
|
||||||
|
{
|
||||||
|
// Allocate a console window for debug output
|
||||||
|
HutaoNativeMethods.AllocConsole();
|
||||||
|
Console.WriteLine("[Bootstrap] Console allocated for debug output");
|
||||||
|
Console.WriteLine($"[Bootstrap] Arguments: {string.Join(" ", args)}");
|
||||||
|
Console.WriteLine($"[Bootstrap] Working Directory: {Environment.CurrentDirectory}");
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
|
||||||
#endif
|
#endif
|
||||||
@@ -124,6 +142,12 @@ public static partial class Bootstrap
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (ConsoleEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bootstrap] Application exiting...");
|
||||||
|
HutaoNativeMethods.FreeConsole();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ internal static class DependencyInjection
|
|||||||
.AddFilter(DbLoggerCategory.Query.Name, level => level >= LogLevel.Information)
|
.AddFilter(DbLoggerCategory.Query.Name, level => level >= LogLevel.Information)
|
||||||
.AddDebug()
|
.AddDebug()
|
||||||
.AddSentryTelemetry();
|
.AddSentryTelemetry();
|
||||||
|
|
||||||
|
// Add console logging if console is enabled
|
||||||
|
if (Bootstrap.ConsoleEnabled)
|
||||||
|
{
|
||||||
|
builder.AddConsole();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.AddMemoryCache()
|
.AddMemoryCache()
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ internal static class SettingKeys
|
|||||||
public const string CultivationWeapon90LevelCurrent = "Snap::Hutao::Cultivation::Weapon90::Level::Current";
|
public const string CultivationWeapon90LevelCurrent = "Snap::Hutao::Cultivation::Weapon90::Level::Current";
|
||||||
public const string CultivationWeapon90LevelTarget = "Snap::Hutao::Cultivation::Weapon90::Level::Target";
|
public const string CultivationWeapon90LevelTarget = "Snap::Hutao::Cultivation::Weapon90::Level::Target";
|
||||||
public const string ResinStatisticsSelectedDropDistribution = "Snap::Hutao::Cultivation::ResinStatistics::DropDistribution";
|
public const string ResinStatisticsSelectedDropDistribution = "Snap::Hutao::Cultivation::ResinStatistics::DropDistribution";
|
||||||
|
public const string CultivationStatisticsMergeUpgradeMaterials = "Snap::Hutao::Cultivation::Statistics::MergeUpgradeMaterials";
|
||||||
|
public const string CultivationStatisticsTalentSynthCritTenPercent = "Snap::Hutao::Cultivation::Statistics::TalentSynthCritTenPercent";
|
||||||
|
public const string CultivationStatisticsWeeklyBossMaterialInterchange = "Snap::Hutao::Cultivation::Statistics::WeeklyBossMaterialInterchange";
|
||||||
|
public const string CultivationRefreshInventoryByCalculatorToAllProjects = "Snap::Hutao::Cultivation::RefreshInventory::ByCalculator::ToAllProjects";
|
||||||
|
|
||||||
// GachaLog
|
// GachaLog
|
||||||
public const string IsEmptyHistoryWishVisible = "Snap::Hutao::GachaLog::HistoryWish::EmptyVisible";
|
public const string IsEmptyHistoryWishVisible = "Snap::Hutao::GachaLog::HistoryWish::EmptyVisible";
|
||||||
|
|||||||
668
src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs
generated
Normal file
668
src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs
generated
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260503131124_AddCultivateEntryRelatedEntryId")]
|
||||||
|
partial class AddCultivateEntryRelatedEntryId
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ArchiveId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Current")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Time")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ArchiveId");
|
||||||
|
|
||||||
|
b.ToTable("achievements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("achievement_archives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Info2")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("RefreshTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("avatar_infos");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarStrategy", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("AvatarId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChineseStrategyId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("OverseaStrategyId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AvatarId");
|
||||||
|
|
||||||
|
b.ToTable("avatar_strategies");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RelatedEntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ProjectId");
|
||||||
|
|
||||||
|
b.HasIndex("RelatedEntryId");
|
||||||
|
|
||||||
|
b.ToTable("cultivate_entries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AvatarIsPromoting")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("AvatarLevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("AvatarLevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("EntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillALevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillALevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillELevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillELevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillQLevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillQLevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("WeaponIsPromoting")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("WeaponLevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("WeaponLevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("EntryId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("cultivate_entry_level_informations");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Count")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("EntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsFinished")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("EntryId");
|
||||||
|
|
||||||
|
b.ToTable("cultivate_items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("ServerTimeZoneOffset")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("cultivate_projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DailyNote")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DailyTaskDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("DailyTaskNotify")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("DailyTaskNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ExpeditionDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ExpeditionNotify")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ExpeditionNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("HomeCoinDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("HomeCoinNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("HomeCoinNotifyThreshold")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("RefreshTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("ResinDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ResinNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ResinNotifyThreshold")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TransformerDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TransformerNotify")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TransformerNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("gacha_archives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ArchiveId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("GachaType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("QueryType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Time")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ArchiveId");
|
||||||
|
|
||||||
|
b.ToTable("gacha_items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Index")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("MacAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Mid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MihoyoSDK")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("game_accounts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.HardChallengeEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HardChallengeData")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("hard_challenges");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Count")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ProjectId");
|
||||||
|
|
||||||
|
b.ToTable("inventory_items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpireTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("object_cache");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.RoleCombatEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleCombatData")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("role_combats");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SpiralAbyss")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("spiral_abysses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.UidProfilePicture", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("AvatarId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("CostumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ProfilePictureId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("RefreshTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("uid_profile_pictures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Aid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CookieToken")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Fingerprint")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Index")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOversea")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("Ltoken");
|
||||||
|
|
||||||
|
b.Property<string>("Mid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PreferredUid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("Stoken");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ArchiveId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Archive");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RelatedEntryId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Project");
|
||||||
|
|
||||||
|
b.Navigation("RelatedEntry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||||
|
.WithOne("LevelInformation")
|
||||||
|
.HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Entry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("EntryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Entry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ArchiveId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Archive");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Project");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LevelInformation");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCultivateEntryRelatedEntryId : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCultivateEntryAssociationOnly : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260504033049_AddCultivateProjectAvatarPropertyBatchPreferences")]
|
||||||
|
partial class AddCultivateProjectAvatarPropertyBatchPreferences
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ArchiveId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Current")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Time")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ArchiveId");
|
||||||
|
|
||||||
|
b.ToTable("achievements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("achievement_archives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Info2")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("RefreshTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("avatar_infos");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarStrategy", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("AvatarId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChineseStrategyId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("OverseaStrategyId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AvatarId");
|
||||||
|
|
||||||
|
b.ToTable("avatar_strategies");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RelatedEntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ProjectId");
|
||||||
|
|
||||||
|
b.HasIndex("RelatedEntryId");
|
||||||
|
|
||||||
|
b.ToTable("cultivate_entries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AvatarIsPromoting")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("AvatarLevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("AvatarLevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("EntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillALevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillALevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillELevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillELevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillQLevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("SkillQLevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("WeaponIsPromoting")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("WeaponLevelFrom")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("WeaponLevelTo")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("EntryId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("cultivate_entry_level_informations");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Count")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("EntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsFinished")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("EntryId");
|
||||||
|
|
||||||
|
b.ToTable("cultivate_items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarPropertyBatchCultivatePreferencesJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("ServerTimeZoneOffset")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("cultivate_projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DailyNote")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DailyTaskDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("DailyTaskNotify")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("DailyTaskNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ExpeditionDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ExpeditionNotify")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ExpeditionNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("HomeCoinDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("HomeCoinNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("HomeCoinNotifyThreshold")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("RefreshTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("ResinDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ResinNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ResinNotifyThreshold")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TransformerDotVisible")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TransformerNotify")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TransformerNotifySuppressed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("gacha_archives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ArchiveId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("GachaType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("QueryType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Time")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ArchiveId");
|
||||||
|
|
||||||
|
b.ToTable("gacha_items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Index")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("MacAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Mid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MihoyoSDK")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("game_accounts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.HardChallengeEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HardChallengeData")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("hard_challenges");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("Count")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ProjectId");
|
||||||
|
|
||||||
|
b.ToTable("inventory_items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpireTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("object_cache");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.RoleCombatEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleCombatData")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("role_combats");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SpiralAbyss")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("spiral_abysses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.UidProfilePicture", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("AvatarId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("CostumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("ProfilePictureId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("RefreshTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Uid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("uid_profile_pictures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InnerId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Aid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CookieToken")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Fingerprint")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Index")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOversea")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSelected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("Ltoken");
|
||||||
|
|
||||||
|
b.Property<string>("Mid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PreferredUid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("Stoken");
|
||||||
|
|
||||||
|
b.HasKey("InnerId");
|
||||||
|
|
||||||
|
b.ToTable("users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ArchiveId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Archive");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RelatedEntryId");
|
||||||
|
|
||||||
|
b.Navigation("Project");
|
||||||
|
|
||||||
|
b.Navigation("RelatedEntry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||||
|
.WithOne("LevelInformation")
|
||||||
|
.HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Entry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("EntryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Entry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ArchiveId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Archive");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Project");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LevelInformation");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCultivateProjectAvatarPropertyBatchPreferences : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "AvatarPropertyBatchCultivatePreferencesJson",
|
||||||
|
table: "cultivate_projects",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AvatarPropertyBatchCultivatePreferencesJson",
|
||||||
|
table: "cultivate_projects");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class FixCultivateEntryRelatedEntryOnDeleteSetNull : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
|
||||||
|
table: "cultivate_entries",
|
||||||
|
column: "RelatedEntryId",
|
||||||
|
principalTable: "cultivate_entries",
|
||||||
|
principalColumn: "InnerId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||||
|
|
||||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||||
{
|
{
|
||||||
@@ -113,6 +113,9 @@ namespace Snap.Hutao.Migrations
|
|||||||
b.Property<Guid>("ProjectId")
|
b.Property<Guid>("ProjectId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RelatedEntryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@@ -120,6 +123,8 @@ namespace Snap.Hutao.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ProjectId");
|
b.HasIndex("ProjectId");
|
||||||
|
|
||||||
|
b.HasIndex("RelatedEntryId");
|
||||||
|
|
||||||
b.ToTable("cultivate_entries");
|
b.ToTable("cultivate_entries");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,6 +212,9 @@ namespace Snap.Hutao.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarPropertyBatchCultivatePreferencesJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<bool>("IsSelected")
|
b.Property<bool>("IsSelected")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@@ -585,7 +593,14 @@ namespace Snap.Hutao.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RelatedEntryId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Project");
|
b.Navigation("Project");
|
||||||
|
|
||||||
|
b.Navigation("RelatedEntry");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Model.Cultivation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 「我的角色」批量同步到当前养成计划时,在批量对话框中使用的目标等级、保存策略等;按 <see cref="Entity.CultivateProject"/> 持久化。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CultivateProjectAvatarPropertyBatchPreferences
|
||||||
|
{
|
||||||
|
public uint AvatarLevelTarget { get; set; }
|
||||||
|
|
||||||
|
public uint SkillATarget { get; set; }
|
||||||
|
|
||||||
|
public uint SkillETarget { get; set; }
|
||||||
|
|
||||||
|
public uint SkillQTarget { get; set; }
|
||||||
|
|
||||||
|
public uint WeaponLevelTarget { get; set; }
|
||||||
|
|
||||||
|
public int ConsumptionSaveStrategyIndex { get; set; }
|
||||||
|
|
||||||
|
public bool ClearAvatarAndWeaponEntriesBeforeSync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行批量前是否先通过养成计算器将游戏背包同步到当前计划(与养成计划「同步背包物品」一致)。
|
||||||
|
/// </summary>
|
||||||
|
public bool SyncInventoryItems { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行批量前是否先从米游社原神战绩同步「我的角色」数据(与我的角色「同步角色信息」一致)。
|
||||||
|
/// </summary>
|
||||||
|
public bool SyncCharacterInfo { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Model.Entity.Configuration;
|
||||||
|
|
||||||
|
internal sealed class CultivateEntryConfiguration : IEntityTypeConfiguration<CultivateEntry>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<CultivateEntry> builder)
|
||||||
|
{
|
||||||
|
builder.HasOne(e => e.RelatedEntry)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.RelatedEntryId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,11 @@ internal sealed class CultivateEntry : IAppDbEntity
|
|||||||
|
|
||||||
public uint Id { get; set; }
|
public uint Id { get; set; }
|
||||||
|
|
||||||
|
public Guid? RelatedEntryId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(RelatedEntryId))]
|
||||||
|
public CultivateEntry? RelatedEntry { get; set; }
|
||||||
|
|
||||||
public static CultivateEntry From(Guid projectId, CultivateType type, uint id)
|
public static CultivateEntry From(Guid projectId, CultivateType type, uint id)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ internal sealed partial class CultivateProject : ISelectable,
|
|||||||
|
|
||||||
public TimeSpan ServerTimeZoneOffset { get; set; }
|
public TimeSpan ServerTimeZoneOffset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="Model.Cultivation.CultivateProjectAvatarPropertyBatchPreferences"/> 的 JSON,按项目记忆批量同步养成选项。
|
||||||
|
/// </summary>
|
||||||
|
public string? AvatarPropertyBatchCultivatePreferencesJson { get; set; }
|
||||||
|
|
||||||
public static CultivateProject From(string name, in TimeSpan serverTimeOffset)
|
public static CultivateProject From(string name, in TimeSpan serverTimeOffset)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ internal sealed partial class AppDbContext : DbContext
|
|||||||
{
|
{
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.ApplyConfiguration(new AvatarInfoConfiguration())
|
.ApplyConfiguration(new AvatarInfoConfiguration())
|
||||||
|
.ApplyConfiguration(new CultivateEntryConfiguration())
|
||||||
.ApplyConfiguration(new DailyNoteEntryConfiguration())
|
.ApplyConfiguration(new DailyNoteEntryConfiguration())
|
||||||
.ApplyConfiguration(new SpiralAbyssEntryConfiguration())
|
.ApplyConfiguration(new SpiralAbyssEntryConfiguration())
|
||||||
.ApplyConfiguration(new RoleCombatEntryConfiguration())
|
.ApplyConfiguration(new RoleCombatEntryConfiguration())
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ internal static class AvatarIds
|
|||||||
public static readonly AvatarId Columbina = 10000125;
|
public static readonly AvatarId Columbina = 10000125;
|
||||||
public static readonly AvatarId Zibai = 10000126;
|
public static readonly AvatarId Zibai = 10000126;
|
||||||
public static readonly AvatarId Illuga = 10000127;
|
public static readonly AvatarId Illuga = 10000127;
|
||||||
|
public static readonly AvatarId Varka = 10000128;
|
||||||
|
public static readonly AvatarId Lohen = 10000129;
|
||||||
|
public static readonly AvatarId Linnea = 10000130;
|
||||||
|
public static readonly AvatarId Nicole = 10000131;
|
||||||
|
public static readonly AvatarId Prune = 10000132;
|
||||||
|
|
||||||
private static readonly FrozenSet<AvatarId> StandardWishIds =
|
private static readonly FrozenSet<AvatarId> StandardWishIds =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ namespace Snap.Hutao.Model.Metadata.Item;
|
|||||||
|
|
||||||
internal static class MaterialIds
|
internal static class MaterialIds
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 材料统计右键「未完成条目」不追溯的材料(摩拉、经验书、精锻用魔矿)。
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsExcludedFromStatisticsConsumerMenu(uint materialId)
|
||||||
|
{
|
||||||
|
return materialId is Mora
|
||||||
|
or WanderersAdvice
|
||||||
|
or AdventurersExperience
|
||||||
|
or HeroesWit
|
||||||
|
or MysticEnhancementOre;
|
||||||
|
}
|
||||||
|
|
||||||
public const uint Mora = 202U; // 摩拉
|
public const uint Mora = 202U; // 摩拉
|
||||||
public const uint WanderersAdvice = 104001U; // 流浪者的经验
|
public const uint WanderersAdvice = 104001U; // 流浪者的经验
|
||||||
public const uint AdventurersExperience = 104002U; // 冒险家的经验
|
public const uint AdventurersExperience = 104002U; // 冒险家的经验
|
||||||
|
|||||||
@@ -28,13 +28,11 @@ internal static class WeaponIds
|
|||||||
|
|
||||||
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
|
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
|
||||||
[
|
[
|
||||||
11501U, 11502U,
|
11501U, 11502U, 11518U, 11519U,
|
||||||
12501U, 12502U,
|
12501U, 12502U,
|
||||||
13502U, 13505U,
|
13502U, 13505U, 13517U,
|
||||||
14501U, 14502U,
|
14501U, 14502U, 14522U, 14523U,
|
||||||
15501U, 15502U,
|
15501U, 15502U, 15515U
|
||||||
15515U, 11518U,
|
|
||||||
14522U, 11519U
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="60568DGPStudio.SnapHutao"
|
Name="60568DGPStudio.SnapHutao"
|
||||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||||
Version="1.18.4.0" />
|
Version="1.18.7.0" />
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Snap Hutao</DisplayName>
|
<DisplayName>Snap Hutao</DisplayName>
|
||||||
|
|||||||
@@ -1349,6 +1349,9 @@ Space Available: {2}</value>
|
|||||||
<data name="ViewDialogCultivateBatchWeaponLevelTarget" xml:space="preserve">
|
<data name="ViewDialogCultivateBatchWeaponLevelTarget" xml:space="preserve">
|
||||||
<value>Weapon Target Level</value>
|
<value>Weapon Target Level</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewDialogCultivateBatchClearAvatarAndWeaponEntries" xml:space="preserve">
|
||||||
|
<value>Clear existing character and weapon entries before updating</value>
|
||||||
|
</data>
|
||||||
<data name="ViewDialogCultivateProjectInputPlaceholder" xml:space="preserve">
|
<data name="ViewDialogCultivateProjectInputPlaceholder" xml:space="preserve">
|
||||||
<value>Enter the plan name here</value>
|
<value>Enter the plan name here</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1880,6 +1883,9 @@ Space Available: {2}</value>
|
|||||||
<data name="ViewModelCultivationEntryViewPromoteOnlyHint" xml:space="preserve">
|
<data name="ViewModelCultivationEntryViewPromoteOnlyHint" xml:space="preserve">
|
||||||
<value>Ascending only</value>
|
<value>Ascending only</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewModelCultivationEntryRelatedAvatar" xml:space="preserve">
|
||||||
|
<value>Linked character: {0}</value>
|
||||||
|
</data>
|
||||||
<data name="ViewModelCultivationProjectAdded" xml:space="preserve">
|
<data name="ViewModelCultivationProjectAdded" xml:space="preserve">
|
||||||
<value>Added successfully</value>
|
<value>Added successfully</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2420,6 +2426,15 @@ Space Available: {2}</value>
|
|||||||
<data name="ViewPageCultivationMaterialListIncompleteFirstLabel" xml:space="preserve">
|
<data name="ViewPageCultivationMaterialListIncompleteFirstLabel" xml:space="preserve">
|
||||||
<value>Uncollected First</value>
|
<value>Uncollected First</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationMergeUpgradeMaterialsLabel" xml:space="preserve">
|
||||||
|
<value>Merge upgrade materials</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageCultivationTalentSynthCritTenPercentLabel" xml:space="preserve">
|
||||||
|
<value>Character talent (10%)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageCultivationWeeklyBossMaterialInterchangeLabel" xml:space="preserve">
|
||||||
|
<value>Weekly boss material interchange</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationMaterialListResinStatisticsLabel" xml:space="preserve">
|
<data name="ViewPageCultivationMaterialListResinStatisticsLabel" xml:space="preserve">
|
||||||
<value>Resin Estimation</value>
|
<value>Resin Estimation</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2429,9 +2444,18 @@ Space Available: {2}</value>
|
|||||||
<data name="ViewPageCultivationMaterialStatistics" xml:space="preserve">
|
<data name="ViewPageCultivationMaterialStatistics" xml:space="preserve">
|
||||||
<value>Material Statistics</value>
|
<value>Material Statistics</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationStatisticsUnfinishedConsumersEmptyList" xml:space="preserve">
|
||||||
|
<value>No unchecked entries</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageCultivationStatisticsConsumerMenuExcluded" xml:space="preserve">
|
||||||
|
<value>Mora, EXP books, and Mystic Enhancement Ore have no per-entry breakdown.</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
|
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
|
||||||
<value>Go</value>
|
<value>Go</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationSyncAllAvatarsAndWeapons" xml:space="preserve">
|
||||||
|
<value>Sync All Characters and Weapons</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
|
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
|
||||||
<value>Sync Inventory Items</value>
|
<value>Sync Inventory Items</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2441,6 +2465,9 @@ Space Available: {2}</value>
|
|||||||
<data name="ViewPageCultivationRefreshInventoryByEmbeddedYae" xml:space="preserve">
|
<data name="ViewPageCultivationRefreshInventoryByEmbeddedYae" xml:space="preserve">
|
||||||
<value>Sync by Embedded Yae</value>
|
<value>Sync by Embedded Yae</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationRefreshInventoryAllPlansShortLabel" xml:space="preserve">
|
||||||
|
<value>Inventory sync affects all plans</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationRemoveEntry" xml:space="preserve">
|
<data name="ViewPageCultivationRemoveEntry" xml:space="preserve">
|
||||||
<value>Delete list</value>
|
<value>Delete list</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -1127,12 +1127,21 @@
|
|||||||
<data name="ServiceGameSetMultiChannelUnauthorizedAccess" xml:space="preserve">
|
<data name="ServiceGameSetMultiChannelUnauthorizedAccess" xml:space="preserve">
|
||||||
<value>无法读取或保存配置文件,请以管理员模式重试</value>
|
<value>无法读取或保存配置文件,请以管理员模式重试</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ServiceGitRepositoryCloneReasonForceInvalid" xml:space="preserve">
|
||||||
|
<value>现有元数据仓库更新失败,正在重新下载</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServiceGitRepositoryCloneReasonInvalidRepo" xml:space="preserve">
|
||||||
|
<value>首次下载或仓库损坏,正在下载元数据</value>
|
||||||
|
</data>
|
||||||
<data name="ServiceGitRepositoryOperationCompleted" xml:space="preserve">
|
<data name="ServiceGitRepositoryOperationCompleted" xml:space="preserve">
|
||||||
<value>操作完成</value>
|
<value>操作完成</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ServiceGitRepositoryOperationFailed" xml:space="preserve">
|
<data name="ServiceGitRepositoryOperationFailed" xml:space="preserve">
|
||||||
<value>操作失败</value>
|
<value>操作失败</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ServiceGitRepositoryUpdatingExisting" xml:space="preserve">
|
||||||
|
<value>检查元数据更新</value>
|
||||||
|
</data>
|
||||||
<data name="ServiceHutaoUserGachaLogExpiredAt" xml:space="preserve">
|
<data name="ServiceHutaoUserGachaLogExpiredAt" xml:space="preserve">
|
||||||
<value>祈愿记录上传服务有效期至</value>
|
<value>祈愿记录上传服务有效期至</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1382,6 +1391,9 @@
|
|||||||
<data name="ViewDialogCultivateBatchWeaponLevelTarget" xml:space="preserve">
|
<data name="ViewDialogCultivateBatchWeaponLevelTarget" xml:space="preserve">
|
||||||
<value>武器目标等级</value>
|
<value>武器目标等级</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewDialogCultivateBatchClearAvatarAndWeaponEntries" xml:space="preserve">
|
||||||
|
<value>更新计划前先清空已有角色与武器条目</value>
|
||||||
|
</data>
|
||||||
<data name="ViewDialogCultivateProjectInputPlaceholder" xml:space="preserve">
|
<data name="ViewDialogCultivateProjectInputPlaceholder" xml:space="preserve">
|
||||||
<value>在此处输入计划名称</value>
|
<value>在此处输入计划名称</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1598,6 +1610,9 @@
|
|||||||
<data name="ViewDialogThirdPartyToolLaunch" xml:space="preserve">
|
<data name="ViewDialogThirdPartyToolLaunch" xml:space="preserve">
|
||||||
<value>启动</value>
|
<value>启动</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewDialogThirdPartyToolVersion" xml:space="preserve">
|
||||||
|
<value>版本:</value>
|
||||||
|
</data>
|
||||||
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
||||||
<value>使用米游社扫描二维码</value>
|
<value>使用米游社扫描二维码</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1919,6 +1934,9 @@
|
|||||||
<data name="ViewModelCultivationEntryViewPromoteOnlyHint" xml:space="preserve">
|
<data name="ViewModelCultivationEntryViewPromoteOnlyHint" xml:space="preserve">
|
||||||
<value>仅突破</value>
|
<value>仅突破</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewModelCultivationEntryRelatedAvatar" xml:space="preserve">
|
||||||
|
<value>关联角色:{0}</value>
|
||||||
|
</data>
|
||||||
<data name="ViewModelCultivationProjectAdded" xml:space="preserve">
|
<data name="ViewModelCultivationProjectAdded" xml:space="preserve">
|
||||||
<value>添加成功</value>
|
<value>添加成功</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2459,6 +2477,15 @@
|
|||||||
<data name="ViewPageCultivationMaterialListIncompleteFirstLabel" xml:space="preserve">
|
<data name="ViewPageCultivationMaterialListIncompleteFirstLabel" xml:space="preserve">
|
||||||
<value>未集齐优先</value>
|
<value>未集齐优先</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationMergeUpgradeMaterialsLabel" xml:space="preserve">
|
||||||
|
<value>升级材料合并</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageCultivationTalentSynthCritTenPercentLabel" xml:space="preserve">
|
||||||
|
<value>角色天赋(10%)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageCultivationWeeklyBossMaterialInterchangeLabel" xml:space="preserve">
|
||||||
|
<value>周本材料转化</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationMaterialListResinStatisticsLabel" xml:space="preserve">
|
<data name="ViewPageCultivationMaterialListResinStatisticsLabel" xml:space="preserve">
|
||||||
<value>树脂预估</value>
|
<value>树脂预估</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2468,9 +2495,18 @@
|
|||||||
<data name="ViewPageCultivationMaterialStatistics" xml:space="preserve">
|
<data name="ViewPageCultivationMaterialStatistics" xml:space="preserve">
|
||||||
<value>材料统计</value>
|
<value>材料统计</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationStatisticsUnfinishedConsumersEmptyList" xml:space="preserve">
|
||||||
|
<value>无未勾选条目</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageCultivationStatisticsConsumerMenuExcluded" xml:space="preserve">
|
||||||
|
<value>摩拉、经验书与魔矿不显示未完成条目。</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
|
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
|
||||||
<value>前往</value>
|
<value>前往</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationSyncAllAvatarsAndWeapons" xml:space="preserve">
|
||||||
|
<value>同步所有角色与武器</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
|
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
|
||||||
<value>同步背包物品</value>
|
<value>同步背包物品</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2480,6 +2516,9 @@
|
|||||||
<data name="ViewPageCultivationRefreshInventoryByEmbeddedYae" xml:space="preserve">
|
<data name="ViewPageCultivationRefreshInventoryByEmbeddedYae" xml:space="preserve">
|
||||||
<value>通过 Embedded Yae 同步</value>
|
<value>通过 Embedded Yae 同步</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationRefreshInventoryAllPlansShortLabel" xml:space="preserve">
|
||||||
|
<value>背包同步影响所有计划</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationRemoveEntry" xml:space="preserve">
|
<data name="ViewPageCultivationRemoveEntry" xml:space="preserve">
|
||||||
<value>删除清单</value>
|
<value>删除清单</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -2429,6 +2429,9 @@
|
|||||||
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
|
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
|
||||||
<value>前往</value>
|
<value>前往</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageCultivationSyncAllAvatarsAndWeapons" xml:space="preserve">
|
||||||
|
<value>同步所有角色與武器</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
|
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
|
||||||
<value>同步背包物品</value>
|
<value>同步背包物品</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Snap.Hutao.Core.Setting;
|
||||||
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
|
using Snap.Hutao.Model.Calculable;
|
||||||
|
using Snap.Hutao.Model.Cultivation;
|
||||||
|
using Snap.Hutao.Model.Entity;
|
||||||
|
using Snap.Hutao.Model.Entity.Primitive;
|
||||||
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using Snap.Hutao.Service.AvatarInfo;
|
||||||
|
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||||
|
using Snap.Hutao.Service.Cultivation.Consumption;
|
||||||
|
using Snap.Hutao.Service.Cultivation.Offline;
|
||||||
|
using Snap.Hutao.Service.Inventory;
|
||||||
|
using Snap.Hutao.Service.Metadata;
|
||||||
|
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||||
|
using Snap.Hutao.Service.User;
|
||||||
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||||
|
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||||
|
using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||||
|
using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
||||||
|
using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
|
[Service(ServiceLifetime.Singleton, typeof(IAvatarPropertyBatchCultivateService))]
|
||||||
|
internal sealed partial class AvatarPropertyBatchCultivateService : IAvatarPropertyBatchCultivateService
|
||||||
|
{
|
||||||
|
private readonly IContentDialogFactory contentDialogFactory;
|
||||||
|
private readonly ICultivationService cultivationService;
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
|
|
||||||
|
[GeneratedConstructor]
|
||||||
|
public partial AvatarPropertyBatchCultivateService(IServiceProvider serviceProvider);
|
||||||
|
|
||||||
|
public async ValueTask<BatchCultivateResult?> ExecuteAsync(SummaryFactoryMetadataContext metadataContext, ImmutableArray<AvatarView> targetAvatars, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CultivateProjectAvatarPropertyBatchPreferences? batchPrefs = await cultivationService
|
||||||
|
.GetAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
CultivatePromotionDeltaBatchDialog dialog = batchPrefs is null
|
||||||
|
? await contentDialogFactory
|
||||||
|
.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>(serviceProvider)
|
||||||
|
.ConfigureAwait(false)
|
||||||
|
: await contentDialogFactory
|
||||||
|
.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>(serviceProvider, batchPrefs)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false) is not (true, { } baseline))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await cultivationService
|
||||||
|
.SaveAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync(ToAvatarPropertyBatchPreferences(baseline))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(baseline.Delta.Weapon);
|
||||||
|
|
||||||
|
ContentDialog progressDialog = await contentDialogFactory
|
||||||
|
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
BatchCultivateResult result = default;
|
||||||
|
using (await contentDialogFactory.BlockAsync(progressDialog).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
ImmutableArray<AvatarView> avatarsToProcess = targetAvatars;
|
||||||
|
|
||||||
|
if (baseline.SyncCharacterInfo)
|
||||||
|
{
|
||||||
|
IUserService userService = serviceProvider.GetRequiredService<IUserService>();
|
||||||
|
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||||
|
{
|
||||||
|
IServiceScopeFactory scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
IAvatarInfoService avatarInfoService = scope.ServiceProvider.GetRequiredService<IAvatarInfoService>();
|
||||||
|
Summary? refreshed = await avatarInfoService
|
||||||
|
.GetSummaryAsync(metadataContext, userAndUid, global::Snap.Hutao.Service.AvatarInfo.RefreshOptionKind.RequestFromHoyolabGameRecord, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (refreshed?.Avatars.Source is { Count: > 0 } sourceAvatars)
|
||||||
|
{
|
||||||
|
HashSet<AvatarId> wanted = [.. targetAvatars.Select(static a => a.Id)];
|
||||||
|
ImmutableArray<AvatarView>.Builder filtered = ImmutableArray.CreateBuilder<AvatarView>();
|
||||||
|
foreach (AvatarView avatar in sourceAvatars)
|
||||||
|
{
|
||||||
|
if (wanted.Contains(avatar.Id))
|
||||||
|
{
|
||||||
|
filtered.Add(avatar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered.Count > 0)
|
||||||
|
{
|
||||||
|
avatarsToProcess = filtered.ToImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseline.SyncInventoryItems)
|
||||||
|
{
|
||||||
|
Snap.Hutao.Core.Database.IAdvancedDbCollectionView<CultivateProject> projects = await cultivationService.GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
|
await cultivationService.EnsureCurrentProjectAsync(projects).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (projects.CurrentItem is { } project)
|
||||||
|
{
|
||||||
|
IMetadataService metadataService = serviceProvider.GetRequiredService<IMetadataService>();
|
||||||
|
ICultivationMetadataContext cultivationContext = await metadataService
|
||||||
|
.GetContextAsync<CultivationMetadataContext>(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
IInventoryService inventoryService = serviceProvider.GetRequiredService<IInventoryService>();
|
||||||
|
await inventoryService
|
||||||
|
.RefreshInventoryAsync(RefreshOptions.CreateForWebCalculator(
|
||||||
|
project,
|
||||||
|
cultivationContext,
|
||||||
|
LocalSetting.Get(SettingKeys.CultivationRefreshInventoryByCalculatorToAllProjects, false)))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseline.ClearAvatarAndWeaponEntriesBeforeSync)
|
||||||
|
{
|
||||||
|
await cultivationService.RemoveAvatarAndWeaponEntriesForCurrentProjectAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmutableArray<CalculatorAvatarPromotionDelta>.Builder deltasBuilder = ImmutableArray.CreateBuilder<CalculatorAvatarPromotionDelta>();
|
||||||
|
foreach (AvatarView avatar in avatarsToProcess)
|
||||||
|
{
|
||||||
|
if (!baseline.Delta.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
|
||||||
|
{
|
||||||
|
++result.SkippedCount;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
deltasBuilder.Add(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmutableArray<CalculatorAvatarPromotionDelta> deltas = deltasBuilder.ToImmutable();
|
||||||
|
|
||||||
|
CalculatorBatchConsumption batchConsumption = OfflineCalculator.CalculateBatchConsumption(deltas, metadataContext);
|
||||||
|
|
||||||
|
foreach ((CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta) in batchConsumption.Items.Zip(deltas))
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!await SaveCultivationAsync(consumption, new CultivatePromotionDeltaOptions(delta, baseline.Strategy)).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
result.StopReason = BatchCultivateStopReason.NoProject;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
++result.SucceedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CultivateProjectAvatarPropertyBatchPreferences ToAvatarPropertyBatchPreferences(CultivatePromotionDeltaOptions baseline)
|
||||||
|
{
|
||||||
|
CalculatorAvatarPromotionDelta d = baseline.Delta;
|
||||||
|
uint sa = 10;
|
||||||
|
uint se = 10;
|
||||||
|
uint sq = 10;
|
||||||
|
if (d.SkillList is [{ } a, { } e, { } q, ..])
|
||||||
|
{
|
||||||
|
sa = a.LevelTarget;
|
||||||
|
se = e.LevelTarget;
|
||||||
|
sq = q.LevelTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CultivateProjectAvatarPropertyBatchPreferences
|
||||||
|
{
|
||||||
|
AvatarLevelTarget = d.AvatarLevelTarget,
|
||||||
|
SkillATarget = sa,
|
||||||
|
SkillETarget = se,
|
||||||
|
SkillQTarget = sq,
|
||||||
|
WeaponLevelTarget = d.Weapon?.LevelTarget ?? 90U,
|
||||||
|
ConsumptionSaveStrategyIndex = (int)baseline.Strategy,
|
||||||
|
ClearAvatarAndWeaponEntriesBeforeSync = baseline.ClearAvatarAndWeaponEntriesBeforeSync,
|
||||||
|
SyncInventoryItems = baseline.SyncInventoryItems,
|
||||||
|
SyncCharacterInfo = baseline.SyncCharacterInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<bool> SaveCultivationAsync(CalculatorConsumption consumption, CultivatePromotionDeltaOptions options)
|
||||||
|
{
|
||||||
|
LevelInformation levelInformation = LevelInformation.From(options.Delta);
|
||||||
|
|
||||||
|
InputConsumption avatarInput = new()
|
||||||
|
{
|
||||||
|
Type = CultivateType.AvatarAndSkill,
|
||||||
|
ItemId = options.Delta.AvatarId,
|
||||||
|
Items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume),
|
||||||
|
LevelInformation = levelInformation,
|
||||||
|
Strategy = options.Strategy,
|
||||||
|
};
|
||||||
|
|
||||||
|
ConsumptionSaveResult avatarSave = await cultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (avatarSave.Kind is ConsumptionSaveResultKind.NoProject)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(options.Delta.Weapon);
|
||||||
|
|
||||||
|
Guid? relatedAvatarEntryId = avatarSave.CreatedEntryInnerId;
|
||||||
|
if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.Skipped)
|
||||||
|
{
|
||||||
|
relatedAvatarEntryId = await cultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.NoItem)
|
||||||
|
{
|
||||||
|
relatedAvatarEntryId = await cultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedAvatarEntryId is null && !consumption.WeaponConsume.IsEmpty)
|
||||||
|
{
|
||||||
|
relatedAvatarEntryId = await cultivationService.EnsureAvatarAssociationStubAsync(options.Delta.AvatarId, levelInformation).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputConsumption weaponInput = new()
|
||||||
|
{
|
||||||
|
Type = CultivateType.Weapon,
|
||||||
|
ItemId = options.Delta.Weapon.Id,
|
||||||
|
Items = consumption.WeaponConsume,
|
||||||
|
LevelInformation = levelInformation,
|
||||||
|
Strategy = options.Strategy,
|
||||||
|
RelatedEntryId = relatedAvatarEntryId,
|
||||||
|
};
|
||||||
|
|
||||||
|
ConsumptionSaveResult weaponSave = await cultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return weaponSave.Kind is not ConsumptionSaveResultKind.NoProject;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
namespace Snap.Hutao.ViewModel.AvatarProperty;
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
internal struct BatchCultivateResult
|
internal struct BatchCultivateResult
|
||||||
{
|
{
|
||||||
public int SucceedCount;
|
public int SucceedCount;
|
||||||
public int SkippedCount;
|
public int SkippedCount;
|
||||||
|
public BatchCultivateStopReason StopReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum BatchCultivateStopReason
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
NoProject = 1,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation.Consumption;
|
||||||
|
|
||||||
|
internal readonly record struct ConsumptionSaveResult(
|
||||||
|
ConsumptionSaveResultKind Kind,
|
||||||
|
Guid? CreatedEntryInnerId = null);
|
||||||
@@ -23,6 +23,9 @@ internal class CultivationMetadataContext : ICultivationMetadataContext
|
|||||||
|
|
||||||
public ImmutableDictionary<MaterialId, Combine> ResultMaterialIdCombineMap { get; set; } = default!;
|
public ImmutableDictionary<MaterialId, Combine> ResultMaterialIdCombineMap { get; set; } = default!;
|
||||||
|
|
||||||
|
public ImmutableArray<ImmutableArray<MaterialId>> WeeklyBossMaterialInterchangeGroups { get; set; }
|
||||||
|
= ImmutableArray<ImmutableArray<MaterialId>>.Empty;
|
||||||
|
|
||||||
public Item GetAvatarItem(AvatarId avatarId)
|
public Item GetAvatarItem(AvatarId avatarId)
|
||||||
{
|
{
|
||||||
return this.GetAvatar(avatarId).GetOrCreateItem();
|
return this.GetAvatar(avatarId).GetOrCreateItem();
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Snap.Hutao.Core.Database;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
|
using Snap.Hutao.Model.Entity.Primitive;
|
||||||
using Snap.Hutao.Service.Abstraction;
|
using Snap.Hutao.Service.Abstraction;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Cultivation;
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
@@ -47,6 +52,29 @@ internal sealed partial class CultivationRepository : ICultivationRepository
|
|||||||
return this.ImmutableArray<CultivateEntry>(e => e.ProjectId == projectId && e.Id == itemId);
|
return this.ImmutableArray<CultivateEntry>(e => e.ProjectId == projectId && e.Id == itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Guid? TryGetAvatarCultivateEntryInnerId(Guid projectId, uint avatarId)
|
||||||
|
{
|
||||||
|
// NOTE: InnerId(Guid) 的大小不代表插入顺序;这里使用 SQLite 的 rowid 选择最新插入的一条。
|
||||||
|
using (IServiceScope scope = ServiceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
AppDbContext db = scope.GetAppDbContext();
|
||||||
|
// EF Core 不会为 SQLite 的隐式 rowid 自动建模;这里用原生 SQL 取最新插入的一条。
|
||||||
|
return db.Set<CultivateEntry>()
|
||||||
|
.FromSqlInterpolated($"""
|
||||||
|
SELECT *
|
||||||
|
FROM cultivate_entries
|
||||||
|
WHERE ProjectId = {projectId}
|
||||||
|
AND Id = {avatarId}
|
||||||
|
AND Type = {(int)CultivateType.AvatarAndSkill}
|
||||||
|
ORDER BY rowid DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(e => (Guid?)e.InnerId)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void AddCultivateEntry(CultivateEntry entry)
|
public void AddCultivateEntry(CultivateEntry entry)
|
||||||
{
|
{
|
||||||
this.Add(entry);
|
this.Add(entry);
|
||||||
@@ -77,6 +105,11 @@ internal sealed partial class CultivationRepository : ICultivationRepository
|
|||||||
return this.ObservableCollection<CultivateProject>();
|
return this.ObservableCollection<CultivateProject>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<Guid> GetCultivateProjectInnerIds()
|
||||||
|
{
|
||||||
|
return this.ImmutableArray<CultivateProject, Guid>(query => query.Select(p => p.InnerId));
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveLevelInformationByEntryId(Guid entryId)
|
public void RemoveLevelInformationByEntryId(Guid entryId)
|
||||||
{
|
{
|
||||||
this.Delete<CultivateEntryLevelInformation>(l => l.EntryId == entryId);
|
this.Delete<CultivateEntryLevelInformation>(l => l.EntryId == entryId);
|
||||||
@@ -96,4 +129,23 @@ internal sealed partial class CultivationRepository : ICultivationRepository
|
|||||||
{
|
{
|
||||||
return this.Single<CultivateEntry, Guid>(query => query.Where(entry => entry.InnerId == entryId).Select(entry => entry.InnerId));
|
return this.Single<CultivateEntry, Guid>(query => query.Where(entry => entry.InnerId == entryId).Select(entry => entry.InnerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<(CultivateEntry Entry, CultivateItem Item)> GetCultivateEntryItemPairsByProjectId(Guid projectId)
|
||||||
|
{
|
||||||
|
using (IServiceScope scope = ServiceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
AppDbContext db = scope.GetAppDbContext();
|
||||||
|
IQueryable<CultivateEntry> entries = db.Set<CultivateEntry>().AsNoTracking().Where(e => e.ProjectId == projectId);
|
||||||
|
return [.. db.Set<CultivateItem>().AsNoTracking()
|
||||||
|
.Join(
|
||||||
|
entries,
|
||||||
|
item => item.EntryId,
|
||||||
|
entry => entry.InnerId,
|
||||||
|
(item, entry) => new { Entry = entry, Item = item })
|
||||||
|
.OrderBy(t => t.Entry.InnerId)
|
||||||
|
.ThenBy(t => t.Item.ItemId)
|
||||||
|
.ToList()
|
||||||
|
.Select(t => (t.Entry, t.Item))];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +124,6 @@ internal sealed partial class CultivationResinStatisticsService : ICultivationRe
|
|||||||
|
|
||||||
private static double GetStatisticsCultivateItemTimes(StatisticsCultivateItem item)
|
private static double GetStatisticsCultivateItemTimes(StatisticsCultivateItem item)
|
||||||
{
|
{
|
||||||
return item.Count - (long)item.Current;
|
return item.Count - (long)item.DisplayCurrent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,28 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.Database;
|
using Snap.Hutao.Core.Database;
|
||||||
|
using Snap.Hutao.Core.Text.Json;
|
||||||
|
using Snap.Hutao.Model;
|
||||||
|
using Snap.Hutao.Model.Cultivation;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Model.Entity.Primitive;
|
using Snap.Hutao.Model.Entity.Primitive;
|
||||||
using Snap.Hutao.Model.Intrinsic;
|
using Snap.Hutao.Model.Intrinsic;
|
||||||
using Snap.Hutao.Model.Metadata;
|
using Snap.Hutao.Model.Metadata;
|
||||||
|
using Snap.Hutao.Model.Metadata.Item;
|
||||||
using Snap.Hutao.Model.Primitive;
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using Snap.Hutao.Service.Abstraction;
|
||||||
using Snap.Hutao.Service.Cultivation.Consumption;
|
using Snap.Hutao.Service.Cultivation.Consumption;
|
||||||
using Snap.Hutao.Service.Inventory;
|
using Snap.Hutao.Service.Inventory;
|
||||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||||
using Snap.Hutao.ViewModel.Cultivation;
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
using ModelItem = Snap.Hutao.Model.Item;
|
using ModelItem = Snap.Hutao.Model.Item;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Cultivation;
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
@@ -63,10 +71,21 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
{
|
{
|
||||||
ImmutableArray<CultivateEntry> entries = cultivationRepository.GetCultivateEntryImmutableArrayIncludingLevelInformationByProjectId(cultivateProject.InnerId);
|
ImmutableArray<CultivateEntry> entries = cultivationRepository.GetCultivateEntryImmutableArrayIncludingLevelInformationByProjectId(cultivateProject.InnerId);
|
||||||
|
|
||||||
|
Dictionary<Guid, CultivateEntry> entryByInnerId = new(entries.Length);
|
||||||
|
foreach (ref readonly CultivateEntry entry in entries.AsSpan())
|
||||||
|
{
|
||||||
|
entryByInnerId[entry.InnerId] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
List<CultivateEntryView> resultEntries = new(entries.Length);
|
List<CultivateEntryView> resultEntries = new(entries.Length);
|
||||||
foreach (ref readonly CultivateEntry entry in entries.AsSpan())
|
foreach (ref readonly CultivateEntry entry in entries.AsSpan())
|
||||||
{
|
{
|
||||||
ImmutableArray<CultivateItem> items = cultivationRepository.GetCultivateItemImmutableArrayByEntryId(entry.InnerId);
|
ImmutableArray<CultivateItem> items = cultivationRepository.GetCultivateItemImmutableArrayByEntryId(entry.InnerId);
|
||||||
|
if (IsHiddenAssociationOnlyAvatarEntry(entry, items.Length))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ImmutableArray<CultivateItemView>.Builder entryItems = ImmutableArray.CreateBuilder<CultivateItemView>(items.Length);
|
ImmutableArray<CultivateItemView>.Builder entryItems = ImmutableArray.CreateBuilder<CultivateItemView>(items.Length);
|
||||||
|
|
||||||
foreach (ref readonly CultivateItem cultivateItem in items.AsSpan())
|
foreach (ref readonly CultivateItem cultivateItem in items.AsSpan())
|
||||||
@@ -83,7 +102,13 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
_ => default!,
|
_ => default!,
|
||||||
};
|
};
|
||||||
|
|
||||||
resultEntries.Add(CultivateEntryView.Create(entry, item, entryItems.ToImmutable()));
|
string? relatedAvatarName = null;
|
||||||
|
if (entry.Type is CultivateType.Weapon && entry.RelatedEntryId is Guid relatedId && entryByInnerId.TryGetValue(relatedId, out CultivateEntry? relatedEntry) && relatedEntry.Type is CultivateType.AvatarAndSkill)
|
||||||
|
{
|
||||||
|
relatedAvatarName = context.GetAvatarItem(relatedEntry.Id).Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultEntries.Add(CultivateEntryView.Create(entry, item, entryItems.ToImmutable(), relatedAvatarName));
|
||||||
}
|
}
|
||||||
|
|
||||||
ObservableCollection<CultivateEntryView> result = resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection();
|
ObservableCollection<CultivateEntryView> result = resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection();
|
||||||
@@ -92,20 +117,26 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<StatisticsCultivateItemCollection> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token)
|
public async ValueTask<StatisticsCultivateItemCollection> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context, CultivationStatisticsMergeOptions mergeOptions, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
return SynchronizedGetStatisticsCultivateItemCollection(cultivateProject, context);
|
token.ThrowIfCancellationRequested();
|
||||||
|
return SynchronizedGetStatisticsCultivateItemCollection(cultivateProject, context, mergeOptions, token);
|
||||||
|
|
||||||
StatisticsCultivateItemCollection SynchronizedGetStatisticsCultivateItemCollection(CultivateProject cultivateProject, ICultivationMetadataContext context)
|
StatisticsCultivateItemCollection SynchronizedGetStatisticsCultivateItemCollection(CultivateProject cultivateProject, ICultivationMetadataContext context, CultivationStatisticsMergeOptions mergeOptions, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
Dictionary</* ItemId */ uint, StatisticsCultivateItem> resultItems = [];
|
Dictionary</* ItemId */ uint, StatisticsCultivateItem> resultItems = [];
|
||||||
Guid projectId = cultivateProject.InnerId;
|
Guid projectId = cultivateProject.InnerId;
|
||||||
|
Dictionary<uint, uint> inventoryCounts = [];
|
||||||
|
|
||||||
foreach (ref readonly CultivateEntry entry in cultivationRepository.GetCultivateEntryImmutableArrayByProjectId(projectId).AsSpan())
|
foreach (ref readonly CultivateEntry entry in cultivationRepository.GetCultivateEntryImmutableArrayByProjectId(projectId).AsSpan())
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
foreach (ref readonly CultivateItem item in cultivationRepository.GetCultivateItemImmutableArrayByEntryId(entry.InnerId).AsSpan())
|
foreach (ref readonly CultivateItem item in cultivationRepository.GetCultivateItemImmutableArrayByEntryId(entry.InnerId).AsSpan())
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
ref StatisticsCultivateItem? existedItem = ref CollectionsMarshal.GetValueRefOrAddDefault(resultItems, item.ItemId, out _);
|
ref StatisticsCultivateItem? existedItem = ref CollectionsMarshal.GetValueRefOrAddDefault(resultItems, item.ItemId, out _);
|
||||||
if (existedItem is null || existedItem.ExcludedFromPresentation)
|
if (existedItem is null || existedItem.ExcludedFromPresentation)
|
||||||
{
|
{
|
||||||
@@ -116,12 +147,14 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
existedItem.Count += item.Count;
|
existedItem.Count += item.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecursiveAddMaterialIngredientsByMaterialId(cultivateProject, context, resultItems, item.ItemId);
|
RecursiveAddMaterialIngredientsByMaterialId(cultivateProject, context, resultItems, item.ItemId, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ref readonly InventoryItem inventoryItem in inventoryRepository.GetInventoryItemImmutableArrayByProjectId(projectId).AsSpan())
|
foreach (ref readonly InventoryItem inventoryItem in inventoryRepository.GetInventoryItemImmutableArrayByProjectId(projectId).AsSpan())
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
inventoryCounts[inventoryItem.ItemId] = inventoryItem.Count;
|
||||||
ref StatisticsCultivateItem existedItem = ref CollectionsMarshal.GetValueRefOrNullRef(resultItems, inventoryItem.ItemId);
|
ref StatisticsCultivateItem existedItem = ref CollectionsMarshal.GetValueRefOrNullRef(resultItems, inventoryItem.ItemId);
|
||||||
if (!Unsafe.IsNullRef(in existedItem))
|
if (!Unsafe.IsNullRef(in existedItem))
|
||||||
{
|
{
|
||||||
@@ -129,10 +162,73 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddWeeklyBossGroupInventoryDonors(
|
||||||
|
resultItems,
|
||||||
|
inventoryCounts,
|
||||||
|
context,
|
||||||
|
cultivateProject.ServerTimeZoneOffset,
|
||||||
|
mergeOptions.WeeklyBossMaterialInterchange);
|
||||||
|
|
||||||
|
CultivationStatisticsSurplusMerge.Apply(resultItems, context, mergeOptions);
|
||||||
|
CultivationStatisticsWeeklyBossInterchange.Apply(
|
||||||
|
resultItems,
|
||||||
|
context.WeeklyBossMaterialInterchangeGroups,
|
||||||
|
mergeOptions.WeeklyBossMaterialInterchange);
|
||||||
|
ApplyStatisticsConsumerMenuLines(resultItems, projectId, context, cultivationRepository, token);
|
||||||
|
|
||||||
return new(resultItems);
|
return new(resultItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddWeeklyBossGroupInventoryDonors(
|
||||||
|
Dictionary<uint, StatisticsCultivateItem> items,
|
||||||
|
Dictionary<uint, uint> inventoryCounts,
|
||||||
|
ICultivationMetadataContext context,
|
||||||
|
TimeSpan offset,
|
||||||
|
bool enabled)
|
||||||
|
{
|
||||||
|
if (!enabled || context.WeeklyBossMaterialInterchangeGroups.IsDefaultOrEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ImmutableArray<MaterialId> group in context.WeeklyBossMaterialInterchangeGroups)
|
||||||
|
{
|
||||||
|
bool anyPlannedInGroup = false;
|
||||||
|
foreach (MaterialId mid in group)
|
||||||
|
{
|
||||||
|
if (items.ContainsKey(mid))
|
||||||
|
{
|
||||||
|
anyPlannedInGroup = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyPlannedInGroup)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MaterialId mid in group)
|
||||||
|
{
|
||||||
|
uint id = mid;
|
||||||
|
if (items.ContainsKey(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inventoryCounts.TryGetValue(id, out uint inv) || inv is 0U)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticsCultivateItem donor = StatisticsCultivateItem.Create(context.GetMaterial(id), offset);
|
||||||
|
donor.Current = inv;
|
||||||
|
items[id] = donor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ValueTask<ResinStatistics> GetResinStatisticsAsync(StatisticsCultivateItemCollection statisticsCultivateItems, CancellationToken token)
|
public ValueTask<ResinStatistics> GetResinStatisticsAsync(StatisticsCultivateItemCollection statisticsCultivateItems, CancellationToken token)
|
||||||
{
|
{
|
||||||
return cultivationResinStatisticsService.GetResinStatisticsAsync(statisticsCultivateItems, token);
|
return cultivationResinStatisticsService.GetResinStatisticsAsync(statisticsCultivateItems, token);
|
||||||
@@ -144,41 +240,84 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
cultivationRepository.RemoveCultivateEntryById(entryId);
|
cultivationRepository.RemoveCultivateEntryById(entryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask RemoveAvatarAndWeaponEntriesForCurrentProjectAsync()
|
||||||
|
{
|
||||||
|
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
|
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
|
||||||
|
Guid projectId = projects.CurrentItem.InnerId;
|
||||||
|
|
||||||
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
|
ImmutableArray<CultivateEntry> entries = cultivationRepository.GetCultivateEntryImmutableArrayByProjectId(projectId);
|
||||||
|
List<Guid> weaponEntryIds = new(entries.Length);
|
||||||
|
List<Guid> avatarEntryIds = new(entries.Length);
|
||||||
|
foreach (ref readonly CultivateEntry entry in entries.AsSpan())
|
||||||
|
{
|
||||||
|
switch (entry.Type)
|
||||||
|
{
|
||||||
|
case CultivateType.Weapon:
|
||||||
|
weaponEntryIds.Add(entry.InnerId);
|
||||||
|
break;
|
||||||
|
case CultivateType.AvatarAndSkill:
|
||||||
|
avatarEntryIds.Add(entry.InnerId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Guid id in weaponEntryIds)
|
||||||
|
{
|
||||||
|
cultivationRepository.RemoveCultivateEntryById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Guid id in avatarEntryIds)
|
||||||
|
{
|
||||||
|
cultivationRepository.RemoveCultivateEntryById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
entryCollectionCache.TryRemove(projectId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
public void SaveCultivateItem(CultivateItemView item)
|
public void SaveCultivateItem(CultivateItemView item)
|
||||||
{
|
{
|
||||||
cultivationRepository.UpdateCultivateItem(item.Entity);
|
cultivationRepository.UpdateCultivateItem(item.Entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption)
|
public async ValueTask<ConsumptionSaveResult> SaveConsumptionAsync(InputConsumption inputConsumption)
|
||||||
{
|
{
|
||||||
// No selected project
|
// No selected project
|
||||||
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return ConsumptionSaveResultKind.NoProject;
|
return new(ConsumptionSaveResultKind.NoProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
|
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
|
||||||
|
Guid projectId = projects.CurrentItem.InnerId;
|
||||||
|
|
||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
// PreserveExisting or CreateNewEntry, but no item
|
// PreserveExisting or CreateNewEntry, but no item
|
||||||
if (inputConsumption is { Strategy: not ConsumptionSaveStrategyKind.OverwriteExisting, Items: [] })
|
if (inputConsumption is { Strategy: not ConsumptionSaveStrategyKind.OverwriteExisting, Items: [] })
|
||||||
{
|
{
|
||||||
return ConsumptionSaveResultKind.NoItem;
|
return new(ConsumptionSaveResultKind.NoItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreserveExisting or OverwriteExisting
|
// PreserveExisting or OverwriteExisting
|
||||||
if (inputConsumption.Strategy is not ConsumptionSaveStrategyKind.CreateNewEntry)
|
if (inputConsumption.Strategy is not ConsumptionSaveStrategyKind.CreateNewEntry)
|
||||||
{
|
{
|
||||||
// Check for existing entries
|
// Check for existing entries
|
||||||
ImmutableArray<CultivateEntry> entries = cultivationRepository.GetCultivateEntryImmutableArrayByProjectIdAndItemId(projects.CurrentItem.InnerId, inputConsumption.ItemId);
|
ImmutableArray<CultivateEntry> entries = cultivationRepository.GetCultivateEntryImmutableArrayByProjectIdAndItemId(projectId, inputConsumption.ItemId);
|
||||||
|
|
||||||
if (entries.Length > 0)
|
if (entries.Length > 0)
|
||||||
{
|
{
|
||||||
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting)
|
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting)
|
||||||
{
|
{
|
||||||
return ConsumptionSaveResultKind.Skipped;
|
return new(ConsumptionSaveResultKind.Skipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.OverwriteExisting)
|
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.OverwriteExisting)
|
||||||
@@ -192,7 +331,8 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
|
|
||||||
if (inputConsumption.Items is [])
|
if (inputConsumption.Items is [])
|
||||||
{
|
{
|
||||||
return ConsumptionSaveResultKind.Removed;
|
entryCollectionCache.TryRemove(projectId, out _);
|
||||||
|
return new(ConsumptionSaveResultKind.Removed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,13 +340,14 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
{
|
{
|
||||||
if (inputConsumption.Items is [])
|
if (inputConsumption.Items is [])
|
||||||
{
|
{
|
||||||
return ConsumptionSaveResultKind.NoItem;
|
return new(ConsumptionSaveResultKind.NoItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
CultivateEntry entry = CultivateEntry.From(projects.CurrentItem.InnerId, inputConsumption.Type, inputConsumption.ItemId);
|
CultivateEntry entry = CultivateEntry.From(projectId, inputConsumption.Type, inputConsumption.ItemId);
|
||||||
|
entry.RelatedEntryId = inputConsumption.RelatedEntryId;
|
||||||
cultivationRepository.AddCultivateEntry(entry);
|
cultivationRepository.AddCultivateEntry(entry);
|
||||||
|
|
||||||
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entry.InnerId, inputConsumption.Type, inputConsumption.LevelInformation);
|
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entry.InnerId, inputConsumption.Type, inputConsumption.LevelInformation);
|
||||||
@@ -217,10 +358,52 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
|
|
||||||
// The consumption save operation is always performed outside cultivation page
|
// The consumption save operation is always performed outside cultivation page
|
||||||
// and without touching the cache. So we have to invalidate the cache manually.
|
// and without touching the cache. So we have to invalidate the cache manually.
|
||||||
entryCollectionCache.TryRemove(projects.CurrentItem.InnerId, out _);
|
entryCollectionCache.TryRemove(projectId, out _);
|
||||||
|
|
||||||
|
return new(ConsumptionSaveResultKind.Added, entry.InnerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConsumptionSaveResultKind.Added;
|
public async ValueTask<Guid?> TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId)
|
||||||
|
{
|
||||||
|
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
|
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
|
||||||
|
|
||||||
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
return cultivationRepository.TryGetAvatarCultivateEntryInnerId(projects.CurrentItem.InnerId, avatarId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Guid?> EnsureAvatarAssociationStubAsync(uint avatarId, LevelInformation levelInformation)
|
||||||
|
{
|
||||||
|
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
|
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
|
||||||
|
|
||||||
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
|
Guid projectId = projects.CurrentItem.InnerId;
|
||||||
|
if (cultivationRepository.TryGetAvatarCultivateEntryInnerId(projectId, avatarId) is Guid existing)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
CultivateEntry entry = CultivateEntry.From(projectId, CultivateType.AvatarAndSkill, avatarId);
|
||||||
|
cultivationRepository.AddCultivateEntry(entry);
|
||||||
|
|
||||||
|
CultivateEntryLevelInformation level = CultivateEntryLevelInformation.From(entry.InnerId, CultivateType.AvatarAndSkill, levelInformation);
|
||||||
|
cultivationRepository.AddLevelInformation(level);
|
||||||
|
|
||||||
|
entryCollectionCache.TryRemove(projectId, out _);
|
||||||
|
return entry.InnerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project)
|
public async ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project)
|
||||||
@@ -244,6 +427,50 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
return ProjectAddResultKind.Added;
|
return ProjectAddResultKind.Added;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<CultivateProjectAvatarPropertyBatchPreferences?> GetAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync()
|
||||||
|
{
|
||||||
|
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
|
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? json = projects.CurrentItem?.AvatarPropertyBatchCultivatePreferencesJson;
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<CultivateProjectAvatarPropertyBatchPreferences>(json, JsonOptions.Default);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SaveAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync(CultivateProjectAvatarPropertyBatchPreferences preferences)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(preferences);
|
||||||
|
|
||||||
|
IAdvancedDbCollectionView<CultivateProject> projects = await GetProjectCollectionAsync().ConfigureAwait(false);
|
||||||
|
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
|
||||||
|
|
||||||
|
CultivateProject project = projects.CurrentItem;
|
||||||
|
|
||||||
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
|
project.AvatarPropertyBatchCultivatePreferencesJson = JsonSerializer.Serialize(preferences, JsonOptions.Default);
|
||||||
|
cultivationRepository.Update(project);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask RemoveProjectAsync(CultivateProject project)
|
public async ValueTask RemoveProjectAsync(CultivateProject project)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(projects);
|
ArgumentNullException.ThrowIfNull(projects);
|
||||||
@@ -280,8 +507,10 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RecursiveAddMaterialIngredientsByMaterialId(CultivateProject cultivateProject, ICultivationMetadataContext context, Dictionary<uint, StatisticsCultivateItem> resultItems, MaterialId materialId)
|
private static void RecursiveAddMaterialIngredientsByMaterialId(CultivateProject cultivateProject, ICultivationMetadataContext context, Dictionary<uint, StatisticsCultivateItem> resultItems, MaterialId materialId, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (materialId == 104003U)
|
if (materialId == 104003U)
|
||||||
{
|
{
|
||||||
foreach (ref readonly MaterialId xpBookId in (ReadOnlySpan<MaterialId>)[104001U, 104002U])
|
foreach (ref readonly MaterialId xpBookId in (ReadOnlySpan<MaterialId>)[104001U, 104002U])
|
||||||
@@ -297,10 +526,165 @@ internal sealed partial class CultivationService : ICultivationService
|
|||||||
{
|
{
|
||||||
foreach (ref readonly IdCount ingredient in combine.Materials.AsSpan())
|
foreach (ref readonly IdCount ingredient in combine.Materials.AsSpan())
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
ref StatisticsCultivateItem? ingredientItem = ref CollectionsMarshal.GetValueRefOrAddDefault(resultItems, ingredient.Id, out _);
|
ref StatisticsCultivateItem? ingredientItem = ref CollectionsMarshal.GetValueRefOrAddDefault(resultItems, ingredient.Id, out _);
|
||||||
ingredientItem ??= StatisticsCultivateItem.Create(context.GetMaterial(ingredient.Id), cultivateProject.ServerTimeZoneOffset);
|
ingredientItem ??= StatisticsCultivateItem.Create(context.GetMaterial(ingredient.Id), cultivateProject.ServerTimeZoneOffset);
|
||||||
RecursiveAddMaterialIngredientsByMaterialId(cultivateProject, context, resultItems, ingredient.Id);
|
RecursiveAddMaterialIngredientsByMaterialId(cultivateProject, context, resultItems, ingredient.Id, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyStatisticsConsumerMenuLines(
|
||||||
|
Dictionary<uint, StatisticsCultivateItem> resultItems,
|
||||||
|
Guid projectId,
|
||||||
|
ICultivationMetadataContext context,
|
||||||
|
ICultivationRepository cultivationRepository,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
ImmutableArray<(CultivateEntry Entry, CultivateItem Item)> pairs = cultivationRepository.GetCultivateEntryItemPairsByProjectId(projectId);
|
||||||
|
ImmutableArray<CultivateEntry> projectEntries = cultivationRepository.GetCultivateEntryImmutableArrayByProjectId(projectId);
|
||||||
|
Dictionary<Guid, CultivateEntry> entryByInnerId = new(projectEntries.Length);
|
||||||
|
foreach (ref readonly CultivateEntry e in projectEntries.AsSpan())
|
||||||
|
{
|
||||||
|
entryByInnerId[e.InnerId] = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<uint, List<(string SortKey, StatisticsConsumerMenuLine Line)>> unfinishedRowsByMaterial = [];
|
||||||
|
|
||||||
|
foreach ((CultivateEntry entry, CultivateItem item) in pairs.AsSpan())
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (item.IsFinished)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string sortKey = $"{FormatStatisticsConsumerEntryName(entry, context, entryByInnerId)}×{item.Count}";
|
||||||
|
StatisticsConsumerMenuLine line = CreateStatisticsConsumerMenuLine(entry, item, context, entryByInnerId);
|
||||||
|
ref List<(string SortKey, StatisticsConsumerMenuLine Line)>? list = ref CollectionsMarshal.GetValueRefOrAddDefault(unfinishedRowsByMaterial, item.ItemId, out _);
|
||||||
|
list ??= [];
|
||||||
|
list.Add((sortKey, line));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((uint materialId, StatisticsCultivateItem stat) in resultItems)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (MaterialIds.IsExcludedFromStatisticsConsumerMenu(materialId))
|
||||||
|
{
|
||||||
|
stat.StatisticsConsumerMenuLines = ImmutableArray.Create(StatisticsConsumerMenuLine.Plain(SH.ViewPageCultivationStatisticsConsumerMenuExcluded));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unfinishedRowsByMaterial.TryGetValue(materialId, out List<(string SortKey, StatisticsConsumerMenuLine Line)>? rows))
|
||||||
|
{
|
||||||
|
rows.Sort(static (a, b) => StringComparer.Ordinal.Compare(a.SortKey, b.SortKey));
|
||||||
|
stat.StatisticsConsumerMenuLines = ImmutableArray.CreateRange(rows.ConvertAll(static r => r.Line));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stat.StatisticsConsumerMenuLines = ImmutableArray.Create(StatisticsConsumerMenuLine.Plain(SH.ViewPageCultivationStatisticsUnfinishedConsumersEmptyList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StatisticsConsumerMenuLine CreateStatisticsConsumerMenuLine(
|
||||||
|
CultivateEntry entry,
|
||||||
|
CultivateItem item,
|
||||||
|
ICultivationMetadataContext context,
|
||||||
|
Dictionary<Guid, CultivateEntry> entryByInnerId)
|
||||||
|
{
|
||||||
|
// 展示用量:全角括号比「×数量」更利落,且与中文混排更协调。
|
||||||
|
string countSuffix = $"\uFF08{item.Count}\uFF09";
|
||||||
|
|
||||||
|
switch (entry.Type)
|
||||||
|
{
|
||||||
|
case CultivateType.AvatarAndSkill:
|
||||||
|
{
|
||||||
|
ModelItem avatarItem = context.GetAvatarItem(entry.Id);
|
||||||
|
return StatisticsConsumerMenuLine.SingleIcon(avatarItem.Icon, avatarItem.Quality, avatarItem.Name, countSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CultivateType.Weapon:
|
||||||
|
{
|
||||||
|
ModelItem weaponItem = context.GetWeaponItem(entry.Id);
|
||||||
|
if (entry.RelatedEntryId is Guid relatedId
|
||||||
|
&& entryByInnerId.TryGetValue(relatedId, out CultivateEntry? related)
|
||||||
|
&& related.Type is CultivateType.AvatarAndSkill)
|
||||||
|
{
|
||||||
|
ModelItem avatarItem = context.GetAvatarItem(related.Id);
|
||||||
|
return StatisticsConsumerMenuLine.AvatarAndWeapon(
|
||||||
|
avatarItem.Icon,
|
||||||
|
avatarItem.Quality,
|
||||||
|
avatarItem.Name,
|
||||||
|
weaponItem.Icon,
|
||||||
|
weaponItem.Quality,
|
||||||
|
weaponItem.Name,
|
||||||
|
countSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatisticsConsumerMenuLine.SingleIcon(weaponItem.Icon, weaponItem.Quality, weaponItem.Name, countSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return StatisticsConsumerMenuLine.Plain($"{Material.Default.Name}{countSuffix}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatStatisticsConsumerEntryName(
|
||||||
|
CultivateEntry entry,
|
||||||
|
ICultivationMetadataContext context,
|
||||||
|
Dictionary<Guid, CultivateEntry> entryByInnerId)
|
||||||
|
{
|
||||||
|
return entry.Type switch
|
||||||
|
{
|
||||||
|
CultivateType.AvatarAndSkill => context.GetAvatarItem(entry.Id).Name,
|
||||||
|
CultivateType.Weapon => FormatStatisticsConsumerWeaponEntryName(entry, context, entryByInnerId),
|
||||||
|
_ => Material.Default.Name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料统计右键「未完成」:武器条目在有关联角色条目时展示「角色名·武器名」,否则(含历史无 RelatedEntryId)仅武器名。
|
||||||
|
/// </summary>
|
||||||
|
private static string FormatStatisticsConsumerWeaponEntryName(
|
||||||
|
CultivateEntry entry,
|
||||||
|
ICultivationMetadataContext context,
|
||||||
|
Dictionary<Guid, CultivateEntry> entryByInnerId)
|
||||||
|
{
|
||||||
|
string weaponName = context.GetWeaponItem(entry.Id).Name;
|
||||||
|
if (entry.RelatedEntryId is not Guid relatedId)
|
||||||
|
{
|
||||||
|
return weaponName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entryByInnerId.TryGetValue(relatedId, out CultivateEntry? related) || related.Type is not CultivateType.AvatarAndSkill)
|
||||||
|
{
|
||||||
|
return weaponName;
|
||||||
|
}
|
||||||
|
|
||||||
|
string avatarName = context.GetAvatarItem(related.Id).Name;
|
||||||
|
return $"{avatarName}·{weaponName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无材料的「已满配」角色占位行不在养成列表展示(仍为武器的 RelatedEntryId 解析目标)。
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsHiddenAssociationOnlyAvatarEntry(CultivateEntry entry, int cultivateItemCount)
|
||||||
|
{
|
||||||
|
if (entry.Type is not CultivateType.AvatarAndSkill || cultivateItemCount != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.LevelInformation is not CultivateEntryLevelInformation li)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return li.AvatarLevelFrom == li.AvatarLevelTo
|
||||||
|
&& li.SkillALevelFrom == li.SkillALevelTo
|
||||||
|
&& li.SkillELevelFrom == li.SkillELevelTo
|
||||||
|
&& li.SkillQLevelFrom == li.SkillQLevelTo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
|
internal readonly record struct CultivationStatisticsMergeOptions(
|
||||||
|
bool MergeUpgradeMaterials,
|
||||||
|
bool TalentSynthCritTenPercent,
|
||||||
|
bool WeeklyBossMaterialInterchange);
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Intrinsic;
|
||||||
|
using Snap.Hutao.Model.Metadata;
|
||||||
|
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||||
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
|
internal static class CultivationStatisticsSurplusMerge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 元数据 <c>Combine.Type</c>:1 角色与武器培养素材(野怪等)、2 武器突破、3 角色天赋;与此三类对应的合成均支持 10% 暴击期望。
|
||||||
|
/// </summary>
|
||||||
|
private static bool CombineTypeSupportsSynthCritTenPercent(uint combineType)
|
||||||
|
{
|
||||||
|
return combineType is 1U or 2U or 3U;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Apply(Dictionary<uint, StatisticsCultivateItem> items, ICultivationMetadataContext context, CultivationStatisticsMergeOptions options)
|
||||||
|
{
|
||||||
|
if (!options.MergeUpgradeMaterials || items.Count is 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool talentCrit = options.TalentSynthCritTenPercent;
|
||||||
|
|
||||||
|
List<Combine> eligibleCombines = [];
|
||||||
|
foreach (Combine combine in context.ResultMaterialIdCombineMap.Values)
|
||||||
|
{
|
||||||
|
if (combine.RecipeType is not RecipeType.RECIPE_TYPE_COMBINE)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combine.Materials.Length is not 1 || combine.Materials[0].Count is not 3 || combine.Result.Count is not 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
eligibleCombines.Add(combine);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<uint, double> virtualAmount = new(items.Count);
|
||||||
|
foreach ((uint id, StatisticsCultivateItem item) in items)
|
||||||
|
{
|
||||||
|
virtualAmount[id] = item.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (IGrouping<uint, Combine> group in eligibleCombines.GroupBy(static c => c.SubType))
|
||||||
|
{
|
||||||
|
List<Combine> groupCombines = [.. group];
|
||||||
|
HashSet<uint> ids = [];
|
||||||
|
foreach (Combine combine in groupCombines)
|
||||||
|
{
|
||||||
|
_ = ids.Add(combine.Result.Id);
|
||||||
|
_ = ids.Add(combine.Materials[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint[] sortedIds = [.. ids.OrderBy(id => GetRankLevel(context, id)).ThenBy(id => id)];
|
||||||
|
|
||||||
|
foreach (uint id in sortedIds)
|
||||||
|
{
|
||||||
|
Combine? upward = FindUpwardCombine(groupCombines, id);
|
||||||
|
if (upward is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!virtualAmount.TryGetValue(id, out double virt))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint need = items.TryGetValue(id, out StatisticsCultivateItem? row) ? row.Count : 0U;
|
||||||
|
double surplus = Math.Max(0D, virt - need);
|
||||||
|
long crafts = (long)(surplus / 3D);
|
||||||
|
if (crafts <= 0L)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double multiplier = talentCrit && CombineTypeSupportsSynthCritTenPercent(upward.Type) ? 1.1D : 1D;
|
||||||
|
double produced = crafts * multiplier;
|
||||||
|
uint resultId = upward.Result.Id;
|
||||||
|
|
||||||
|
virtualAmount[id] = virt - (crafts * 3L);
|
||||||
|
|
||||||
|
if (!virtualAmount.TryGetValue(resultId, out double atResult))
|
||||||
|
{
|
||||||
|
atResult = items.TryGetValue(resultId, out StatisticsCultivateItem? r) ? r.Current : 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualAmount[resultId] = atResult + produced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((uint id, StatisticsCultivateItem item) in items)
|
||||||
|
{
|
||||||
|
if (virtualAmount.TryGetValue(id, out double v))
|
||||||
|
{
|
||||||
|
item.MergeAdjustedCurrent = (uint)Math.Clamp(Math.Floor(v + 1e-6), 0D, uint.MaxValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Combine? FindUpwardCombine(List<Combine> groupCombines, uint ingredientId)
|
||||||
|
{
|
||||||
|
foreach (Combine combine in groupCombines)
|
||||||
|
{
|
||||||
|
if (combine.Materials is [{ Id: var mid, Count: 3 }] && mid == ingredientId)
|
||||||
|
{
|
||||||
|
return combine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QualityType GetRankLevel(ICultivationMetadataContext context, uint materialId)
|
||||||
|
{
|
||||||
|
return context.GetMaterial(materialId).RankLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料统计:同一周本 Boss 材料池内,将超出需求的虚拟持有量 1:1 调配给池内缺口(不计异梦溶媒消耗)。
|
||||||
|
/// </summary>
|
||||||
|
internal static class CultivationStatisticsWeeklyBossInterchange
|
||||||
|
{
|
||||||
|
public static void Apply(
|
||||||
|
Dictionary<uint, StatisticsCultivateItem> items,
|
||||||
|
ImmutableArray<ImmutableArray<MaterialId>> interchangeGroups,
|
||||||
|
bool enabled)
|
||||||
|
{
|
||||||
|
if (!enabled || interchangeGroups.IsDefault || interchangeGroups.IsEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ImmutableArray<MaterialId> group in interchangeGroups)
|
||||||
|
{
|
||||||
|
ApplySingleGroup(items, group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplySingleGroup(Dictionary<uint, StatisticsCultivateItem> items, ImmutableArray<MaterialId> group)
|
||||||
|
{
|
||||||
|
List<uint> poolIds = [];
|
||||||
|
foreach (MaterialId mid in group)
|
||||||
|
{
|
||||||
|
uint id = mid;
|
||||||
|
if (items.ContainsKey(id))
|
||||||
|
{
|
||||||
|
poolIds.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poolIds.Count < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<uint, uint> virt = new(poolIds.Count);
|
||||||
|
foreach (uint id in poolIds)
|
||||||
|
{
|
||||||
|
StatisticsCultivateItem it = items[id];
|
||||||
|
virt[id] = it.MergeAdjustedCurrent ?? it.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
uint? donor = null;
|
||||||
|
uint maxSurplus = 0U;
|
||||||
|
foreach (uint id in poolIds)
|
||||||
|
{
|
||||||
|
uint v = virt[id];
|
||||||
|
uint need = items[id].Count;
|
||||||
|
if (v > need && v - need > maxSurplus)
|
||||||
|
{
|
||||||
|
maxSurplus = v - need;
|
||||||
|
donor = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint? receiver = null;
|
||||||
|
uint maxDeficit = 0U;
|
||||||
|
foreach (uint id in poolIds)
|
||||||
|
{
|
||||||
|
if (id == donor)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint v = virt[id];
|
||||||
|
uint need = items[id].Count;
|
||||||
|
if (v < need && need - v > maxDeficit)
|
||||||
|
{
|
||||||
|
maxDeficit = need - v;
|
||||||
|
receiver = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (donor is null || receiver is null || maxSurplus is 0U || maxDeficit is 0U)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
virt[donor.Value]--;
|
||||||
|
virt[receiver.Value]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (uint id in poolIds)
|
||||||
|
{
|
||||||
|
StatisticsCultivateItem it = items[id];
|
||||||
|
uint baseline = it.MergeAdjustedCurrent ?? it.Current;
|
||||||
|
uint finalV = virt[id];
|
||||||
|
if (finalV != baseline)
|
||||||
|
{
|
||||||
|
it.WeeklyBossInterchangeAdjustedCurrent = finalV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||||
|
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
|
internal interface IAvatarPropertyBatchCultivateService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see langword="null"/> when the baseline dialog is dismissed without confirmation.
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<BatchCultivateResult?> ExecuteAsync(SummaryFactoryMetadataContext metadataContext, ImmutableArray<AvatarView> targetAvatars, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ internal interface ICultivationMetadataContext : IMetadataContext,
|
|||||||
IMetadataDictionaryIdMaterialSource,
|
IMetadataDictionaryIdMaterialSource,
|
||||||
IMetadataDictionaryIdAvatarSource,
|
IMetadataDictionaryIdAvatarSource,
|
||||||
IMetadataDictionaryIdWeaponSource,
|
IMetadataDictionaryIdWeaponSource,
|
||||||
IMetadataDictionaryResultMaterialIdCombineSource
|
IMetadataDictionaryResultMaterialIdCombineSource,
|
||||||
|
IMetadataWeeklyBossMaterialInterchangeGroupsSource
|
||||||
{
|
{
|
||||||
Item GetAvatarItem(AvatarId avatarId);
|
Item GetAvatarItem(AvatarId avatarId);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ internal interface ICultivationRepository : IRepository<CultivateEntryLevelInfor
|
|||||||
|
|
||||||
ObservableCollection<CultivateProject> GetCultivateProjectCollection();
|
ObservableCollection<CultivateProject> GetCultivateProjectCollection();
|
||||||
|
|
||||||
|
ImmutableArray<Guid> GetCultivateProjectInnerIds();
|
||||||
|
|
||||||
CultivateProject? GetCultivateProjectById(Guid projectId);
|
CultivateProject? GetCultivateProjectById(Guid projectId);
|
||||||
|
|
||||||
void AddCultivateEntry(CultivateEntry entry);
|
void AddCultivateEntry(CultivateEntry entry);
|
||||||
@@ -43,5 +45,16 @@ internal interface ICultivationRepository : IRepository<CultivateEntryLevelInfor
|
|||||||
|
|
||||||
ImmutableArray<CultivateEntry> GetCultivateEntryImmutableArrayByProjectIdAndItemId(Guid projectId, uint itemId);
|
ImmutableArray<CultivateEntry> GetCultivateEntryImmutableArrayByProjectIdAndItemId(Guid projectId, uint itemId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析当前计划中指定角色的养成条目(类型为 CultivateType.AvatarAndSkill)。
|
||||||
|
/// 若存在多条历史记录,取最近插入数据库的一条(按 SQLite rowid 倒序)。
|
||||||
|
/// </summary>
|
||||||
|
Guid? TryGetAvatarCultivateEntryInnerId(Guid projectId, uint avatarId);
|
||||||
|
|
||||||
Guid GetCultivateProjectIdByEntryId(Guid entryId);
|
Guid GetCultivateProjectIdByEntryId(Guid entryId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联表查询某计划下所有养成物品及其所属条目(用于材料统计未勾选条目等)。
|
||||||
|
/// </summary>
|
||||||
|
ImmutableArray<(CultivateEntry Entry, CultivateItem Item)> GetCultivateEntryItemPairsByProjectId(Guid projectId);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.Database;
|
using Snap.Hutao.Core.Database;
|
||||||
|
using Snap.Hutao.Model.Cultivation;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Service.Cultivation.Consumption;
|
using Snap.Hutao.Service.Cultivation.Consumption;
|
||||||
using Snap.Hutao.ViewModel.Cultivation;
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
@@ -17,17 +18,40 @@ internal interface ICultivationService
|
|||||||
|
|
||||||
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntryCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
|
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntryCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
|
||||||
|
|
||||||
ValueTask<StatisticsCultivateItemCollection> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token);
|
ValueTask<StatisticsCultivateItemCollection> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context, CultivationStatisticsMergeOptions mergeOptions, CancellationToken token);
|
||||||
|
|
||||||
ValueTask<ResinStatistics> GetResinStatisticsAsync(StatisticsCultivateItemCollection statisticsCultivateItems, CancellationToken token);
|
ValueTask<ResinStatistics> GetResinStatisticsAsync(StatisticsCultivateItemCollection statisticsCultivateItems, CancellationToken token);
|
||||||
|
|
||||||
ValueTask RemoveCultivateEntryAsync(Guid entryId);
|
ValueTask RemoveCultivateEntryAsync(Guid entryId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除当前选中养成计划中角色与武器类型的全部养成条目。
|
||||||
|
/// </summary>
|
||||||
|
ValueTask RemoveAvatarAndWeaponEntriesForCurrentProjectAsync();
|
||||||
|
|
||||||
ValueTask RemoveProjectAsync(CultivateProject project);
|
ValueTask RemoveProjectAsync(CultivateProject project);
|
||||||
|
|
||||||
ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption);
|
ValueTask<ConsumptionSaveResult> SaveConsumptionAsync(InputConsumption inputConsumption);
|
||||||
|
|
||||||
|
ValueTask<Guid?> TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前计划中尚无该角色的养成条目时,插入一条无材料行的角色占位条目(等级信息与 delta 一致),供武器 RelatedEntryId 关联。
|
||||||
|
/// 若已存在角色条目则返回其 InnerId。
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<Guid?> EnsureAvatarAssociationStubAsync(uint avatarId, LevelInformation levelInformation);
|
||||||
|
|
||||||
void SaveCultivateItem(CultivateItemView item);
|
void SaveCultivateItem(CultivateItemView item);
|
||||||
|
|
||||||
ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project);
|
ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取当前选中养成计划下「我的角色」批量同步对话框的已保存选项;未保存时返回 <see langword="null"/>。
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<CultivateProjectAvatarPropertyBatchPreferences?> GetAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将批量同步对话框的选项写入当前选中养成计划。
|
||||||
|
/// </summary>
|
||||||
|
ValueTask SaveAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync(CultivateProjectAvatarPropertyBatchPreferences preferences);
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,9 @@ internal sealed class InputConsumption
|
|||||||
public required LevelInformation LevelInformation { get; init; }
|
public required LevelInformation LevelInformation { get; init; }
|
||||||
|
|
||||||
public required ConsumptionSaveStrategyKind Strategy { get; init; }
|
public required ConsumptionSaveStrategyKind Strategy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 武器条目关联的养成角色条目的主键(自引用外键,可为空)。
|
||||||
|
/// </summary>
|
||||||
|
public Guid? RelatedEntryId { get; init; }
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ internal sealed partial class StatisticsCultivateItemCollection : ICollection<St
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MaterialIdComparer.Shared.Compare(x.Inner.Id, y.Inner.Id);
|
return StatisticsCultivateItemComparer.CompareCore(x, y);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,53 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.Collection.Generic;
|
|
||||||
using Snap.Hutao.Model.Primitive;
|
using Snap.Hutao.Model.Primitive;
|
||||||
using Snap.Hutao.ViewModel.Cultivation;
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Cultivation;
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
internal sealed class StatisticsCultivateItemComparer : DelegatingPropertyComparer<StatisticsCultivateItem, MaterialId>
|
/// <summary>
|
||||||
|
/// 材料统计默认顺序:与原先一致按 <see cref="MaterialIdComparer"/>,但当两项均为「角色培养素材」时先按 <see cref="Snap.Hutao.Model.Metadata.Item.Material.RankLevel"/> 再按物品 Id。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class StatisticsCultivateItemComparer : IComparer<StatisticsCultivateItem>
|
||||||
{
|
{
|
||||||
private static readonly LazySlim<StatisticsCultivateItemComparer> LazyShared = new(() => new());
|
private static readonly LazySlim<StatisticsCultivateItemComparer> LazyShared = new(() => new());
|
||||||
|
|
||||||
private StatisticsCultivateItemComparer()
|
private StatisticsCultivateItemComparer()
|
||||||
: base(static i => i.Inner.Id, MaterialIdComparer.Shared)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StatisticsCultivateItemComparer Shared { get => LazyShared.Value; }
|
public static StatisticsCultivateItemComparer Shared { get => LazyShared.Value; }
|
||||||
|
|
||||||
|
public int Compare(StatisticsCultivateItem? x, StatisticsCultivateItem? y)
|
||||||
|
{
|
||||||
|
return (x, y) switch
|
||||||
|
{
|
||||||
|
(null, not null) => -1,
|
||||||
|
(not null, null) => 1,
|
||||||
|
(null, null) => 0,
|
||||||
|
(not null, not null) => CompareCore(x, y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int CompareCore(StatisticsCultivateItem x, StatisticsCultivateItem y)
|
||||||
|
{
|
||||||
|
string? tx = x.Inner.TypeDescription;
|
||||||
|
string? ty = y.Inner.TypeDescription;
|
||||||
|
if (IsCharacterLevelUpMaterial(tx) && IsCharacterLevelUpMaterial(ty))
|
||||||
|
{
|
||||||
|
int rank = x.Inner.RankLevel.CompareTo(y.Inner.RankLevel);
|
||||||
|
if (rank is not 0)
|
||||||
|
{
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialIdComparer.Shared.Compare(x.Inner.Id, y.Inner.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCharacterLevelUpMaterial(string? typeDescription)
|
||||||
|
{
|
||||||
|
return typeDescription == SH.ModelMetadataMaterialCharacterLevelUpMaterial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Intrinsic;
|
||||||
|
using Snap.Hutao.Model.Metadata;
|
||||||
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Cultivation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自 Combine 列表解析周本材料异梦转化互通组:配方为 Type=9、CONVERT、产物×1、材料为「异梦溶媒」+ 另一周本材料各×1。
|
||||||
|
/// </summary>
|
||||||
|
internal static class WeeklyBossMaterialInterchangeGroupsBuilder
|
||||||
|
{
|
||||||
|
/// <summary>异梦溶媒 Id(转化消耗,统计虚拟调配时不扣溶媒,仅利用池内 1:1 等价)。</summary>
|
||||||
|
private const uint DreamSolventMaterialId = 113021U;
|
||||||
|
|
||||||
|
public static ImmutableArray<ImmutableArray<MaterialId>> Build(ImmutableArray<Combine> combines)
|
||||||
|
{
|
||||||
|
Dictionary<MaterialId, HashSet<MaterialId>> adjacency = [];
|
||||||
|
|
||||||
|
foreach (Combine combine in combines)
|
||||||
|
{
|
||||||
|
if (combine.Type is not 9U)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combine.RecipeType is not RecipeType.RECIPE_TYPE_CONVERT)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combine.Materials.Length is not 2 || combine.Result.Count is not 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialId resultId = combine.Result.Id;
|
||||||
|
MaterialId? otherMaterial = null;
|
||||||
|
foreach (ref readonly IdCount m in combine.Materials.AsSpan())
|
||||||
|
{
|
||||||
|
if (m.Id == DreamSolventMaterialId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.Count is not 1)
|
||||||
|
{
|
||||||
|
otherMaterial = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
otherMaterial = m.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherMaterial is null || otherMaterial.Value == resultId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddUndirectedEdge(adjacency, resultId, otherMaterial.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToComponents(adjacency);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddUndirectedEdge(Dictionary<MaterialId, HashSet<MaterialId>> adjacency, MaterialId a, MaterialId b)
|
||||||
|
{
|
||||||
|
if (!adjacency.TryGetValue(a, out HashSet<MaterialId>? setA))
|
||||||
|
{
|
||||||
|
setA = [];
|
||||||
|
adjacency[a] = setA;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adjacency.TryGetValue(b, out HashSet<MaterialId>? setB))
|
||||||
|
{
|
||||||
|
setB = [];
|
||||||
|
adjacency[b] = setB;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = setA.Add(b);
|
||||||
|
_ = setB.Add(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableArray<ImmutableArray<MaterialId>> ToComponents(Dictionary<MaterialId, HashSet<MaterialId>> adjacency)
|
||||||
|
{
|
||||||
|
if (adjacency.Count is 0)
|
||||||
|
{
|
||||||
|
return ImmutableArray<ImmutableArray<MaterialId>>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<MaterialId> visited = [];
|
||||||
|
ImmutableArray<ImmutableArray<MaterialId>>.Builder groups = ImmutableArray.CreateBuilder<ImmutableArray<MaterialId>>();
|
||||||
|
|
||||||
|
foreach (MaterialId start in adjacency.Keys)
|
||||||
|
{
|
||||||
|
if (visited.Contains(start))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MaterialId> component = [];
|
||||||
|
Queue<MaterialId> queue = new();
|
||||||
|
queue.Enqueue(start);
|
||||||
|
visited.Add(start);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
MaterialId id = queue.Dequeue();
|
||||||
|
component.Add(id);
|
||||||
|
|
||||||
|
if (!adjacency.TryGetValue(id, out HashSet<MaterialId>? neighbors))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MaterialId n in neighbors)
|
||||||
|
{
|
||||||
|
if (visited.Add(n))
|
||||||
|
{
|
||||||
|
queue.Enqueue(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.Count >= 2)
|
||||||
|
{
|
||||||
|
component.Sort(MaterialIdComparer.Shared);
|
||||||
|
groups.Add(component.ToImmutableArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.ToImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,15 @@ internal sealed partial class GachaLogService : IGachaLogService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IAdvancedDbCollectionView<GachaArchive>> RefreshArchiveCollectionAsync()
|
||||||
|
{
|
||||||
|
using (await archivesLock.LockAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
archives = null;
|
||||||
|
return archives = gachaLogRepository.GetGachaArchiveCollection().ToAdvancedDbCollectionView(serviceProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<GachaStatistics> GetStatisticsAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
|
public async ValueTask<GachaStatistics> GetStatisticsAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
|
||||||
{
|
{
|
||||||
using (ValueStopwatch.MeasureExecution(logger))
|
using (ValueStopwatch.MeasureExecution(logger))
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ internal interface IGachaLogService
|
|||||||
ValueTask RemoveArchiveAsync(GachaArchive archive);
|
ValueTask RemoveArchiveAsync(GachaArchive archive);
|
||||||
|
|
||||||
ValueTask<IAdvancedDbCollectionView<GachaArchive>> GetArchiveCollectionAsync();
|
ValueTask<IAdvancedDbCollectionView<GachaArchive>> GetArchiveCollectionAsync();
|
||||||
|
|
||||||
|
ValueTask<IAdvancedDbCollectionView<GachaArchive>> RefreshArchiveCollectionAsync();
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
|
|||||||
{
|
{
|
||||||
private readonly AsyncKeyedLock<string> repoLock = new();
|
private readonly AsyncKeyedLock<string> repoLock = new();
|
||||||
private readonly BackgroundActivityOptions backgroundActivityOptions;
|
private readonly BackgroundActivityOptions backgroundActivityOptions;
|
||||||
|
private readonly ILogger<GitRepositoryService> logger;
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
|
|||||||
}
|
}
|
||||||
catch (Exception first)
|
catch (Exception first)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning(first, "[Metadata] Failed to update existing repository, fallback to reclone: Directory={Directory}, Url={Url}", directory, info.HttpsUrl.OriginalString);
|
||||||
exceptions.Add(first);
|
exceptions.Add(first);
|
||||||
return EnsureRepository(activity, directory, info, true);
|
return EnsureRepository(activity, directory, info, true);
|
||||||
}
|
}
|
||||||
@@ -110,6 +112,14 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
|
|||||||
{
|
{
|
||||||
// Increase & decrease count in the same method, so that crash in the middle can correctly count as failure.
|
// Increase & decrease count in the same method, so that crash in the middle can correctly count as failure.
|
||||||
RepositoryAffinity.IncreaseFailure(info);
|
RepositoryAffinity.IncreaseFailure(info);
|
||||||
|
|
||||||
|
// Debug: Log the initial state
|
||||||
|
bool isRepoValid = Repository.IsValid(directory);
|
||||||
|
bool directoryExists = Directory.Exists(directory);
|
||||||
|
|
||||||
|
logger.LogInformation("[Metadata] Checking repository: Directory={Directory}, Exists={Exists}, IsValid={IsValid}, ForceInvalid={ForceInvalid}",
|
||||||
|
directory, directoryExists, isRepoValid, forceInvalid);
|
||||||
|
|
||||||
FetchOptions fetchOptions = new()
|
FetchOptions fetchOptions = new()
|
||||||
{
|
{
|
||||||
Depth = 1,
|
Depth = 1,
|
||||||
@@ -142,10 +152,18 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
|
|||||||
CertificateCheck = static (cert, valid, host) => true,
|
CertificateCheck = static (cert, valid, host) => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (forceInvalid || !Repository.IsValid(directory))
|
if (forceInvalid || !isRepoValid)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(directory))
|
// Debug: Log why we're cloning
|
||||||
|
string reason = forceInvalid
|
||||||
|
? SH.ServiceGitRepositoryCloneReasonForceInvalid
|
||||||
|
: SH.ServiceGitRepositoryCloneReasonInvalidRepo;
|
||||||
|
logger.LogInformation("[Metadata] Cloning repository: Reason={Reason}, Url={Url}", reason, info.HttpsUrl.OriginalString);
|
||||||
|
activity.Update(taskContext, reason, false, false, false, false);
|
||||||
|
|
||||||
|
if (directoryExists)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("[Metadata] Deleting existing directory before clone");
|
||||||
Directory.SetReadOnly(directory, false);
|
Directory.SetReadOnly(directory, false);
|
||||||
Directory.Delete(directory, true);
|
Directory.Delete(directory, true);
|
||||||
}
|
}
|
||||||
@@ -154,9 +172,15 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
|
|||||||
{
|
{
|
||||||
Checkout = true,
|
Checkout = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.LogInformation("[Metadata] Clone completed successfully");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Debug: Log that we're updating
|
||||||
|
logger.LogInformation("[Metadata] Updating existing repository");
|
||||||
|
activity.Update(taskContext, SH.ServiceGitRepositoryUpdatingExisting, false, false, false, false);
|
||||||
|
|
||||||
// We need to ensure local repo is up to date
|
// We need to ensure local repo is up to date
|
||||||
using (Repository repo = new(directory))
|
using (Repository repo = new(directory))
|
||||||
{
|
{
|
||||||
@@ -177,17 +201,20 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
|
|||||||
repo.Network.Remotes.Update("origin", remote => remote.Url = info.HttpsUrl.OriginalString);
|
repo.Network.Remotes.Update("origin", remote => remote.Url = info.HttpsUrl.OriginalString);
|
||||||
repo.RemoveUntrackedFiles();
|
repo.RemoveUntrackedFiles();
|
||||||
fetchOptions.UpdateFetchHead = false;
|
fetchOptions.UpdateFetchHead = false;
|
||||||
Commands.Fetch(repo, repo.Head.RemoteName, Array.Empty<string>(), fetchOptions, default);
|
Commands.Fetch(repo, "origin", Array.Empty<string>(), fetchOptions, default);
|
||||||
|
|
||||||
// Manually patch .git/shallow file
|
// Manually patch .git/shallow file
|
||||||
File.WriteAllText(Path.Combine(directory, ".git//shallow"), string.Join("", repo.Branches.Where(static branch => branch.IsRemote).Select(static branch => $"{branch.Tip.Sha}\n")));
|
File.WriteAllText(Path.Combine(directory, ".git", "shallow"), string.Join("", repo.Branches.Where(static branch => branch.IsRemote).Select(static branch => $"{branch.Tip.Sha}\n")));
|
||||||
|
|
||||||
Branch remoteBranch = repo.Branches["origin/main"];
|
Branch remoteBranch = repo.Branches["origin/main"];
|
||||||
Branch localBranch = repo.Branches["main"] ?? repo.CreateBranch("main", remoteBranch.Tip);
|
Branch localBranch = repo.Branches["main"] ?? repo.CreateBranch("main", remoteBranch.Tip);
|
||||||
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
|
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
|
||||||
|
Commands.Checkout(repo, localBranch);
|
||||||
repo.Reset(ResetMode.Hard, remoteBranch.Tip);
|
repo.Reset(ResetMode.Hard, remoteBranch.Tip);
|
||||||
repo.RemoveUntrackedFiles();
|
repo.RemoveUntrackedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("[Metadata] Update completed successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
RepositoryAffinity.DecreaseFailure(info);
|
RepositoryAffinity.DecreaseFailure(info);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ internal sealed partial class InventoryService : IInventoryService
|
|||||||
private readonly PromotionDeltaFactory promotionDeltaFactory;
|
private readonly PromotionDeltaFactory promotionDeltaFactory;
|
||||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||||
private readonly IInventoryRepository inventoryRepository;
|
private readonly IInventoryRepository inventoryRepository;
|
||||||
|
private readonly ICultivationRepository cultivationRepository;
|
||||||
private readonly IUserService userService;
|
private readonly IUserService userService;
|
||||||
private readonly IMessenger messenger;
|
private readonly IMessenger messenger;
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ internal sealed partial class InventoryService : IInventoryService
|
|||||||
{
|
{
|
||||||
case RefreshOptionKind.WebCalculator:
|
case RefreshOptionKind.WebCalculator:
|
||||||
ArgumentNullException.ThrowIfNull(refreshOptions.MetadataContext);
|
ArgumentNullException.ThrowIfNull(refreshOptions.MetadataContext);
|
||||||
return RefreshInventoryByCalculatorAsync(refreshOptions.MetadataContext, refreshOptions.Project);
|
return RefreshInventoryByCalculatorAsync(refreshOptions);
|
||||||
case RefreshOptionKind.EmbeddedYae:
|
case RefreshOptionKind.EmbeddedYae:
|
||||||
ArgumentNullException.ThrowIfNull(refreshOptions.YaeService);
|
ArgumentNullException.ThrowIfNull(refreshOptions.YaeService);
|
||||||
ArgumentNullException.ThrowIfNull(refreshOptions.ViewModelSupportLaunchExecution);
|
ArgumentNullException.ThrowIfNull(refreshOptions.ViewModelSupportLaunchExecution);
|
||||||
@@ -71,8 +72,12 @@ internal sealed partial class InventoryService : IInventoryService
|
|||||||
inventoryRepository.RemoveInventoryItemRangeByProjectId(projectId);
|
inventoryRepository.RemoveInventoryItemRangeByProjectId(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask RefreshInventoryByCalculatorAsync(ICultivationMetadataContext context, CultivateProject project)
|
private async ValueTask RefreshInventoryByCalculatorAsync(RefreshOptions options)
|
||||||
{
|
{
|
||||||
|
ICultivationMetadataContext context = options.MetadataContext!;
|
||||||
|
CultivateProject project = options.Project;
|
||||||
|
bool syncToAllProjects = options.SyncCalculatorInventoryToAllProjects;
|
||||||
|
|
||||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
||||||
{
|
{
|
||||||
messenger.Send(InfoBarMessage.Warning(SH.MustSelectUserAndUid));
|
messenger.Send(InfoBarMessage.Warning(SH.MustSelectUserAndUid));
|
||||||
@@ -97,9 +102,37 @@ internal sealed partial class InventoryService : IInventoryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (batchConsumption is { OverallConsume: { IsDefault: false } items })
|
if (batchConsumption is { OverallConsume: { IsDefault: false } items })
|
||||||
|
{
|
||||||
|
static IEnumerable<InventoryItem> ToInventoryItems(ImmutableArray<Item> consumeItems, Guid projectId)
|
||||||
|
{
|
||||||
|
static uint ToSafeCount(Item item)
|
||||||
|
{
|
||||||
|
long delta = (long)item.Num - item.LackNum;
|
||||||
|
if (delta <= 0)
|
||||||
|
{
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta >= uint.MaxValue ? uint.MaxValue : (uint)delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumeItems.SelectAsArray(static (item, pid) => InventoryItem.From(pid, item.Id, ToSafeCount(item)), projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncToAllProjects)
|
||||||
|
{
|
||||||
|
ImmutableArray<Guid> projectIds = cultivationRepository.GetCultivateProjectInnerIds();
|
||||||
|
foreach (Guid projectId in projectIds.AsSpan())
|
||||||
|
{
|
||||||
|
inventoryRepository.RemoveInventoryItemRangeByProjectId(projectId);
|
||||||
|
inventoryRepository.AddInventoryItemRangeByProjectId(ToInventoryItems(items, projectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
inventoryRepository.RemoveInventoryItemRangeByProjectId(project.InnerId);
|
inventoryRepository.RemoveInventoryItemRangeByProjectId(project.InnerId);
|
||||||
inventoryRepository.AddInventoryItemRangeByProjectId(items.SelectAsArray(static (item, project) => InventoryItem.From(project.InnerId, item.Id, (uint)((int)item.Num - item.LackNum)), project));
|
inventoryRepository.AddInventoryItemRangeByProjectId(ToInventoryItems(items, project.InnerId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ internal sealed class RefreshOptions
|
|||||||
|
|
||||||
public required IViewModelSupportLaunchExecution? ViewModelSupportLaunchExecution { get; init; }
|
public required IViewModelSupportLaunchExecution? ViewModelSupportLaunchExecution { get; init; }
|
||||||
|
|
||||||
public static RefreshOptions CreateForWebCalculator(CultivateProject project, ICultivationMetadataContext context)
|
/// <summary>
|
||||||
|
/// 通过养成计算器同步背包时,是否将结果写入所有养成计划(否则仅当前计划)。
|
||||||
|
/// </summary>
|
||||||
|
public bool SyncCalculatorInventoryToAllProjects { get; init; }
|
||||||
|
|
||||||
|
public static RefreshOptions CreateForWebCalculator(CultivateProject project, ICultivationMetadataContext context, bool syncCalculatorInventoryToAllProjects = false)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@@ -33,6 +38,7 @@ internal sealed class RefreshOptions
|
|||||||
MetadataContext = context,
|
MetadataContext = context,
|
||||||
YaeService = default,
|
YaeService = default,
|
||||||
ViewModelSupportLaunchExecution = default,
|
ViewModelSupportLaunchExecution = default,
|
||||||
|
SyncCalculatorInventoryToAllProjects = syncCalculatorInventoryToAllProjects,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Intrinsic;
|
||||||
|
using Snap.Hutao.Model.Metadata;
|
||||||
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Metadata;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combine 元数据中多条配方可共享同一产物 Id(例如元素断片既有 3×碎屑合成,也有用其他元素石与尘的转化)。
|
||||||
|
/// 构建 MaterialId→Combine 映射时必须优先保留「单材料×3→产物×1」的合成台主链,否则后项覆盖前项会导致养成统计无法向下展开原料。
|
||||||
|
/// </summary>
|
||||||
|
internal static class CombineResultMaterialIdMapFactory
|
||||||
|
{
|
||||||
|
public static ImmutableDictionary<MaterialId, Combine> ToImmutableDictionary(ImmutableArray<Combine> combines)
|
||||||
|
{
|
||||||
|
Dictionary<MaterialId, Combine> map = [];
|
||||||
|
|
||||||
|
foreach (Combine current in combines)
|
||||||
|
{
|
||||||
|
MaterialId key = current.Result.Id;
|
||||||
|
if (!map.TryGetValue(key, out Combine? existing))
|
||||||
|
{
|
||||||
|
map[key] = current;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ComparePreference(current, existing) > 0)
|
||||||
|
{
|
||||||
|
map[key] = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.ToImmutableDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>正数表示 <paramref name="incoming"/> 应取代已有项。</summary>
|
||||||
|
private static int ComparePreference(Combine incoming, Combine existing)
|
||||||
|
{
|
||||||
|
return Score(incoming).CompareTo(Score(existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Score(Combine c)
|
||||||
|
{
|
||||||
|
if (c.Materials.Length is 1 && c.Materials[0].Count is 3 && c.Result.Count is 1)
|
||||||
|
{
|
||||||
|
return c.RecipeType switch
|
||||||
|
{
|
||||||
|
RecipeType.RECIPE_TYPE_COMBINE => 300,
|
||||||
|
RecipeType.RECIPE_TYPE_CONVERT => 200,
|
||||||
|
_ => 150,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.RecipeType is RecipeType.RECIPE_TYPE_COMBINE)
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 周本 Boss 掉落材料「异梦转化」互通组(由 Combine 元数据解析)。
|
||||||
|
/// </summary>
|
||||||
|
internal interface IMetadataWeeklyBossMaterialInterchangeGroupsSource : IMetadataContext
|
||||||
|
{
|
||||||
|
ImmutableArray<ImmutableArray<MaterialId>> WeeklyBossMaterialInterchangeGroups { get; set; }
|
||||||
|
}
|
||||||
@@ -211,6 +211,11 @@ internal static class MetadataServiceContextExtension
|
|||||||
{
|
{
|
||||||
dictionaryResultMaterialIdCombineSource.ResultMaterialIdCombineMap = await metadataService.GetResultMaterialIdToCombineMapAsync(token).ConfigureAwait(false);
|
dictionaryResultMaterialIdCombineSource.ResultMaterialIdCombineMap = await metadataService.GetResultMaterialIdToCombineMapAsync(token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context is IMetadataWeeklyBossMaterialInterchangeGroupsSource weeklyBossInterchange)
|
||||||
|
{
|
||||||
|
weeklyBossInterchange.WeeklyBossMaterialInterchangeGroups = await metadataService.GetWeeklyBossMaterialInterchangeGroupsAsync(token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context is IMetadataSupportInitialization supportInitialization)
|
if (context is IMetadataSupportInitialization supportInitialization)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Snap.Hutao.Model.Metadata.Reliquary;
|
|||||||
using Snap.Hutao.Model.Metadata.Tower;
|
using Snap.Hutao.Model.Metadata.Tower;
|
||||||
using Snap.Hutao.Model.Metadata.Weapon;
|
using Snap.Hutao.Model.Metadata.Weapon;
|
||||||
using Snap.Hutao.Model.Primitive;
|
using Snap.Hutao.Model.Primitive;
|
||||||
|
using Snap.Hutao.Service.Cultivation;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Metadata;
|
namespace Snap.Hutao.Service.Metadata;
|
||||||
@@ -216,12 +217,29 @@ internal static class MetadataServiceImmutableDictionaryExtension
|
|||||||
token);
|
token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<ImmutableDictionary<MaterialId, Combine>> GetResultMaterialIdToCombineMapAsync(CancellationToken token = default)
|
public async ValueTask<ImmutableDictionary<MaterialId, Combine>> GetResultMaterialIdToCombineMapAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return metadataService.FromCacheAsDictionaryAsync<MaterialId, Combine>(
|
string cacheKey = $"{nameof(MetadataService)}.Cache.{MetadataFileStrategies.Combine.Name}.Map.{nameof(MaterialId)}.{nameof(Combine)}.PreferThreeToOne";
|
||||||
MetadataFileStrategies.Combine,
|
ImmutableDictionary<MaterialId, Combine>? result = await metadataService.MemoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
||||||
c => c.Result.Id,
|
{
|
||||||
token);
|
ImmutableArray<Combine> array = await metadataService.FromCacheOrFileAsync<Combine>(MetadataFileStrategies.Combine, token).ConfigureAwait(false);
|
||||||
|
return CombineResultMaterialIdMapFactory.ToImmutableDictionary(array);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<ImmutableArray<ImmutableArray<MaterialId>>> GetWeeklyBossMaterialInterchangeGroupsAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
string cacheKey = $"{nameof(MetadataService)}.Cache.{MetadataFileStrategies.Combine.Name}.WeeklyBossMaterialInterchangeGroups";
|
||||||
|
ImmutableArray<ImmutableArray<MaterialId>>? result = await metadataService.MemoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
||||||
|
{
|
||||||
|
ImmutableArray<Combine> array = await metadataService.FromCacheOrFileAsync<Combine>(MetadataFileStrategies.Combine, token).ConfigureAwait(false);
|
||||||
|
return WeeklyBossMaterialInterchangeGroupsBuilder.Build(array);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result ?? ImmutableArray<ImmutableArray<MaterialId>>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValueTask<ImmutableDictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue>(MetadataFileStrategy strategy, CancellationToken token)
|
private ValueTask<ImmutableDictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue>(MetadataFileStrategy strategy, CancellationToken token)
|
||||||
|
|||||||
@@ -34,4 +34,18 @@ internal interface IThirdPartyToolService
|
|||||||
/// <param name="tool">工具信息</param>
|
/// <param name="tool">工具信息</param>
|
||||||
/// <returns>是否已下载</returns>
|
/// <returns>是否已下载</returns>
|
||||||
bool IsToolDownloaded(ToolInfo tool);
|
bool IsToolDownloaded(ToolInfo tool);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地工具信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tool">工具信息</param>
|
||||||
|
/// <returns>本地工具信息,如果不存在则返回null</returns>
|
||||||
|
LocalToolInfo? GetLocalToolInfo(ToolInfo tool);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查工具是否需要更新
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tool">工具信息</param>
|
||||||
|
/// <returns>是否需要更新</returns>
|
||||||
|
bool NeedsUpdate(ToolInfo tool);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,9 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.ThirdPartyTool;
|
namespace Snap.Hutao.Service.ThirdPartyTool;
|
||||||
|
|
||||||
@@ -17,8 +19,9 @@ namespace Snap.Hutao.Service.ThirdPartyTool;
|
|||||||
[Service(ServiceLifetime.Singleton, typeof(IThirdPartyToolService))]
|
[Service(ServiceLifetime.Singleton, typeof(IThirdPartyToolService))]
|
||||||
internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
||||||
{
|
{
|
||||||
private const string ApiBaseUrl = "https://htserver.wdg.cloudns.ch/api";
|
private const string ApiBaseUrl = "https://htserver.wdg12.work/api";
|
||||||
private const string ToolsEndpoint = "/tools";
|
private const string ToolsEndpoint = "/tools";
|
||||||
|
private const string ToolInfoFileName = "tool_info.json";
|
||||||
|
|
||||||
private readonly IHttpClientFactory httpClientFactory;
|
private readonly IHttpClientFactory httpClientFactory;
|
||||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||||
@@ -89,40 +92,31 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
string toolDirectory = GetToolDirectory(tool);
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
Directory.CreateDirectory(toolDirectory);
|
|
||||||
|
|
||||||
int totalFiles = tool.Files.Count;
|
// 如果需要更新,先清理旧文件
|
||||||
int downloadedFiles = 0;
|
if (NeedsUpdate(tool) && Directory.Exists(toolDirectory))
|
||||||
|
{
|
||||||
|
Directory.Delete(toolDirectory, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(toolDirectory);
|
||||||
|
|
||||||
using (HttpClient httpClient = httpClientFactory.CreateClient())
|
using (HttpClient httpClient = httpClientFactory.CreateClient())
|
||||||
{
|
{
|
||||||
foreach (string fileName in tool.Files)
|
if (tool.IsCompressed)
|
||||||
{
|
{
|
||||||
string fileUrl = $"{tool.Url}{fileName}";
|
// 压缩包模式:下载并解压
|
||||||
string localFilePath = Path.Combine(toolDirectory, fileName);
|
await DownloadAndExtractCompressedToolAsync(httpClient, tool, toolDirectory, progress, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
// 如果文件已存在,跳过下载
|
else
|
||||||
if (File.Exists(localFilePath))
|
|
||||||
{
|
{
|
||||||
downloadedFiles++;
|
// 非压缩包模式:直接下载所有文件
|
||||||
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
await DownloadFilesAsync(httpClient, tool, toolDirectory, progress, token).ConfigureAwait(false);
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载文件
|
// 保存本地工具信息
|
||||||
HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
SaveLocalToolInfo(tool);
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
|
||||||
using (FileStream fileStream = new(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
|
||||||
{
|
|
||||||
await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadedFiles++;
|
|
||||||
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -139,15 +133,31 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
|||||||
{
|
{
|
||||||
string toolDirectory = GetToolDirectory(tool);
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
|
|
||||||
// 查找可执行文件(.exe)
|
// 优先使用 main_exe,如果没有则查找可执行文件
|
||||||
string? executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
|
string? executablePath = tool.MainExe;
|
||||||
|
if (string.IsNullOrEmpty(executablePath))
|
||||||
|
{
|
||||||
|
executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还是没有,尝试从目录中查找
|
||||||
|
if (string.IsNullOrEmpty(executablePath))
|
||||||
|
{
|
||||||
|
string[] exeFiles = Directory.GetFiles(toolDirectory, "*.exe", SearchOption.TopDirectoryOnly);
|
||||||
|
executablePath = exeFiles.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(executablePath))
|
if (string.IsNullOrEmpty(executablePath))
|
||||||
{
|
{
|
||||||
messenger.Send(InfoBarMessage.Warning(SH.ServiceThirdPartyToolNoExecutableFound));
|
messenger.Send(InfoBarMessage.Warning(SH.ServiceThirdPartyToolNoExecutableFound));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
string fullPath = Path.Combine(toolDirectory, executablePath);
|
// 如果 executablePath 是完整路径,直接使用;否则拼接目录
|
||||||
|
string fullPath = Path.IsPathRooted(executablePath)
|
||||||
|
? executablePath
|
||||||
|
: Path.Combine(toolDirectory, Path.GetFileName(executablePath));
|
||||||
|
|
||||||
if (!File.Exists(fullPath))
|
if (!File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
messenger.Send(InfoBarMessage.Warning(SH.FormatServiceThirdPartyToolFileNotFound(fullPath)));
|
messenger.Send(InfoBarMessage.Warning(SH.FormatServiceThirdPartyToolFileNotFound(fullPath)));
|
||||||
@@ -192,7 +202,20 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查所有文件是否存在
|
// 检查工具信息文件是否存在
|
||||||
|
LocalToolInfo? localInfo = GetLocalToolInfo(tool);
|
||||||
|
if (localInfo is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于压缩包,检查目录是否有内容
|
||||||
|
if (tool.IsCompressed)
|
||||||
|
{
|
||||||
|
return Directory.GetFiles(toolDirectory, "*", SearchOption.AllDirectories).Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非压缩包,检查所有文件是否存在
|
||||||
foreach (string fileName in tool.Files)
|
foreach (string fileName in tool.Files)
|
||||||
{
|
{
|
||||||
string filePath = Path.Combine(toolDirectory, fileName);
|
string filePath = Path.Combine(toolDirectory, fileName);
|
||||||
@@ -205,6 +228,217 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalToolInfo? GetLocalToolInfo(ToolInfo tool)
|
||||||
|
{
|
||||||
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
|
string infoFilePath = Path.Combine(toolDirectory, ToolInfoFileName);
|
||||||
|
|
||||||
|
if (!File.Exists(infoFilePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = File.ReadAllText(infoFilePath);
|
||||||
|
return JsonSerializer.Deserialize<LocalToolInfo>(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool NeedsUpdate(ToolInfo tool)
|
||||||
|
{
|
||||||
|
LocalToolInfo? localInfo = GetLocalToolInfo(tool);
|
||||||
|
if (localInfo is null)
|
||||||
|
{
|
||||||
|
return true; // 没有本地信息,需要下载
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较版本号
|
||||||
|
return IsNewerVersion(tool.Version, localInfo.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DownloadAndExtractCompressedToolAsync(
|
||||||
|
HttpClient httpClient,
|
||||||
|
ToolInfo tool,
|
||||||
|
string toolDirectory,
|
||||||
|
IProgress<double>? progress,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
// 压缩包模式通常只有一个 zip 文件
|
||||||
|
string zipFileName = tool.Files.FirstOrDefault(f => f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? tool.Files[0];
|
||||||
|
|
||||||
|
string zipUrl = $"{tool.Url}{zipFileName}";
|
||||||
|
string zipFilePath = Path.Combine(toolDirectory, zipFileName);
|
||||||
|
|
||||||
|
// 下载 zip 文件
|
||||||
|
progress?.Report(0);
|
||||||
|
|
||||||
|
HttpResponseMessage response = await httpClient.GetAsync(zipUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||||
|
using (FileStream fileStream = new(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress?.Report(50);
|
||||||
|
|
||||||
|
// 解压 zip 文件
|
||||||
|
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
|
||||||
|
{
|
||||||
|
// 检查是否有根目录需要处理
|
||||||
|
bool hasRootFolder = HasSingleRootFolder(archive);
|
||||||
|
|
||||||
|
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
// 这是一个目录,创建它
|
||||||
|
string? destinationPath = GetDestinationPath(entry.FullName, toolDirectory, hasRootFolder);
|
||||||
|
if (destinationPath is not null)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destinationPath);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? destFilePath = GetDestinationPath(entry.FullName, toolDirectory, hasRootFolder);
|
||||||
|
if (destFilePath is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
string? destDir = Path.GetDirectoryName(destFilePath);
|
||||||
|
if (!string.IsNullOrEmpty(destDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.ExtractToFile(destFilePath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress?.Report(90);
|
||||||
|
|
||||||
|
// 删除 zip 文件
|
||||||
|
File.Delete(zipFilePath);
|
||||||
|
|
||||||
|
progress?.Report(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DownloadFilesAsync(
|
||||||
|
HttpClient httpClient,
|
||||||
|
ToolInfo tool,
|
||||||
|
string toolDirectory,
|
||||||
|
IProgress<double>? progress,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
int totalFiles = tool.Files.Count;
|
||||||
|
int downloadedFiles = 0;
|
||||||
|
|
||||||
|
foreach (string fileName in tool.Files)
|
||||||
|
{
|
||||||
|
string fileUrl = $"{tool.Url}{fileName}";
|
||||||
|
string localFilePath = Path.Combine(toolDirectory, fileName);
|
||||||
|
|
||||||
|
// 如果文件已存在,跳过下载
|
||||||
|
if (File.Exists(localFilePath))
|
||||||
|
{
|
||||||
|
downloadedFiles++;
|
||||||
|
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||||
|
using (FileStream fileStream = new(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadedFiles++;
|
||||||
|
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveLocalToolInfo(ToolInfo tool)
|
||||||
|
{
|
||||||
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
|
string infoFilePath = Path.Combine(toolDirectory, ToolInfoFileName);
|
||||||
|
|
||||||
|
LocalToolInfo localInfo = LocalToolInfo.FromToolInfo(tool);
|
||||||
|
string json = JsonSerializer.Serialize(localInfo, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(infoFilePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNewerVersion(string remoteVersion, string localVersion)
|
||||||
|
{
|
||||||
|
// 使用 Version 类进行比较
|
||||||
|
if (Version.TryParse(remoteVersion, out Version? remote) && Version.TryParse(localVersion, out Version? local))
|
||||||
|
{
|
||||||
|
return remote > local;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果无法解析为版本号,进行字符串比较
|
||||||
|
return !string.Equals(remoteVersion, localVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasSingleRootFolder(ZipArchive archive)
|
||||||
|
{
|
||||||
|
// 检查是否所有条目都在同一个根目录下
|
||||||
|
HashSet<string> rootFolders = [];
|
||||||
|
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||||
|
{
|
||||||
|
int separatorIndex = entry.FullName.IndexOf('/');
|
||||||
|
if (separatorIndex > 0)
|
||||||
|
{
|
||||||
|
rootFolders.Add(entry.FullName[..separatorIndex]);
|
||||||
|
}
|
||||||
|
else if (separatorIndex < 0 && !string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
// 直接在根目录下的文件
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootFolders.Count == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetDestinationPath(string entryPath, string toolDirectory, bool hasRootFolder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entryPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRootFolder)
|
||||||
|
{
|
||||||
|
// 移除根目录前缀
|
||||||
|
int separatorIndex = entryPath.IndexOf('/');
|
||||||
|
if (separatorIndex >= 0 && separatorIndex < entryPath.Length - 1)
|
||||||
|
{
|
||||||
|
return Path.Combine(toolDirectory, entryPath[(separatorIndex + 1)..]);
|
||||||
|
}
|
||||||
|
else if (separatorIndex >= 0)
|
||||||
|
{
|
||||||
|
// 这是根目录本身
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(toolDirectory, entryPath);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetToolDirectory(ToolInfo tool)
|
private static string GetToolDirectory(ToolInfo tool)
|
||||||
{
|
{
|
||||||
// 使用数据目录/工具名作为存储路径
|
// 使用数据目录/工具名作为存储路径
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Snap.Hutao.Core.ExceptionService;
|
|||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Model.InterChange.GachaLog;
|
using Snap.Hutao.Model.InterChange.GachaLog;
|
||||||
using Snap.Hutao.Service.GachaLog;
|
using Snap.Hutao.Service.GachaLog;
|
||||||
|
using Snap.Hutao.ViewModel.GachaLog;
|
||||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ internal abstract partial class AbstractUIGF40ImportService : IUIGFImportService
|
|||||||
{
|
{
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
|
private readonly IMessenger messenger;
|
||||||
|
|
||||||
[GeneratedConstructor]
|
[GeneratedConstructor]
|
||||||
public partial AbstractUIGF40ImportService(IServiceProvider serviceProvider);
|
public partial AbstractUIGF40ImportService(IServiceProvider serviceProvider);
|
||||||
@@ -21,6 +23,7 @@ internal abstract partial class AbstractUIGF40ImportService : IUIGFImportService
|
|||||||
{
|
{
|
||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
ImportGachaArchives(importOptions.UIGF.Hk4e, importOptions.GachaArchiveUids);
|
ImportGachaArchives(importOptions.UIGF.Hk4e, importOptions.GachaArchiveUids);
|
||||||
|
messenger.Send(GachaLogImportedMessage.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportGachaArchives(ImmutableArray<UIGFEntry<Hk4eItem>> entries, HashSet<uint> uids)
|
private void ImportGachaArchives(ImmutableArray<UIGFEntry<Hk4eItem>> entries, HashSet<uint> uids)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<UseWinUI>true</UseWinUI>
|
<UseWinUI>true</UseWinUI>
|
||||||
<UseWPF>False</UseWPF>
|
<UseWPF>False</UseWPF>
|
||||||
<!-- 配置版本号 -->
|
<!-- 配置版本号 -->
|
||||||
<Version>1.18.4.0</Version>
|
<Version>1.18.7.0</Version>
|
||||||
|
|
||||||
<UseWindowsForms>False</UseWindowsForms>
|
<UseWindowsForms>False</UseWindowsForms>
|
||||||
<ImplicitUsings>False</ImplicitUsings>
|
<ImplicitUsings>False</ImplicitUsings>
|
||||||
|
|||||||
@@ -117,11 +117,18 @@ internal partial class ScopedPage : Page
|
|||||||
|
|
||||||
if (DataContext is IViewModel viewModel)
|
if (DataContext is IViewModel viewModel)
|
||||||
{
|
{
|
||||||
// Wait to ensure critical viewmodel operation is completed
|
try
|
||||||
|
{
|
||||||
|
// Wait to ensure critical viewmodel operation is completed.
|
||||||
|
// The view model might already be disposed when window shutdown and page unload race.
|
||||||
using (viewModel.CriticalSection.Enter())
|
using (viewModel.CriticalSection.Enter())
|
||||||
{
|
{
|
||||||
viewModel.Uninitialize();
|
viewModel.Uninitialize();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<!-- DocumentLink -->
|
<!-- DocumentLink -->
|
||||||
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
|
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
|
||||||
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
|
<x:String x:Key="DocumentLink_Home">https://htserver.wdg12.work</x:String>
|
||||||
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
|
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
|
||||||
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
|
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
|
||||||
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/advanced/loopback.html</x:String>
|
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/advanced/loopback.html</x:String>
|
||||||
@@ -13,32 +13,32 @@
|
|||||||
<x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String>
|
<x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String>
|
||||||
|
|
||||||
<!-- AvatarCard -->
|
<!-- AvatarCard -->
|
||||||
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://htserver.wdg.cloudns.ch/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://htserver.wdg12.work/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
||||||
|
|
||||||
<!-- Bg -->
|
<!-- Bg -->
|
||||||
<x:String x:Key="UI_ItemIcon_None">https://htserver.wdg.cloudns.ch/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
<x:String x:Key="UI_ItemIcon_None">https://htserver.wdg12.work/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
||||||
|
|
||||||
<!-- Mark -->
|
<!-- Mark -->
|
||||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Events_Proce">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
|
||||||
<x:String x:Key="UI_MarkQuest_Events_Start">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Events_Start">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
|
||||||
<x:String x:Key="UI_MarkQuest_Main_Proce">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Main_Proce">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
|
||||||
<x:String x:Key="UI_MarkQuest_Main_Start">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Main_Start">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
|
||||||
<x:String x:Key="UI_MarkTower">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkTower.png</x:String>
|
<x:String x:Key="UI_MarkTower">https://htserver.wdg12.work/static/raw/Mark/UI_MarkTower.png</x:String>
|
||||||
<!-- ItemIcon -->
|
<!-- ItemIcon -->
|
||||||
<x:String x:Key="UI_ItemIcon_106">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
|
<x:String x:Key="UI_ItemIcon_106">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
|
||||||
<x:String x:Key="UI_ItemIcon_204">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
|
<x:String x:Key="UI_ItemIcon_204">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
|
||||||
<x:String x:Key="UI_ItemIcon_210">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
|
<x:String x:Key="UI_ItemIcon_210">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
|
||||||
<x:String x:Key="UI_ItemIcon_220021">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
<x:String x:Key="UI_ItemIcon_220021">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
||||||
|
|
||||||
<!-- EmotionIcon -->
|
<!-- EmotionIcon -->
|
||||||
<x:String x:Key="UI_EmotionIcon52">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
|
<x:String x:Key="UI_EmotionIcon52">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon71">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
<x:String x:Key="UI_EmotionIcon71">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon89">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
|
<x:String x:Key="UI_EmotionIcon89">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon250">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
<x:String x:Key="UI_EmotionIcon250">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon271">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
<x:String x:Key="UI_EmotionIcon271">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon272">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
<x:String x:Key="UI_EmotionIcon272">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon293">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
<x:String x:Key="UI_EmotionIcon293">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon433">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
|
<x:String x:Key="UI_EmotionIcon433">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon445">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
<x:String x:Key="UI_EmotionIcon445">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon585">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
|
<x:String x:Key="UI_EmotionIcon585">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -9,13 +9,18 @@ namespace Snap.Hutao.UI.Xaml.Markup;
|
|||||||
[MarkupExtensionReturnType(ReturnType = typeof(string))]
|
[MarkupExtensionReturnType(ReturnType = typeof(string))]
|
||||||
internal sealed partial class ResourceStringExtension : MarkupExtension
|
internal sealed partial class ResourceStringExtension : MarkupExtension
|
||||||
{
|
{
|
||||||
public SHName Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
public string? CultureName { get; set; }
|
public string? CultureName { get; set; }
|
||||||
|
|
||||||
protected override object ProvideValue()
|
protected override object ProvideValue()
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(Name))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
CultureInfo cultureInfo = CultureName is not null ? CultureInfo.GetCultureInfo(CultureName) : CultureInfo.CurrentCulture;
|
CultureInfo cultureInfo = CultureName is not null ? CultureInfo.GetCultureInfo(CultureName) : CultureInfo.CurrentCulture;
|
||||||
return SH.ResourceManager.GetString(string.Intern(Name.ToString()), cultureInfo) ?? string.Empty;
|
return SH.ResourceManager.GetString(string.Intern(Name), cultureInfo) ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
|
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
|
||||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||||
</ContentDialog.Resources>
|
</ContentDialog.Resources>
|
||||||
|
<!-- ScrollViewer:随 ContentDialog 主题高度上限自适应;内容过高时纵向滚动,避免裁切或写死 MaxHeight -->
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
||||||
<cwc:SettingsCard Header="{shuxm:ResourceString Name=ViewDialogCultivateBatchAvatarLevelTarget}">
|
<cwc:SettingsCard Header="{shuxm:ResourceString Name=ViewDialogCultivateBatchAvatarLevelTarget}">
|
||||||
<NumberBox
|
<NumberBox
|
||||||
@@ -77,5 +79,19 @@
|
|||||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
|
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
|
||||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
|
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
|
||||||
</RadioButtons>
|
</RadioButtons>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
Margin="0,13,0,0"
|
||||||
|
Content="{shuxm:ResourceString Name=ViewDialogCultivateBatchClearAvatarAndWeaponEntries}"
|
||||||
|
IsChecked="{x:Bind ClearAvatarAndWeaponEntriesBeforeSync, Mode=TwoWay}"/>
|
||||||
|
<CheckBox
|
||||||
|
Margin="0,6,0,0"
|
||||||
|
Content="{shuxm:ResourceString Name=ViewPageCultivationRefreshInventory}"
|
||||||
|
IsChecked="{x:Bind SyncInventoryItems, Mode=TwoWay}"/>
|
||||||
|
<CheckBox
|
||||||
|
Margin="0,6,0,0"
|
||||||
|
Content="{shuxm:ResourceString Name=ViewAvatarPropertySyncDataButtonLabel}"
|
||||||
|
IsChecked="{x:Bind SyncCharacterInfo, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
</ContentDialog>
|
</ContentDialog>
|
||||||
@@ -4,18 +4,31 @@
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Factory.ContentDialog;
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
|
using Snap.Hutao.Model.Cultivation;
|
||||||
using Snap.Hutao.Service.Cultivation.Consumption;
|
using Snap.Hutao.Service.Cultivation.Consumption;
|
||||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||||
|
|
||||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
|
||||||
[DependencyProperty<AvatarPromotionDelta>("PromotionDelta", NotNull = true, CreateDefaultValueCallbackName = nameof(CreatePromotionDeltaDefaultValue))]
|
[DependencyProperty<AvatarPromotionDelta>("PromotionDelta", NotNull = true, CreateDefaultValueCallbackName = nameof(CreatePromotionDeltaDefaultValue))]
|
||||||
|
[DependencyProperty<bool>("ClearAvatarAndWeaponEntriesBeforeSync", DefaultValue = false, NotNull = true)]
|
||||||
|
[DependencyProperty<bool>("SyncInventoryItems", DefaultValue = false, NotNull = true)]
|
||||||
|
[DependencyProperty<bool>("SyncCharacterInfo", DefaultValue = false, NotNull = true)]
|
||||||
internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
||||||
{
|
{
|
||||||
private readonly IContentDialogFactory contentDialogFactory;
|
private readonly IContentDialogFactory contentDialogFactory;
|
||||||
|
|
||||||
[GeneratedConstructor(InitializeComponent = true)]
|
public CultivatePromotionDeltaBatchDialog(IServiceProvider serviceProvider, CultivateProjectAvatarPropertyBatchPreferences? initialPreferences = null)
|
||||||
public partial CultivatePromotionDeltaBatchDialog(IServiceProvider serviceProvider);
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
|
||||||
|
|
||||||
|
if (initialPreferences is not null)
|
||||||
|
{
|
||||||
|
ApplyInitialPreferences(initialPreferences);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaBaselineAsync()
|
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaBaselineAsync()
|
||||||
{
|
{
|
||||||
@@ -45,7 +58,29 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
|||||||
LocalSetting.Set(SettingKeys.CultivationWeapon90LevelTarget, weapon.LevelTarget);
|
LocalSetting.Set(SettingKeys.CultivationWeapon90LevelTarget, weapon.LevelTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(true, new(PromotionDelta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
|
return new(true, new CultivatePromotionDeltaOptions(PromotionDelta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex, ClearAvatarAndWeaponEntriesBeforeSync, SyncInventoryItems, SyncCharacterInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyInitialPreferences(CultivateProjectAvatarPropertyBatchPreferences p)
|
||||||
|
{
|
||||||
|
PromotionDelta.AvatarLevelTarget = p.AvatarLevelTarget;
|
||||||
|
|
||||||
|
if (PromotionDelta.SkillList is [{ } a, { } e, { } q, ..])
|
||||||
|
{
|
||||||
|
a.LevelTarget = p.SkillATarget;
|
||||||
|
e.LevelTarget = p.SkillETarget;
|
||||||
|
q.LevelTarget = p.SkillQTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PromotionDelta.Weapon is { } w)
|
||||||
|
{
|
||||||
|
w.LevelTarget = p.WeaponLevelTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveModeSelector.SelectedIndex = int.Clamp(p.ConsumptionSaveStrategyIndex, 0, 2);
|
||||||
|
ClearAvatarAndWeaponEntriesBeforeSync = p.ClearAvatarAndWeaponEntriesBeforeSync;
|
||||||
|
SyncInventoryItems = p.SyncInventoryItems;
|
||||||
|
SyncCharacterInfo = p.SyncCharacterInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object CreatePromotionDeltaDefaultValue()
|
private static object CreatePromotionDeltaDefaultValue()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
|||||||
|
|
||||||
internal sealed class CultivatePromotionDeltaOptions
|
internal sealed class CultivatePromotionDeltaOptions
|
||||||
{
|
{
|
||||||
public CultivatePromotionDeltaOptions(AvatarPromotionDelta delta, ConsumptionSaveStrategyKind strategy)
|
public CultivatePromotionDeltaOptions(AvatarPromotionDelta delta, ConsumptionSaveStrategyKind strategy, bool clearAvatarAndWeaponEntriesBeforeSync = false, bool syncInventoryItems = false, bool syncCharacterInfo = false)
|
||||||
{
|
{
|
||||||
delta.AvatarLevelTarget = delta.AvatarLevelTarget switch
|
delta.AvatarLevelTarget = delta.AvatarLevelTarget switch
|
||||||
{
|
{
|
||||||
@@ -19,9 +19,21 @@ internal sealed class CultivatePromotionDeltaOptions
|
|||||||
|
|
||||||
Delta = delta;
|
Delta = delta;
|
||||||
Strategy = strategy;
|
Strategy = strategy;
|
||||||
|
ClearAvatarAndWeaponEntriesBeforeSync = clearAvatarAndWeaponEntriesBeforeSync;
|
||||||
|
SyncInventoryItems = syncInventoryItems;
|
||||||
|
SyncCharacterInfo = syncCharacterInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AvatarPromotionDelta Delta { get; }
|
public AvatarPromotionDelta Delta { get; }
|
||||||
|
|
||||||
public ConsumptionSaveStrategyKind Strategy { get; }
|
public ConsumptionSaveStrategyKind Strategy { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量同步前是否清空当前计划中已有的角色与武器养成条目(不含家具等其它类型)。
|
||||||
|
/// </summary>
|
||||||
|
public bool ClearAvatarAndWeaponEntriesBeforeSync { get; }
|
||||||
|
|
||||||
|
public bool SyncInventoryItems { get; }
|
||||||
|
|
||||||
|
public bool SyncCharacterInfo { get; }
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ internal sealed partial class HutaoPassportRegisterDialog : ContentDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
|
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
|
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ internal sealed partial class HutaoPassportResetUsernameDialog : ContentDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
|
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
|
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -31,8 +32,17 @@
|
|||||||
Text="{x:Bind Tool.Description, Mode=OneWay}"
|
Text="{x:Bind Tool.Description, Mode=OneWay}"
|
||||||
TextWrapping="Wrap"/>
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{shuxm:ResourceString Name=ViewDialogThirdPartyToolVersion}"/>
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{x:Bind Tool.Version, Mode=OneWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
Grid.Row="2"
|
Grid.Row="3"
|
||||||
Height="4"
|
Height="4"
|
||||||
IsIndeterminate="{x:Bind IsDownloading.Value, Mode=OneWay, FallbackValue=False}"
|
IsIndeterminate="{x:Bind IsDownloading.Value, Mode=OneWay, FallbackValue=False}"
|
||||||
Visibility="{x:Bind IsDownloading.Value, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
Visibility="{x:Bind IsDownloading.Value, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
|||||||
@@ -39,8 +39,15 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
|
|||||||
{
|
{
|
||||||
IsDownloading = true;
|
IsDownloading = true;
|
||||||
|
|
||||||
// 检查工具是否已下载
|
if (tool is null)
|
||||||
if (tool is not null && !thirdPartyToolService.IsToolDownloaded(tool))
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查工具是否需要下载或更新
|
||||||
|
bool needDownload = !thirdPartyToolService.IsToolDownloaded(tool) || thirdPartyToolService.NeedsUpdate(tool);
|
||||||
|
|
||||||
|
if (needDownload)
|
||||||
{
|
{
|
||||||
// 下载工具
|
// 下载工具
|
||||||
bool downloadSuccess = await thirdPartyToolService.DownloadToolAsync(tool, null).ConfigureAwait(false);
|
bool downloadSuccess = await thirdPartyToolService.DownloadToolAsync(tool, null).ConfigureAwait(false);
|
||||||
@@ -53,8 +60,6 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动工具
|
// 启动工具
|
||||||
if (tool is not null)
|
|
||||||
{
|
|
||||||
bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
|
bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
|
||||||
if (launchSuccess)
|
if (launchSuccess)
|
||||||
{
|
{
|
||||||
@@ -63,7 +68,6 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
messenger.Send(InfoBarMessage.Error(ex));
|
messenger.Send(InfoBarMessage.Error(ex));
|
||||||
|
|||||||
@@ -268,7 +268,7 @@
|
|||||||
<TextBlock>
|
<TextBlock>
|
||||||
<TextBlock.Inlines>
|
<TextBlock.Inlines>
|
||||||
<Run Text="{shuxm:ResourceString Name=ViewGuideStepAgreementIHaveReadText}"/>
|
<Run Text="{shuxm:ResourceString Name=ViewGuideStepAgreementIHaveReadText}"/>
|
||||||
<Hyperlink NavigateUri="https://github.com/DGP-Studio/Snap.Hutao/blob/main/LICENSE">
|
<Hyperlink NavigateUri="https://github.com/wangdage12/Snap.Hutao/blob/main/LICENSE">
|
||||||
<Run Text="{shuxm:ResourceString Name=ViewGuideStepAgreementOpenSourceLicense}"/>
|
<Run Text="{shuxm:ResourceString Name=ViewGuideStepAgreementOpenSourceLicense}"/>
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
</TextBlock.Inlines>
|
</TextBlock.Inlines>
|
||||||
|
|||||||
@@ -198,6 +198,11 @@
|
|||||||
Opacity="0.7"
|
Opacity="0.7"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{Binding Description}"/>
|
Text="{Binding Description}"/>
|
||||||
|
<TextBlock
|
||||||
|
Opacity="0.7"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{Binding RelatedAvatarLine}"
|
||||||
|
Visibility="{Binding RelatedAvatarLine, Converter={StaticResource EmptyObjectToVisibilityConverter}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||||
@@ -227,7 +232,69 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="StatisticsItemTemplate" x:DataType="shvcu:StatisticsCultivateItem">
|
<DataTemplate x:Key="StatisticsItemTemplate" x:DataType="shvcu:StatisticsCultivateItem">
|
||||||
<shuxcc:HorizontalCard Background="{Binding IsToday, Converter={ThemeResource BoolToStatisticsBrushSelector}}">
|
<Border Background="Transparent">
|
||||||
|
<shuxcc:HorizontalCard
|
||||||
|
Background="{Binding IsToday, Converter={ThemeResource BoolToStatisticsBrushSelector}}"
|
||||||
|
IsRightTapEnabled="True"
|
||||||
|
IsTabStop="True"
|
||||||
|
UseSystemFocusVisuals="True">
|
||||||
|
<FrameworkElement.ContextFlyout>
|
||||||
|
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding0Style}">
|
||||||
|
<ScrollViewer MaxHeight="320" MinWidth="220" Padding="12,10">
|
||||||
|
<ItemsControl ItemsSource="{Binding StatisticsConsumerMenuLines, Mode=OneWay}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="shvcu:StatisticsConsumerMenuLine">
|
||||||
|
<Grid Margin="0,3">
|
||||||
|
<TextBlock
|
||||||
|
Style="{ThemeResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind PlainMessage, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="{x:Bind IsPlainMessage, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="4"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{x:Bind ShowRichRow, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<shuxc:ItemIcon
|
||||||
|
Width="28"
|
||||||
|
Height="28"
|
||||||
|
Icon="{x:Bind LeadingIcon, Mode=OneWay}"
|
||||||
|
Quality="{x:Bind LeadingQuality, Mode=OneWay}"/>
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{ThemeResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind FirstName, Mode=OneWay}"/>
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="4"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{x:Bind HasSecondIcon, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{ThemeResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind BetweenSeparator, Mode=OneWay}"/>
|
||||||
|
<shuxc:ItemIcon
|
||||||
|
Width="28"
|
||||||
|
Height="28"
|
||||||
|
Icon="{x:Bind SecondIcon, Mode=OneWay}"
|
||||||
|
Quality="{x:Bind SecondQuality, Mode=OneWay}"/>
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{ThemeResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind SecondName, Mode=OneWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{ThemeResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind CountSuffix, Mode=OneWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Flyout>
|
||||||
|
</FrameworkElement.ContextFlyout>
|
||||||
<shuxcc:HorizontalCard.Left>
|
<shuxcc:HorizontalCard.Left>
|
||||||
<Grid Grid.Column="0">
|
<Grid Grid.Column="0">
|
||||||
<shuxc:ItemIcon
|
<shuxc:ItemIcon
|
||||||
@@ -256,14 +323,44 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding Inner.Name}"
|
Text="{Binding Inner.Name}"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
TextTrimming="CharacterEllipsis"/>
|
||||||
<TextBlock
|
<StackPanel
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="0,0,0,0"
|
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding FormattedCount}"/>
|
Orientation="Horizontal"
|
||||||
|
Spacing="0">
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding FormattedCount}"
|
||||||
|
Visibility="{Binding ShowNonMergeCompactCount, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="0"
|
||||||
|
Visibility="{Binding ShowMergeSpacedWithoutParen, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding DisplayCurrent, Mode=OneWay}"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding SlashCountSuffix, Mode=OneWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="0"
|
||||||
|
Visibility="{Binding ShowMergeInventoryParen, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||||
|
Text="{Binding DisplayCurrent, Mode=OneWay}"
|
||||||
|
Visibility="{Binding MergeDisplayLeadUseRed, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||||
|
Text="{Binding DisplayCurrent, Mode=OneWay}"
|
||||||
|
Visibility="{Binding MergeDisplayLeadUseGreen, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding RawInventoryParenthetical, Mode=OneWay}"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding SlashCountSuffixForParen, Mode=OneWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</shuxcc:HorizontalCard.Right>
|
</shuxcc:HorizontalCard.Right>
|
||||||
</shuxcc:HorizontalCard>
|
</shuxcc:HorizontalCard>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="InventoryItemTemplate" x:DataType="shvcu:InventoryItemView">
|
<DataTemplate x:Key="InventoryItemTemplate" x:DataType="shvcu:InventoryItemView">
|
||||||
@@ -374,7 +471,13 @@
|
|||||||
CommandParameter="{Binding Projects.CurrentItem, Mode=OneWay}"
|
CommandParameter="{Binding Projects.CurrentItem, Mode=OneWay}"
|
||||||
Icon="{shuxm:FontIcon Glyph=}"
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
Label="{shuxm:ResourceString Name=ViewPageCultivationClearInventory}"/>
|
Label="{shuxm:ResourceString Name=ViewPageCultivationClearInventory}"/>
|
||||||
<AppBarButton Icon="{shuxm:FontIcon Glyph=}" Label="{shuxm:ResourceString Name=ViewPageCultivationRefreshInventory}">
|
<AppBarButton
|
||||||
|
Command="{Binding SyncAllAvatarsAndWeaponsCommand}"
|
||||||
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
|
Label="{shuxm:ResourceString Name=ViewPageCultivationSyncAllAvatarsAndWeapons}"/>
|
||||||
|
<AppBarButton
|
||||||
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
|
Label="{shuxm:ResourceString Name=ViewPageCultivationRefreshInventory}">
|
||||||
<AppBarButton.Flyout>
|
<AppBarButton.Flyout>
|
||||||
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
||||||
<MenuFlyoutItem
|
<MenuFlyoutItem
|
||||||
@@ -388,6 +491,10 @@
|
|||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</AppBarButton.Flyout>
|
</AppBarButton.Flyout>
|
||||||
</AppBarButton>
|
</AppBarButton>
|
||||||
|
<AppBarToggleButton
|
||||||
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
|
IsChecked="{Binding SyncInventoryByCalculatorToAllProjects, Mode=TwoWay}"
|
||||||
|
Label="{shuxm:ResourceString Name=ViewPageCultivationRefreshInventoryAllPlansShortLabel}"/>
|
||||||
<AppBarButton
|
<AppBarButton
|
||||||
Command="{Binding AddProjectCommand}"
|
Command="{Binding AddProjectCommand}"
|
||||||
Icon="{shuxm:FontIcon Glyph=}"
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
@@ -482,6 +589,25 @@
|
|||||||
IsChecked="{Binding IncompleteFirst, Mode=TwoWay}"
|
IsChecked="{Binding IncompleteFirst, Mode=TwoWay}"
|
||||||
Label="{shuxm:ResourceString Name=ViewPageCultivationMaterialListIncompleteFirstLabel}"
|
Label="{shuxm:ResourceString Name=ViewPageCultivationMaterialListIncompleteFirstLabel}"
|
||||||
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
|
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
|
||||||
|
<AppBarToggleButton
|
||||||
|
Command="{Binding RefreshStatisticsItemsCommand}"
|
||||||
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
|
IsChecked="{Binding MergeUpgradeMaterials, Mode=TwoWay}"
|
||||||
|
Label="{shuxm:ResourceString Name=ViewPageCultivationMergeUpgradeMaterialsLabel}"
|
||||||
|
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
|
||||||
|
<AppBarToggleButton
|
||||||
|
Command="{Binding RefreshStatisticsItemsCommand}"
|
||||||
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
|
IsChecked="{Binding TalentSynthCritTenPercent, Mode=TwoWay}"
|
||||||
|
IsEnabled="{Binding MergeUpgradeMaterials}"
|
||||||
|
Label="{shuxm:ResourceString Name=ViewPageCultivationTalentSynthCritTenPercentLabel}"
|
||||||
|
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
|
||||||
|
<AppBarToggleButton
|
||||||
|
Command="{Binding RefreshStatisticsItemsCommand}"
|
||||||
|
Icon="{shuxm:FontIcon Glyph=}"
|
||||||
|
IsChecked="{Binding WeeklyBossMaterialInterchange, Mode=TwoWay}"
|
||||||
|
Label="{shuxm:ResourceString Name=ViewPageCultivationWeeklyBossMaterialInterchangeLabel}"
|
||||||
|
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
|
||||||
<AppBarButton Icon="{shuxm:FontIcon Glyph=}" Label="{shuxm:ResourceString Name=ViewPageCultivationMaterialListResinStatisticsLabel}">
|
<AppBarButton Icon="{shuxm:FontIcon Glyph=}" Label="{shuxm:ResourceString Name=ViewPageCultivationMaterialListResinStatisticsLabel}">
|
||||||
<AppBarButton.Flyout>
|
<AppBarButton.Flyout>
|
||||||
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding0Style}" Placement="BottomEdgeAlignedRight">
|
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding0Style}" Placement="BottomEdgeAlignedRight">
|
||||||
@@ -617,7 +743,6 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</GridView.ItemContainerStyle>
|
</GridView.ItemContainerStyle>
|
||||||
</GridView>
|
</GridView>
|
||||||
|
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
|||||||
@@ -93,13 +93,13 @@
|
|||||||
<cwcont:SettingsExpander.Items>
|
<cwcont:SettingsExpander.Items>
|
||||||
<cwcont:SettingsCard
|
<cwcont:SettingsCard
|
||||||
Command="{Binding NavigateToUriCommand}"
|
Command="{Binding NavigateToUriCommand}"
|
||||||
CommandParameter="https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose"
|
CommandParameter="https://github.com/wangdage12/Snap.Hutao"
|
||||||
Description="{shuxm:ResourceString Name=ViewPageFeedbackGithubIssuesDescription}"
|
Description="{shuxm:ResourceString Name=ViewPageFeedbackGithubIssuesDescription}"
|
||||||
Header="GitHub Issues"
|
Header="GitHub Issues"
|
||||||
IsClickEnabled="True"/>
|
IsClickEnabled="True"/>
|
||||||
<cwcont:SettingsCard
|
<cwcont:SettingsCard
|
||||||
Command="{Binding NavigateToUriCommand}"
|
Command="{Binding NavigateToUriCommand}"
|
||||||
CommandParameter="https://status.snapgenshin.cn/status"
|
CommandParameter="https://stats.uptimerobot.com/fHxWxdxK61"
|
||||||
Description="{shuxm:ResourceString Name=ViewPageFeedbackServerStatusDescription}"
|
Description="{shuxm:ResourceString Name=ViewPageFeedbackServerStatusDescription}"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageFeedbackServerStatusHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageFeedbackServerStatusHeader}"
|
||||||
IsClickEnabled="True"/>
|
IsClickEnabled="True"/>
|
||||||
|
|||||||
@@ -9,24 +9,26 @@ using Snap.Hutao.Core.Setting;
|
|||||||
using Snap.Hutao.Factory.ContentDialog;
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
using Snap.Hutao.Model;
|
using Snap.Hutao.Model;
|
||||||
using Snap.Hutao.Model.Calculable;
|
using Snap.Hutao.Model.Calculable;
|
||||||
|
using Snap.Hutao.Model.Cultivation;
|
||||||
using Snap.Hutao.Model.Entity.Primitive;
|
using Snap.Hutao.Model.Entity.Primitive;
|
||||||
using Snap.Hutao.Service;
|
using Snap.Hutao.Service;
|
||||||
using Snap.Hutao.Service.AvatarInfo;
|
using Snap.Hutao.Service.AvatarInfo;
|
||||||
using Snap.Hutao.Service.AvatarInfo.Factory;
|
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||||
using Snap.Hutao.Service.Cultivation;
|
using Snap.Hutao.Service.Cultivation;
|
||||||
using Snap.Hutao.Service.Cultivation.Consumption;
|
using Snap.Hutao.Service.Cultivation.Consumption;
|
||||||
|
using BatchCultivateResult = Snap.Hutao.Service.Cultivation.BatchCultivateResult;
|
||||||
using Snap.Hutao.Service.Cultivation.Offline;
|
using Snap.Hutao.Service.Cultivation.Offline;
|
||||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||||
using Snap.Hutao.Service.Notification;
|
using Snap.Hutao.Service.Notification;
|
||||||
using Snap.Hutao.Service.User;
|
using Snap.Hutao.Service.User;
|
||||||
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
||||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
using Snap.Hutao.ViewModel.User;
|
using Snap.Hutao.ViewModel.User;
|
||||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
|
||||||
using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||||
using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
||||||
using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
|
using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
|
||||||
@@ -39,6 +41,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
{
|
{
|
||||||
private readonly ExclusiveTokenProvider refreshTokenProvider = new();
|
private readonly ExclusiveTokenProvider refreshTokenProvider = new();
|
||||||
private readonly AvatarPropertyViewModelScopeContext scopeContext;
|
private readonly AvatarPropertyViewModelScopeContext scopeContext;
|
||||||
|
private readonly IAvatarPropertyBatchCultivateService avatarPropertyBatchCultivateService;
|
||||||
|
|
||||||
private SummaryFactoryMetadataContext? metadataContext;
|
private SummaryFactoryMetadataContext? metadataContext;
|
||||||
|
|
||||||
@@ -227,7 +230,10 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
if (!await SaveCultivationAsync(batchConsumption.Items.Single(), deltaOptions).ConfigureAwait(false))
|
if (!await SaveCultivationAsync(batchConsumption.Items.Single(), deltaOptions).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
scopeContext.Messenger.Send(InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning));
|
scopeContext.Messenger.Send(InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scopeContext.Messenger.Send(CultivationProjectEntriesChangedMessage.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("BatchCultivateCommand")]
|
[Command("BatchCultivateCommand")]
|
||||||
@@ -268,51 +274,23 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
targetAvatars = [.. avatars.Source];
|
targetAvatars = [.. avatars.Source];
|
||||||
}
|
}
|
||||||
|
|
||||||
CultivatePromotionDeltaBatchDialog dialog = await scopeContext.ContentDialogFactory
|
ArgumentNullException.ThrowIfNull(metadataContext);
|
||||||
.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>(scopeContext.ServiceProvider)
|
BatchCultivateResult? batchResult = await avatarPropertyBatchCultivateService
|
||||||
|
.ExecuteAsync(metadataContext, targetAvatars, CancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false) is not (true, { } baseline))
|
if (batchResult is not { } result)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(baseline.Delta.Weapon);
|
if (result.StopReason is not BatchCultivateStopReason.None)
|
||||||
|
|
||||||
ContentDialog progressDialog = await scopeContext.ContentDialogFactory
|
|
||||||
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
BatchCultivateResult result = default;
|
|
||||||
using (await scopeContext.ContentDialogFactory.BlockAsync(progressDialog).ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
ImmutableArray<CalculatorAvatarPromotionDelta>.Builder deltasBuilder = ImmutableArray.CreateBuilder<CalculatorAvatarPromotionDelta>();
|
scopeContext.Messenger.Send(InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning));
|
||||||
foreach (AvatarView avatar in targetAvatars)
|
return;
|
||||||
{
|
|
||||||
if (!baseline.Delta.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
|
|
||||||
{
|
|
||||||
++result.SkippedCount;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deltasBuilder.Add(copy);
|
scopeContext.Messenger.Send(CultivationProjectEntriesChangedMessage.Empty);
|
||||||
}
|
|
||||||
|
|
||||||
ImmutableArray<CalculatorAvatarPromotionDelta> deltas = deltasBuilder.ToImmutable();
|
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(metadataContext);
|
|
||||||
CalculatorBatchConsumption batchConsumption = OfflineCalculator.CalculateBatchConsumption(deltas, metadataContext);
|
|
||||||
|
|
||||||
foreach ((CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta) in batchConsumption.Items.Zip(deltas))
|
|
||||||
{
|
|
||||||
if (!await SaveCultivationAsync(consumption, new(delta, baseline.Strategy), true).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
++result.SucceedCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoBarMessage message = result.SkippedCount > 0
|
InfoBarMessage message = result.SkippedCount > 0
|
||||||
? InfoBarMessage.Warning(SH.FormatViewModelCultivationBatchAddIncompleted(result.SucceedCount, result.SkippedCount))
|
? InfoBarMessage.Warning(SH.FormatViewModelCultivationBatchAddIncompleted(result.SucceedCount, result.SkippedCount))
|
||||||
@@ -335,9 +313,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
Strategy = options.Strategy,
|
Strategy = options.Strategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
ConsumptionSaveResultKind avatarSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
|
ConsumptionSaveResult avatarSave = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
|
||||||
|
|
||||||
InfoBarMessage? avatarMessage = avatarSaveKind switch
|
InfoBarMessage? avatarMessage = avatarSave.Kind switch
|
||||||
{
|
{
|
||||||
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
||||||
ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
||||||
@@ -351,13 +329,29 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
scopeContext.Messenger.Send(avatarMessage);
|
scopeContext.Messenger.Send(avatarMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarSaveKind is ConsumptionSaveResultKind.NoProject)
|
if (avatarSave.Kind is ConsumptionSaveResultKind.NoProject)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(options.Delta.Weapon);
|
ArgumentNullException.ThrowIfNull(options.Delta.Weapon);
|
||||||
|
|
||||||
|
Guid? relatedAvatarEntryId = avatarSave.CreatedEntryInnerId;
|
||||||
|
if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.Skipped)
|
||||||
|
{
|
||||||
|
relatedAvatarEntryId = await scopeContext.CultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.NoItem)
|
||||||
|
{
|
||||||
|
relatedAvatarEntryId = await scopeContext.CultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedAvatarEntryId is null && !consumption.WeaponConsume.IsEmpty)
|
||||||
|
{
|
||||||
|
relatedAvatarEntryId = await scopeContext.CultivationService.EnsureAvatarAssociationStubAsync(options.Delta.AvatarId, levelInformation).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
InputConsumption weaponInput = new()
|
InputConsumption weaponInput = new()
|
||||||
{
|
{
|
||||||
Type = CultivateType.Weapon,
|
Type = CultivateType.Weapon,
|
||||||
@@ -365,10 +359,11 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
Items = consumption.WeaponConsume,
|
Items = consumption.WeaponConsume,
|
||||||
LevelInformation = levelInformation,
|
LevelInformation = levelInformation,
|
||||||
Strategy = options.Strategy,
|
Strategy = options.Strategy,
|
||||||
|
RelatedEntryId = relatedAvatarEntryId,
|
||||||
};
|
};
|
||||||
|
|
||||||
ConsumptionSaveResultKind weaponSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
|
ConsumptionSaveResult weaponSave = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
|
||||||
InfoBarMessage? weaponMessage = weaponSaveKind switch
|
InfoBarMessage? weaponMessage = weaponSave.Kind switch
|
||||||
{
|
{
|
||||||
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
||||||
ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
||||||
@@ -382,7 +377,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
|||||||
scopeContext.Messenger.Send(weaponMessage);
|
scopeContext.Messenger.Send(weaponMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return weaponSaveKind is not ConsumptionSaveResultKind.NoProject;
|
return weaponSave.Kind is not ConsumptionSaveResultKind.NoProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("ExportToTextCommand")]
|
[Command("ExportToTextCommand")]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Snap.Hutao.ViewModel.Cultivation;
|
|||||||
|
|
||||||
internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
|
internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
|
||||||
{
|
{
|
||||||
private CultivateEntryView(CultivateEntry entry, Item item, ImmutableArray<CultivateItemView> items)
|
private CultivateEntryView(CultivateEntry entry, Item item, ImmutableArray<CultivateItemView> items, string? relatedAvatarName)
|
||||||
{
|
{
|
||||||
Id = entry.Id;
|
Id = entry.Id;
|
||||||
EntryId = entry.InnerId;
|
EntryId = entry.InnerId;
|
||||||
@@ -25,6 +25,8 @@ internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
|
|||||||
Items = items;
|
Items = items;
|
||||||
Type = entry.Type;
|
Type = entry.Type;
|
||||||
|
|
||||||
|
RelatedAvatarLine = relatedAvatarName is { } name ? SH.FormatViewModelCultivationEntryRelatedAvatar(name) : null;
|
||||||
|
|
||||||
Description = ParseDescription(entry);
|
Description = ParseDescription(entry);
|
||||||
IsToday = items.Any(i => i.IsToday);
|
IsToday = items.Any(i => i.IsToday);
|
||||||
RotationalItemIds = [.. items.Where(i => i.DaysOfWeek is not DaysOfWeek.Any).Select(i => i.Inner.Id)];
|
RotationalItemIds = [.. items.Where(i => i.DaysOfWeek is not DaysOfWeek.Any).Select(i => i.Inner.Id)];
|
||||||
@@ -110,12 +112,17 @@ internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
|
|||||||
|
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 武器条目在养成计算中与角色条目关联时的展示文案(含本地化前缀)。
|
||||||
|
/// </summary>
|
||||||
|
public string? RelatedAvatarLine { get; }
|
||||||
|
|
||||||
internal Guid EntryId { get; }
|
internal Guid EntryId { get; }
|
||||||
|
|
||||||
internal CultivateType Type { get; }
|
internal CultivateType Type { get; }
|
||||||
|
|
||||||
public static CultivateEntryView Create(CultivateEntry entry, Item item, ImmutableArray<CultivateItemView> items)
|
public static CultivateEntryView Create(CultivateEntry entry, Item item, ImmutableArray<CultivateItemView> items, string? relatedAvatarName = null)
|
||||||
{
|
{
|
||||||
return new(entry, item, items);
|
return new(entry, item, items, relatedAvatarName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前养成计划的条目在 UI 外被批量变更(例如从「我的角色」同步)后,通知养成页刷新列表与统计。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CultivationProjectEntriesChangedMessage
|
||||||
|
{
|
||||||
|
public static readonly CultivationProjectEntriesChangedMessage Empty = new();
|
||||||
|
|
||||||
|
private CultivationProjectEntriesChangedMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,33 +2,40 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Core.Database;
|
using Snap.Hutao.Core.Database;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.Logging;
|
using Snap.Hutao.Core.Logging;
|
||||||
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Factory.ContentDialog;
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
|
using Snap.Hutao.Service.AvatarInfo;
|
||||||
|
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||||
using Snap.Hutao.Service.Cultivation;
|
using Snap.Hutao.Service.Cultivation;
|
||||||
using Snap.Hutao.Service.Inventory;
|
using Snap.Hutao.Service.Inventory;
|
||||||
using Snap.Hutao.Service.Metadata;
|
using Snap.Hutao.Service.Metadata;
|
||||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||||
using Snap.Hutao.Service.Navigation;
|
using Snap.Hutao.Service.Navigation;
|
||||||
using Snap.Hutao.Service.Notification;
|
using Snap.Hutao.Service.Notification;
|
||||||
|
using Snap.Hutao.Service.User;
|
||||||
using Snap.Hutao.Service.Yae;
|
using Snap.Hutao.Service.Yae;
|
||||||
|
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||||
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
||||||
using Snap.Hutao.UI.Xaml.Data;
|
using Snap.Hutao.UI.Xaml.Data;
|
||||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
using Snap.Hutao.ViewModel.Game;
|
using Snap.Hutao.ViewModel.Game;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using BatchCultivateResult = Snap.Hutao.Service.Cultivation.BatchCultivateResult;
|
||||||
|
|
||||||
namespace Snap.Hutao.ViewModel.Cultivation;
|
namespace Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
|
||||||
[SuppressMessage("", "CA1001")]
|
[SuppressMessage("", "CA1001")]
|
||||||
[BindableCustomPropertyProvider]
|
[BindableCustomPropertyProvider]
|
||||||
[Service(ServiceLifetime.Scoped)]
|
[Service(ServiceLifetime.Scoped)]
|
||||||
internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
internal sealed partial class CultivationViewModel : Abstraction.ViewModel, IRecipient<CultivationProjectEntriesChangedMessage>
|
||||||
{
|
{
|
||||||
private readonly ExclusiveTokenProvider exclusiveTokenProvider = new();
|
private readonly ExclusiveTokenProvider exclusiveTokenProvider = new();
|
||||||
|
|
||||||
@@ -36,6 +43,9 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
private readonly ICultivationService cultivationService;
|
private readonly ICultivationService cultivationService;
|
||||||
private readonly INavigationService navigationService;
|
private readonly INavigationService navigationService;
|
||||||
private readonly IInventoryService inventoryService;
|
private readonly IInventoryService inventoryService;
|
||||||
|
private readonly IAvatarInfoService avatarInfoService;
|
||||||
|
private readonly IAvatarPropertyBatchCultivateService avatarPropertyBatchCultivateService;
|
||||||
|
private readonly IUserService userService;
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly IMetadataService metadataService;
|
private readonly IMetadataService metadataService;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
@@ -70,6 +80,18 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IncompleteFirst { get; set; }
|
public partial bool IncompleteFirst { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool MergeUpgradeMaterials { get; set; } = LocalSetting.Get(SettingKeys.CultivationStatisticsMergeUpgradeMaterials, false);
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool TalentSynthCritTenPercent { get; set; } = LocalSetting.Get(SettingKeys.CultivationStatisticsTalentSynthCritTenPercent, false);
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool WeeklyBossMaterialInterchange { get; set; } = LocalSetting.Get(SettingKeys.CultivationStatisticsWeeklyBossMaterialInterchange, false);
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool SyncInventoryByCalculatorToAllProjects { get; set; } = LocalSetting.Get(SettingKeys.CultivationRefreshInventoryByCalculatorToAllProjects, false);
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ObservableCollection<StatisticsCultivateItem>? StatisticsItems { get; set; }
|
public partial ObservableCollection<StatisticsCultivateItem>? StatisticsItems { get; set; }
|
||||||
|
|
||||||
@@ -81,6 +103,9 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
|
|
||||||
protected override async ValueTask<bool> LoadOverrideAsync(CancellationToken token)
|
protected override async ValueTask<bool> LoadOverrideAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
messenger.UnregisterAll(this);
|
||||||
|
messenger.Register<CultivationProjectEntriesChangedMessage>(this);
|
||||||
|
|
||||||
if (!await metadataService.InitializeAsync().ConfigureAwait(false))
|
if (!await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -111,6 +136,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
|
|
||||||
protected override void UninitializeOverride()
|
protected override void UninitializeOverride()
|
||||||
{
|
{
|
||||||
|
messenger.UnregisterAll(this);
|
||||||
using (Projects?.SuppressChangeCurrentItem())
|
using (Projects?.SuppressChangeCurrentItem())
|
||||||
{
|
{
|
||||||
Projects = default;
|
Projects = default;
|
||||||
@@ -122,6 +148,27 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
UpdateEntryCollectionAsync(Projects?.CurrentItem).SafeForget();
|
UpdateEntryCollectionAsync(Projects?.CurrentItem).SafeForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnSyncInventoryByCalculatorToAllProjectsChanged(bool value)
|
||||||
|
{
|
||||||
|
LocalSetting.Set(SettingKeys.CultivationRefreshInventoryByCalculatorToAllProjects, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(CultivationProjectEntriesChangedMessage _)
|
||||||
|
{
|
||||||
|
ReceiveProjectEntriesChangedAsync().SafeForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask ReceiveProjectEntriesChangedAsync()
|
||||||
|
{
|
||||||
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
if (Projects?.CurrentItem is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateEntryCollectionAsync(Projects.CurrentItem).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
[Command("AddProjectCommand")]
|
[Command("AddProjectCommand")]
|
||||||
private async Task AddProjectAsync()
|
private async Task AddProjectAsync()
|
||||||
{
|
{
|
||||||
@@ -292,7 +339,9 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
|
|
||||||
using (await contentDialogFactory.BlockAsync(dialog).ConfigureAwait(false))
|
using (await contentDialogFactory.BlockAsync(dialog).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await inventoryService.RefreshInventoryAsync(RefreshOptions.CreateForWebCalculator(Projects.CurrentItem, metadataContext)).ConfigureAwait(false);
|
await inventoryService
|
||||||
|
.RefreshInventoryAsync(RefreshOptions.CreateForWebCalculator(Projects.CurrentItem, metadataContext, SyncInventoryByCalculatorToAllProjects))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
await UpdateInventoryItemsAsync().ConfigureAwait(false);
|
await UpdateInventoryItemsAsync().ConfigureAwait(false);
|
||||||
await UpdateStatisticsItemsAsync().ConfigureAwait(false);
|
await UpdateStatisticsItemsAsync().ConfigureAwait(false);
|
||||||
@@ -300,6 +349,63 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command("SyncAllAvatarsAndWeaponsCommand")]
|
||||||
|
private async Task SyncAllAvatarsAndWeaponsAsync()
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory2.CreateUI("Sync all avatars and weapons to cultivation", "CultivationViewModel.Command", []));
|
||||||
|
|
||||||
|
if (Projects?.CurrentItem is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Warning(SH.MustSelectUserAndUid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryFactoryMetadataContext summaryContext = await metadataService.GetContextAsync<SummaryFactoryMetadataContext>(CancellationToken).ConfigureAwait(false);
|
||||||
|
Summary? summary = await avatarInfoService.GetSummaryAsync(summaryContext, userAndUid, global::Snap.Hutao.Service.AvatarInfo.RefreshOptionKind.None, CancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (summary is not { Avatars: { } avatars })
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Warning(SH.ViewPageAvatarPropertyDefaultDescription));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatars.Source.Count < 1)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Warning(SH.ViewPageAvatarPropertyDefaultDescription));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmutableArray<AvatarView> targetAvatars = [.. avatars.Source];
|
||||||
|
|
||||||
|
BatchCultivateResult? batchResult = await avatarPropertyBatchCultivateService
|
||||||
|
.ExecuteAsync(summaryContext, targetAvatars, CancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (batchResult is not { } result)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.StopReason is not BatchCultivateStopReason.None)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messenger.Send(CultivationProjectEntriesChangedMessage.Empty);
|
||||||
|
|
||||||
|
InfoBarMessage message = result.SkippedCount > 0
|
||||||
|
? InfoBarMessage.Warning(SH.FormatViewModelCultivationBatchAddIncompleted(result.SucceedCount, result.SkippedCount))
|
||||||
|
: InfoBarMessage.Success(SH.FormatViewModelCultivationBatchAddCompleted(result.SucceedCount, result.SkippedCount));
|
||||||
|
|
||||||
|
messenger.Send(message);
|
||||||
|
}
|
||||||
|
|
||||||
[Command("ClearInventoryCommand")]
|
[Command("ClearInventoryCommand")]
|
||||||
private async Task ClearInventoryAsync(CultivateProject? project)
|
private async Task ClearInventoryAsync(CultivateProject? project)
|
||||||
{
|
{
|
||||||
@@ -356,12 +462,21 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
|
|||||||
|
|
||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
|
bool merge = MergeUpgradeMaterials;
|
||||||
|
bool talentCrit = merge && TalentSynthCritTenPercent;
|
||||||
|
bool weeklyBoss = WeeklyBossMaterialInterchange;
|
||||||
|
LocalSetting.Set(SettingKeys.CultivationStatisticsMergeUpgradeMaterials, merge);
|
||||||
|
LocalSetting.Set(SettingKeys.CultivationStatisticsTalentSynthCritTenPercent, talentCrit);
|
||||||
|
LocalSetting.Set(SettingKeys.CultivationStatisticsWeeklyBossMaterialInterchange, weeklyBoss);
|
||||||
|
|
||||||
CancellationToken token = exclusiveTokenProvider.GetNewToken();
|
CancellationToken token = exclusiveTokenProvider.GetNewToken();
|
||||||
StatisticsCultivateItemCollection statistics;
|
StatisticsCultivateItemCollection statistics;
|
||||||
ResinStatistics resinStatistics;
|
ResinStatistics resinStatistics;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
statistics = await cultivationService.GetStatisticsCultivateItemCollectionAsync(Projects.CurrentItem, metadataContext, token).ConfigureAwait(false);
|
statistics = await cultivationService
|
||||||
|
.GetStatisticsCultivateItemCollectionAsync(Projects.CurrentItem, metadataContext, new CultivationStatisticsMergeOptions(merge, talentCrit, weeklyBoss), token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
resinStatistics = await cultivationService.GetResinStatisticsAsync(statistics, token).ConfigureAwait(false);
|
resinStatistics = await cultivationService.GetResinStatisticsAsync(statistics, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Intrinsic;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料统计右键浮层中一行「未完成」养成条目:名称前展示角色/武器图标。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class StatisticsConsumerMenuLine
|
||||||
|
{
|
||||||
|
private StatisticsConsumerMenuLine()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPlainMessage { get; private init; }
|
||||||
|
|
||||||
|
public string? PlainMessage { get; private init; }
|
||||||
|
|
||||||
|
public bool ShowRichRow => !IsPlainMessage;
|
||||||
|
|
||||||
|
public Uri LeadingIcon { get; private init; } = default!;
|
||||||
|
|
||||||
|
public QualityType LeadingQuality { get; private init; }
|
||||||
|
|
||||||
|
public bool HasSecondIcon { get; private init; }
|
||||||
|
|
||||||
|
public Uri SecondIcon { get; private init; } = default!;
|
||||||
|
|
||||||
|
public QualityType SecondQuality { get; private init; }
|
||||||
|
|
||||||
|
public string FirstName { get; private init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>双名称行中间分隔(与武器关联条目的「角色·武器」展示一致,使用间隔号)。</summary>
|
||||||
|
public string BetweenSeparator { get; private init; } = "\u00B7";
|
||||||
|
|
||||||
|
public string? SecondName { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>计划需求量后缀,一般为全角括号包裹的数量 <c>(12)</c>。</summary>
|
||||||
|
public string CountSuffix { get; private init; } = string.Empty;
|
||||||
|
|
||||||
|
public static StatisticsConsumerMenuLine Plain(string message)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
IsPlainMessage = true,
|
||||||
|
PlainMessage = message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StatisticsConsumerMenuLine SingleIcon(Uri icon, QualityType quality, string name, string countSuffix)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
LeadingIcon = icon,
|
||||||
|
LeadingQuality = quality,
|
||||||
|
FirstName = name,
|
||||||
|
CountSuffix = countSuffix,
|
||||||
|
HasSecondIcon = false,
|
||||||
|
SecondIcon = default!,
|
||||||
|
SecondQuality = QualityType.QUALITY_NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StatisticsConsumerMenuLine AvatarAndWeapon(
|
||||||
|
Uri avatarIcon,
|
||||||
|
QualityType avatarQuality,
|
||||||
|
string avatarName,
|
||||||
|
Uri weaponIcon,
|
||||||
|
QualityType weaponQuality,
|
||||||
|
string weaponName,
|
||||||
|
string countSuffix)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
LeadingIcon = avatarIcon,
|
||||||
|
LeadingQuality = avatarQuality,
|
||||||
|
FirstName = avatarName,
|
||||||
|
HasSecondIcon = true,
|
||||||
|
SecondIcon = weaponIcon,
|
||||||
|
SecondQuality = weaponQuality,
|
||||||
|
SecondName = weaponName,
|
||||||
|
CountSuffix = countSuffix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Model.Metadata.Item;
|
using Snap.Hutao.Model.Metadata.Item;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace Snap.Hutao.ViewModel.Cultivation;
|
namespace Snap.Hutao.ViewModel.Cultivation;
|
||||||
|
|
||||||
@@ -29,12 +30,55 @@ internal sealed class StatisticsCultivateItem
|
|||||||
|
|
||||||
public uint Current { get; set; }
|
public uint Current { get; set; }
|
||||||
|
|
||||||
public bool IsFinished { get => Current >= Count; }
|
/// <summary>
|
||||||
|
/// 升级材料合并后的展示用持有量;未启用合并时为 <see langword="null"/>。
|
||||||
|
/// </summary>
|
||||||
|
public uint? MergeAdjustedCurrent { get; set; }
|
||||||
|
|
||||||
public string FormattedCount { get => $"{Current}/{Count}"; }
|
/// <summary>
|
||||||
|
/// 周本材料异梦转化池内调配后的展示用持有量;未启用时为 <see langword="null"/>。在 <see cref="MergeAdjustedCurrent"/> 之后应用。
|
||||||
|
/// </summary>
|
||||||
|
public uint? WeeklyBossInterchangeAdjustedCurrent { get; set; }
|
||||||
|
|
||||||
|
public uint DisplayCurrent { get => WeeklyBossInterchangeAdjustedCurrent ?? MergeAdjustedCurrent ?? Current; }
|
||||||
|
|
||||||
|
public bool IsFinished { get => DisplayCurrent >= Count; }
|
||||||
|
|
||||||
|
public string FormattedCount { get => $"{DisplayCurrent}/{Count}"; }
|
||||||
|
|
||||||
|
private bool HasStatisticsAdjustedDisplay { get => MergeAdjustedCurrent.HasValue || WeeklyBossInterchangeAdjustedCurrent.HasValue; }
|
||||||
|
|
||||||
|
/// <summary>未启用合并展示链时,使用紧凑 <see cref="FormattedCount"/>。</summary>
|
||||||
|
public bool ShowNonMergeCompactCount { get => !HasStatisticsAdjustedDisplay; }
|
||||||
|
|
||||||
|
/// <summary>已合并但合成前后有效持有量与背包数一致,不显示括号,仅「合并后 / 需求」加空格。</summary>
|
||||||
|
public bool ShowMergeSpacedWithoutParen { get => HasStatisticsAdjustedDisplay && DisplayCurrent == Current; }
|
||||||
|
|
||||||
|
/// <summary>合并后有效持有与背包原数不同,显示「合并后 (背包原数)」。</summary>
|
||||||
|
public bool ShowMergeInventoryParen { get => HasStatisticsAdjustedDisplay && DisplayCurrent != Current; }
|
||||||
|
|
||||||
|
/// <summary>首位合并显示量 > 背包原数时着红色(相对原库存变多)。</summary>
|
||||||
|
public bool MergeDisplayLeadUseRed { get => HasStatisticsAdjustedDisplay && DisplayCurrent > Current; }
|
||||||
|
|
||||||
|
/// <summary>首位合并显示量 < 背包原数时着绿色(相对原库存变少,如低档被向上消耗)。</summary>
|
||||||
|
public bool MergeDisplayLeadUseGreen { get => HasStatisticsAdjustedDisplay && DisplayCurrent < Current; }
|
||||||
|
|
||||||
|
/// <summary>背包原数,紧接在合并后数字后,如 <c>(44)</c>。</summary>
|
||||||
|
public string RawInventoryParenthetical { get => $"({Current})"; }
|
||||||
|
|
||||||
|
/// <summary>有括号行:「/需求」紧接括号,如 <c>/55</c>。</summary>
|
||||||
|
public string SlashCountSuffixForParen { get => $"/{Count}"; }
|
||||||
|
|
||||||
|
/// <summary>无括号行:「 / 需求」含空格。</summary>
|
||||||
|
public string SlashCountSuffix { get => $" / {Count}"; }
|
||||||
|
|
||||||
public bool IsToday { get => Inner.IsItemOfToday(offset, true); }
|
public bool IsToday { get => Inner.IsItemOfToday(offset, true); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料统计右键浮层中按行展示的「未完成」养成条目(每人一行,名称前为角色/武器图标,需求量以括号标注)。
|
||||||
|
/// </summary>
|
||||||
|
public ImmutableArray<StatisticsConsumerMenuLine> StatisticsConsumerMenuLines { get; set; }
|
||||||
|
|
||||||
internal bool ExcludedFromPresentation { get; set; }
|
internal bool ExcludedFromPresentation { get; set; }
|
||||||
|
|
||||||
public static StatisticsCultivateItem Create(Material inner, TimeSpan offset)
|
public static StatisticsCultivateItem Create(Material inner, TimeSpan offset)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.ViewModel.GachaLog;
|
||||||
|
|
||||||
|
internal sealed class GachaLogImportedMessage
|
||||||
|
{
|
||||||
|
public static readonly GachaLogImportedMessage Empty = new();
|
||||||
|
|
||||||
|
private GachaLogImportedMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ namespace Snap.Hutao.ViewModel.GachaLog;
|
|||||||
|
|
||||||
[BindableCustomPropertyProvider]
|
[BindableCustomPropertyProvider]
|
||||||
[Service(ServiceLifetime.Scoped)]
|
[Service(ServiceLifetime.Scoped)]
|
||||||
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
|
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel, IRecipient<GachaLogImportedMessage>
|
||||||
{
|
{
|
||||||
private readonly IContentDialogFactory contentDialogFactory;
|
private readonly IContentDialogFactory contentDialogFactory;
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
@@ -127,6 +127,22 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
|
|||||||
UpdateStatisticsAsync(Archives?.CurrentItem).SafeForget();
|
UpdateStatisticsAsync(Archives?.CurrentItem).SafeForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void Receive(GachaLogImportedMessage message)
|
||||||
|
{
|
||||||
|
await RefreshArchiveCollectionAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask RefreshArchiveCollectionAsync()
|
||||||
|
{
|
||||||
|
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
IAdvancedDbCollectionView<GachaArchive> archives = await gachaLogService.RefreshArchiveCollectionAsync().ConfigureAwait(false);
|
||||||
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
Archives = archives;
|
||||||
|
Archives.MoveCurrentTo(Archives.Source.SelectedOrFirstOrDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Command("RefreshByWebCacheCommand")]
|
[Command("RefreshByWebCacheCommand")]
|
||||||
private async Task RefreshByWebCacheAsync()
|
private async Task RefreshByWebCacheAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -193,11 +193,11 @@ internal sealed partial class SpiralAbyssViewModel : Abstraction.ViewModel, IRec
|
|||||||
{
|
{
|
||||||
HutaoResponse response = await spiralAbyssClient.UploadRecordAsync(record).ConfigureAwait(false);
|
HutaoResponse response = await spiralAbyssClient.UploadRecordAsync(record).ConfigureAwait(false);
|
||||||
|
|
||||||
if (response is ILocalizableResponse localizableResponse)
|
if (response is ILocalizableResponse)
|
||||||
{
|
{
|
||||||
messenger.Send(InfoBarMessage.Any(
|
messenger.Send(InfoBarMessage.Any(
|
||||||
response is { ReturnCode: 0 } ? InfoBarSeverity.Success : InfoBarSeverity.Warning,
|
response is { ReturnCode: 0 } ? InfoBarSeverity.Success : InfoBarSeverity.Warning,
|
||||||
localizableResponse.GetLocalizationMessage()));
|
response.GetLocalizationMessageOrMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using Snap.Hutao.Service.Notification;
|
|||||||
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
||||||
using Snap.Hutao.UI.Xaml.Data;
|
using Snap.Hutao.UI.Xaml.Data;
|
||||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||||
|
|
||||||
@@ -155,7 +156,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
|||||||
Strategy = deltaOptions.Strategy,
|
Strategy = deltaOptions.Strategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
InfoBarMessage? message = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false) switch
|
ConsumptionSaveResult result = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false);
|
||||||
|
InfoBarMessage? message = result.Kind switch
|
||||||
{
|
{
|
||||||
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
||||||
ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
||||||
@@ -168,6 +170,11 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
|||||||
{
|
{
|
||||||
messenger.Send(message);
|
messenger.Send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.Kind is not ConsumptionSaveResultKind.NoProject)
|
||||||
|
{
|
||||||
|
messenger.Send(CultivationProjectEntriesChangedMessage.Empty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (HutaoException ex)
|
catch (HutaoException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using Snap.Hutao.Service.Notification;
|
|||||||
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
|
||||||
using Snap.Hutao.UI.Xaml.Data;
|
using Snap.Hutao.UI.Xaml.Data;
|
||||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
using Snap.Hutao.ViewModel.Cultivation;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||||
|
|
||||||
@@ -146,7 +147,8 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
|||||||
Strategy = deltaOptions.Strategy,
|
Strategy = deltaOptions.Strategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
InfoBarMessage? message = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false) switch
|
ConsumptionSaveResult result = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false);
|
||||||
|
InfoBarMessage? message = result.Kind switch
|
||||||
{
|
{
|
||||||
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
|
||||||
ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
|
||||||
@@ -159,6 +161,11 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
|||||||
{
|
{
|
||||||
messenger.Send(message);
|
messenger.Send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.Kind is not ConsumptionSaveResultKind.NoProject)
|
||||||
|
{
|
||||||
|
messenger.Send(CultivationProjectEntriesChangedMessage.Empty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (HutaoException ex)
|
catch (HutaoException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ namespace Snap.Hutao.Web.Endpoint.Hutao;
|
|||||||
[Service(ServiceLifetime.Singleton, typeof(IHutaoEndpoints), Key = HutaoEndpointsKind.Release)]
|
[Service(ServiceLifetime.Singleton, typeof(IHutaoEndpoints), Key = HutaoEndpointsKind.Release)]
|
||||||
internal sealed class HutaoEndpointsForRelease : IHutaoEndpoints
|
internal sealed class HutaoEndpointsForRelease : IHutaoEndpoints
|
||||||
{
|
{
|
||||||
string IHomaRootAccess.Root { get => "https://htserver.wdg.cloudns.ch/api"; }
|
string IHomaRootAccess.Root { get => "https://htserver.wdg12.work/api"; }
|
||||||
|
|
||||||
string IInfrastructureRootAccess.Root { get => "https://htserver.wdg.cloudns.ch/api"; }
|
string IInfrastructureRootAccess.Root { get => "https://htserver.wdg12.work/api"; }
|
||||||
|
|
||||||
string IInfrastructureRawRootAccess.RawRoot { get => "https://htserver.wdg.cloudns.ch/api"; }
|
string IInfrastructureRawRootAccess.RawRoot { get => "https://htserver.wdg12.work/api"; }
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ namespace Snap.Hutao.Web.Endpoint.Hutao;
|
|||||||
|
|
||||||
internal static class StaticResourcesEndpoints
|
internal static class StaticResourcesEndpoints
|
||||||
{
|
{
|
||||||
public static string Root { get => "https://htserver.wdg.cloudns.ch"; }
|
public static string Root { get => "https://htserver.wdg12.work"; }
|
||||||
|
|
||||||
public static Uri UIIconNone { get; } = StaticRaw("Bg", "UI_Icon_None.png").ToUri();
|
public static Uri UIIconNone { get; } = StaticRaw("Bg", "UI_Icon_None.png").ToUri();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ internal sealed class HutaoResponse : Web.Response.Response, ILocalizableRespons
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrDefault());
|
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +48,6 @@ internal sealed class HutaoResponse<TData> : Response<TData>, ILocalizableRespon
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrDefault());
|
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Web.ThirdPartyTool;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本地工具信息,用于保存工具的本地状态
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LocalToolInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 工具名称
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工具版本号
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为压缩包
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("is_compressed")]
|
||||||
|
public bool IsCompressed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主可执行文件名
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("main_exe")]
|
||||||
|
public string? MainExe { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载时间
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("download_time")]
|
||||||
|
public DateTimeOffset DownloadTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 ToolInfo 创建 LocalToolInfo
|
||||||
|
/// </summary>
|
||||||
|
public static LocalToolInfo FromToolInfo(ToolInfo toolInfo)
|
||||||
|
{
|
||||||
|
return new LocalToolInfo
|
||||||
|
{
|
||||||
|
Name = toolInfo.Name,
|
||||||
|
Version = toolInfo.Version,
|
||||||
|
IsCompressed = toolInfo.IsCompressed,
|
||||||
|
MainExe = toolInfo.MainExe,
|
||||||
|
DownloadTime = DateTimeOffset.Now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,4 +16,13 @@ internal sealed class ToolInfo
|
|||||||
|
|
||||||
[JsonPropertyName("files")]
|
[JsonPropertyName("files")]
|
||||||
public List<string> Files { get; set; } = default!;
|
public List<string> Files { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("is_compressed")]
|
||||||
|
public bool IsCompressed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("main_exe")]
|
||||||
|
public string? MainExe { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; set; } = default!;
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,10 @@ internal static unsafe class HutaoNativeMethods
|
|||||||
// ReSharper restore InconsistentNaming
|
// ReSharper restore InconsistentNaming
|
||||||
public const string DllName = "Snap.Hutao.Native.dll";
|
public const string DllName = "Snap.Hutao.Native.dll";
|
||||||
|
|
||||||
|
// Console APIs
|
||||||
|
public const int STD_OUTPUT_HANDLE = -11;
|
||||||
|
public const int STD_ERROR_HANDLE = -12;
|
||||||
|
|
||||||
public static HutaoNative HutaoCreateInstance()
|
public static HutaoNative HutaoCreateInstance()
|
||||||
{
|
{
|
||||||
nint pv = default;
|
nint pv = default;
|
||||||
@@ -54,6 +58,36 @@ internal static unsafe class HutaoNativeMethods
|
|||||||
return HutaoHResultIsWin32(hr, error);
|
return HutaoHResultIsWin32(hr, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a new console for the calling process.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>TRUE if the function succeeds; otherwise, FALSE.</returns>
|
||||||
|
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||||
|
public static extern BOOL AllocConsole();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detaches the calling process from its console.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>TRUE if the function succeeds; otherwise, FALSE.</returns>
|
||||||
|
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||||
|
public static extern BOOL FreeConsole();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches the calling process to the console of the specified process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dwProcessId">The identifier of the process whose console is to be used.</param>
|
||||||
|
/// <returns>TRUE if the function succeeds; otherwise, FALSE.</returns>
|
||||||
|
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||||
|
public static extern BOOL AttachConsole(uint dwProcessId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a handle to the specified standard device.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nStdHandle">The standard device.</param>
|
||||||
|
/// <returns>A handle to the specified device.</returns>
|
||||||
|
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||||
|
public static extern nint GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
[DllImport(DllName, CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
|
[DllImport(DllName, CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
|
||||||
private static extern HRESULT HutaoCreateInstance(HutaoNative.Vftbl** ppv);
|
private static extern HRESULT HutaoCreateInstance(HutaoNative.Vftbl** ppv);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user