Merge branch 'main' into d-v3
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>BetterGI</AssemblyName>
|
||||
<Version>0.49.0</Version>
|
||||
<Version>0.49.1-alpha.3</Version>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<PackageReference Include="BetterGI.VCRuntime" Version="14.44.35208" />
|
||||
<PackageReference Include="BetterGI.Assets.Map" Version="1.0.9" />
|
||||
<PackageReference Include="BetterGI.Assets.Model" Version="1.0.9" />
|
||||
<PackageReference Include="BetterGI.Assets.Model" Version="1.0.10" />
|
||||
<PackageReference Include="BetterGI.Assets.Other" Version="1.0.8" />
|
||||
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
@@ -177,12 +177,6 @@
|
||||
<None Update="User\**">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="GameTask\GetGridIcons\gridIcon.onnx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="GameTask\GetGridIcons\训练集原型特征.csv">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="GameTask\LogParse\Assets\log.css">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
@@ -192,6 +186,9 @@
|
||||
<None Update="GameTask\GameLoading\Assets\1920x1080\enter_game.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="GameTask\Common\Element\Assets\1920x1080\in_domain.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Wpf.Ui.Controls;
|
||||
@@ -47,7 +48,7 @@ public partial class CommonConfig : ObservableObject
|
||||
/// 当前主题类型(新版主题)
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ThemeType _currentThemeType = ThemeType.DarkMica;
|
||||
private ThemeType _currentThemeType = OsVersionHelper.IsWindows11_22523_OrGreater? ThemeType.DarkMica : ThemeType.DarkNone;
|
||||
|
||||
/// <summary>
|
||||
/// 主题(旧版主题,兼容性保留)
|
||||
|
||||
@@ -101,7 +101,7 @@ public partial class OtherConfig : ObservableObject
|
||||
/// PaddleOCR模型配置
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private PaddleOcrModelConfig _paddleOcrModelConfig = PaddleOcrModelConfig.V4;
|
||||
private PaddleOcrModelConfig _paddleOcrModelConfig = PaddleOcrModelConfig.V4Auto;
|
||||
}
|
||||
|
||||
//public partial class OtherConfig : ObservableObject
|
||||
|
||||
@@ -89,6 +89,10 @@ public class OcrFactory : IDisposable
|
||||
{
|
||||
return _config.PaddleOcrModelConfig switch
|
||||
{
|
||||
PaddleOcrModelConfig.V4Auto =>
|
||||
new PaddleOcrService(App.ServiceProvider.GetRequiredService<BgiOnnxFactory>(),
|
||||
PaddleOcrService.PaddleOcrModelType.FromCultureInfoV4(GetCultureInfo()) ??
|
||||
PaddleOcrService.PaddleOcrModelType.V4),
|
||||
PaddleOcrModelConfig.V5Auto =>
|
||||
new PaddleOcrService(App.ServiceProvider.GetRequiredService<BgiOnnxFactory>(),
|
||||
PaddleOcrService.PaddleOcrModelType.FromCultureInfo(GetCultureInfo()) ??
|
||||
|
||||
@@ -201,6 +201,43 @@ public class PaddleOcrService : IOcrService, IDisposable
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 中英文优先使用V4模型,其他语言使用V5模型
|
||||
/// </summary>
|
||||
/// <param name="cultureInfo"></param>
|
||||
/// <returns></returns>
|
||||
public static PaddleOcrModelType? FromCultureInfoV4(CultureInfo cultureInfo)
|
||||
{
|
||||
var v5 = FromCultureInfo(cultureInfo);
|
||||
// 如果用的是v5, 那么优先用V4的细分模型
|
||||
if (v5 == V5)
|
||||
{
|
||||
List<string> names =
|
||||
[
|
||||
cultureInfo.EnglishName.ToLowerInvariant(), cultureInfo.Name.ToLowerInvariant(),
|
||||
cultureInfo.ThreeLetterISOLanguageName.ToLowerInvariant(),
|
||||
cultureInfo.TwoLetterISOLanguageName.ToLowerInvariant()
|
||||
];
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (name.Equals("en"))
|
||||
{
|
||||
return V4En;
|
||||
}
|
||||
else if (name.Equals("zh-hant") || name.Equals("zh-tw") || name.Equals("zh-hk"))
|
||||
{
|
||||
return V5;
|
||||
}
|
||||
}
|
||||
|
||||
return V4;
|
||||
}
|
||||
else
|
||||
{
|
||||
return v5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PaddleOcrService(BgiOnnxFactory bgiOnnxFactory, PaddleOcrModelType modelType)
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
public enum PaddleOcrModelConfig
|
||||
{
|
||||
V5Auto,
|
||||
V5,
|
||||
V4,
|
||||
V4En,
|
||||
V5Latin,
|
||||
V5Eslav,
|
||||
V5Korean,
|
||||
V4Auto = 2,
|
||||
V5Auto = 0,
|
||||
V4 = 7,
|
||||
V4En = 3,
|
||||
V5 = 1,
|
||||
V5Latin = 4,
|
||||
V5Eslav = 5,
|
||||
V5Korean = 6,
|
||||
}
|
||||
@@ -7,14 +7,14 @@ using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation;
|
||||
using BetterGenshinImpact.GameTask.AutoPathing.Handler;
|
||||
using BetterGenshinImpact.GameTask.AutoWood;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.ViewModel.Pages;
|
||||
using Microsoft.ClearScript;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.ClearScript;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterGenshinImpact.Core.Script.Dependence;
|
||||
@@ -23,7 +23,7 @@ public class Dispatcher
|
||||
{
|
||||
private readonly ILogger<Dispatcher> _logger = App.GetLogger<Dispatcher>();
|
||||
|
||||
private object _config = null;
|
||||
private readonly object _config;
|
||||
|
||||
public Dispatcher(object config)
|
||||
{
|
||||
@@ -111,7 +111,7 @@ public class Dispatcher
|
||||
/// <param name="customCt">自定义取消令牌,允许从JS控制任务取消</param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public async Task RunTask(SoloTask soloTask, CancellationToken? customCt = null)
|
||||
public async Task<object?> RunTask(SoloTask soloTask, CancellationToken? customCt = null)
|
||||
{
|
||||
if (soloTask == null)
|
||||
{
|
||||
@@ -140,119 +140,129 @@ public class Dispatcher
|
||||
// 根据名称执行任务
|
||||
switch (soloTask.Name)
|
||||
{
|
||||
case "AutoGeniusInvokation":
|
||||
string content;
|
||||
case "AutoGeniusInvokation":
|
||||
string content;
|
||||
// 检查是否有自定义策略内容
|
||||
if (soloTask.Config != null)
|
||||
{
|
||||
var jsObject = (ScriptObject)soloTask.Config;
|
||||
content = ScriptObjectConverter.GetValue(jsObject, "strategy", "");
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
if (soloTask.Config != null)
|
||||
{
|
||||
var jsObject = (ScriptObject)soloTask.Config;
|
||||
content = ScriptObjectConverter.GetValue(jsObject, "strategy", "");
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
// 回退到原有逻辑
|
||||
if (taskSettingsPageViewModel.GetTcgStrategy(out content))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (taskSettingsPageViewModel.GetTcgStrategy(out content))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 回退到原有逻辑
|
||||
if (taskSettingsPageViewModel.GetTcgStrategy(out content))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await new AutoGeniusInvokationTask(new GeniusInvokationTaskParam(content)).Start(cancellationToken);
|
||||
break;
|
||||
if (taskSettingsPageViewModel.GetTcgStrategy(out content))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
await new AutoGeniusInvokationTask(new GeniusInvokationTaskParam(content)).Start(cancellationToken);
|
||||
return null;
|
||||
|
||||
case "AutoWood":
|
||||
await new AutoWoodTask(new WoodTaskParam(taskSettingsPageViewModel.AutoWoodRoundNum,
|
||||
taskSettingsPageViewModel.AutoWoodDailyMaxCount)).Start(cancellationToken);
|
||||
break;
|
||||
return null;
|
||||
|
||||
case "AutoFight":
|
||||
await new AutoFightHandler().RunAsyncByScript(cancellationToken, null, _config);
|
||||
break;
|
||||
return null;
|
||||
|
||||
case "AutoDomain":
|
||||
if (taskSettingsPageViewModel.GetFightStrategy(out var path))
|
||||
{
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
await new AutoDomainTask(new AutoDomainParam(0, path)).Start(cancellationToken);
|
||||
break;
|
||||
return null;
|
||||
|
||||
case "AutoFishing":
|
||||
await new AutoFishingTask(AutoFishingTaskParam.BuildFromSoloTaskConfig(soloTask.Config)).Start(
|
||||
cancellationToken);
|
||||
break;
|
||||
return null;
|
||||
case "AutoEat":
|
||||
string? foodName = soloTask.Config == null ? null : ScriptObjectConverter.GetValue<string?>((ScriptObject)soloTask.Config, "foodName", null);
|
||||
FoodEffectType? foodEffectType = soloTask.Config == null ? null : (FoodEffectType?)ScriptObjectConverter.GetValue<int?>((ScriptObject)soloTask.Config, "foodEffectType", null);
|
||||
|
||||
if (foodName != null && foodEffectType != null)
|
||||
{
|
||||
throw new NotSupportedException("不能同时指定foodName和foodEffectType");
|
||||
}
|
||||
string? foodName = soloTask.Config == null ? null : ScriptObjectConverter.GetValue<string?>((ScriptObject)soloTask.Config, "foodName", null);
|
||||
FoodEffectType? foodEffectType = soloTask.Config == null ? null : (FoodEffectType?)ScriptObjectConverter.GetValue<int?>((ScriptObject)soloTask.Config, "foodEffectType", null);
|
||||
|
||||
if (foodName == null)
|
||||
{
|
||||
if (foodEffectType != null)
|
||||
if (foodName != null && foodEffectType != null)
|
||||
{
|
||||
PathingPartyConfig? pathingPartyConfig = _config as PathingPartyConfig;
|
||||
if (pathingPartyConfig == null)
|
||||
throw new NotSupportedException("不能同时指定foodName和foodEffectType");
|
||||
}
|
||||
|
||||
if (foodName == null)
|
||||
{
|
||||
if (foodEffectType != null)
|
||||
{
|
||||
throw new NotSupportedException("foodEffectType参数需要调度器配置,请在调度器下使用");
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (foodEffectType)
|
||||
PathingPartyConfig? pathingPartyConfig = _config as PathingPartyConfig;
|
||||
if (pathingPartyConfig == null)
|
||||
{
|
||||
case FoodEffectType.ATKBoostingDish:
|
||||
foodName = pathingPartyConfig.AutoEatConfig.DefaultAtkBoostingDishName;
|
||||
if (foodName == null)
|
||||
{
|
||||
_logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的攻击类料理");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case FoodEffectType.AdventurersDish:
|
||||
foodName = pathingPartyConfig.AutoEatConfig.DefaultAdventurersDishName;
|
||||
if (foodName == null)
|
||||
{
|
||||
_logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的冒险类料理");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case FoodEffectType.DEFBoostingDish:
|
||||
foodName = pathingPartyConfig.AutoEatConfig.DefaultDefBoostingDishName;
|
||||
if (foodName == null)
|
||||
{
|
||||
_logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的防御类料理");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("JS脚本入参错误:错误的foodEffectType");
|
||||
throw new NotSupportedException("foodEffectType参数需要调度器配置,请在调度器下使用");
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (foodEffectType)
|
||||
{
|
||||
case FoodEffectType.ATKBoostingDish:
|
||||
foodName = pathingPartyConfig.AutoEatConfig.DefaultAtkBoostingDishName;
|
||||
if (foodName == null)
|
||||
{
|
||||
_logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的攻击类料理");
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case FoodEffectType.AdventurersDish:
|
||||
foodName = pathingPartyConfig.AutoEatConfig.DefaultAdventurersDishName;
|
||||
if (foodName == null)
|
||||
{
|
||||
_logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的冒险类料理");
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case FoodEffectType.DEFBoostingDish:
|
||||
foodName = pathingPartyConfig.AutoEatConfig.DefaultDefBoostingDishName;
|
||||
if (foodName == null)
|
||||
{
|
||||
_logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的防御类料理");
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("JS脚本入参错误:错误的foodEffectType");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var autoEatConfig = TaskContext.Instance().Config.AutoEatConfig;
|
||||
return await new AutoEatTask(new AutoEatParam()
|
||||
{
|
||||
CheckInterval = autoEatConfig.CheckInterval,
|
||||
EatInterval = autoEatConfig.EatInterval,
|
||||
ShowNotification = autoEatConfig.ShowNotification,
|
||||
FoodName = foodName
|
||||
}).Start(cancellationToken);
|
||||
}
|
||||
|
||||
var autoEatConfig = TaskContext.Instance().Config.AutoEatConfig;
|
||||
await new AutoEatTask(new AutoEatParam()
|
||||
case "CountInventoryItem":
|
||||
{
|
||||
CheckInterval = autoEatConfig.CheckInterval,
|
||||
EatInterval = autoEatConfig.EatInterval,
|
||||
ShowNotification = autoEatConfig.ShowNotification,
|
||||
FoodName = foodName
|
||||
}).Start(cancellationToken);
|
||||
|
||||
break;
|
||||
if (soloTask.Config == null)
|
||||
{
|
||||
throw new NullReferenceException($"{nameof(soloTask.Config)}为空");
|
||||
}
|
||||
GridScreenName gridScreenName = ScriptObjectConverter.GetValue<GridScreenName?>((ScriptObject)soloTask.Config, "gridScreenName", null) ?? throw new Exception("gridScreenName为空或错误");
|
||||
string foodName = ScriptObjectConverter.GetValue<string?>((ScriptObject)soloTask.Config, "foodName", null) ?? throw new Exception("foodName为空");
|
||||
return await new CountInventoryItem(gridScreenName, foodName).Start(cancellationToken);
|
||||
}
|
||||
default:
|
||||
throw new ArgumentException($"未知的任务名称: {soloTask.Name}", nameof(soloTask.Name));
|
||||
}
|
||||
|
||||
@@ -57,7 +57,12 @@ public class EngineExtend
|
||||
engine.AddHostType("Region", typeof(Region));
|
||||
|
||||
engine.AddHostType("CombatScenes", typeof(CombatScenes));
|
||||
engine.AddHostType("CombatScenes", typeof(Avatar));
|
||||
engine.AddHostType("Avatar", typeof(Avatar));
|
||||
|
||||
|
||||
engine.AddHostObject("OpenCvSharp", new HostTypeCollection("OpenCvSharp"));
|
||||
|
||||
|
||||
|
||||
|
||||
// 添加C#的类型
|
||||
|
||||
@@ -7,6 +7,8 @@ using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.Model;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterGenshinImpact.Core.Script.Project;
|
||||
|
||||
@@ -77,4 +79,21 @@ public class Manifest
|
||||
|
||||
return settingItems;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string ShortDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
var lines = this.Description.Split('\n');
|
||||
if (lines.Length > 6)
|
||||
{
|
||||
return String.Join('\n', lines.Take(6).Append("……"));
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.Description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Windows.UI.Xaml.Automation;
|
||||
using BetterGenshinImpact.View.Windows;
|
||||
using LibGit2Sharp;
|
||||
using LibGit2Sharp.Handlers;
|
||||
@@ -120,9 +121,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
// 如果仓库不存在,执行浅克隆操作
|
||||
_logger.LogInformation($"浅克隆仓库: {repoUrl} 到 {repoPath}");
|
||||
|
||||
CloneRepository(repoUrl, repoPath, onCheckoutProgress);
|
||||
|
||||
// CloneRepository(repoUrl, repoPath);
|
||||
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
|
||||
updated = true;
|
||||
}
|
||||
else
|
||||
@@ -142,68 +141,76 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
_logger.LogWarning(ex, "备份repo.json失败,继续更新仓库");
|
||||
}
|
||||
|
||||
// 检查是否为有效的Git仓库
|
||||
if (!Repository.IsValid(repoPath))
|
||||
{
|
||||
Toast.Error($"不是有效的Git仓库,将重新克隆");
|
||||
UIDispatcherHelper.Invoke(() => Toast.Error("不是有效的Git仓库,将重新克隆"));
|
||||
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
|
||||
updated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
using var repo = new Repository(repoPath);
|
||||
|
||||
// 检查远程URL是否需要更新
|
||||
var origin = repo.Network.Remotes["origin"];
|
||||
if (origin.Url != repoUrl)
|
||||
{
|
||||
// 远程URL已更改,需要更新
|
||||
_logger.LogInformation($"更新远程URL: 从 {origin.Url} 到 {repoUrl}");
|
||||
repo.Network.Remotes.Update("origin", r => r.Url = repoUrl);
|
||||
// 远程URL已更改,需要删除重新克隆
|
||||
_logger.LogInformation($"远程URL已更改: 从 {origin.Url} 到 {repoUrl},将重新克隆");
|
||||
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
|
||||
updated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取远程分支信息
|
||||
// 获取远程分支信息并拉取最新数据
|
||||
var remote = repo.Network.Remotes["origin"];
|
||||
var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification);
|
||||
|
||||
// 使用浅拉取选项
|
||||
var fetchOptions = new FetchOptions();
|
||||
fetchOptions.ProxyOptions.ProxyType = ProxyType.None;
|
||||
var fetchOptions = new FetchOptions
|
||||
{
|
||||
ProxyOptions = { ProxyType = ProxyType.None }
|
||||
};
|
||||
|
||||
Commands.Fetch(repo, remote.Name, refSpecs, fetchOptions, "拉取最新更新");
|
||||
|
||||
// 获取当前分支
|
||||
var branch = repo.Branches["refs/heads/origin/main"] ?? repo.Branches["main"];
|
||||
if (branch == null)
|
||||
{
|
||||
throw new Exception("未找到main或master分支");
|
||||
}
|
||||
|
||||
// 如果是本地分支,需要设置上游分支
|
||||
if (!branch.IsRemote)
|
||||
{
|
||||
var trackingBranch = repo.Branches[$"origin/{branch.FriendlyName}"];
|
||||
if (trackingBranch != null && branch.TrackedBranch == null)
|
||||
{
|
||||
branch = repo.Branches.Update(branch,
|
||||
b => b.TrackedBranch = trackingBranch.CanonicalName);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
// 获取本地和远程commit
|
||||
var currentCommitSha = repo.Head.Tip.Sha;
|
||||
|
||||
// 合并或重置到最新
|
||||
if (branch.TrackedBranch != null)
|
||||
// 获取远程release分支的最新commit
|
||||
var remoteBranch = repo.Branches["refs/remotes/origin/release"];
|
||||
if (remoteBranch == null)
|
||||
{
|
||||
var trackingBranch = branch.TrackedBranch;
|
||||
var mergeResult = Commands.Pull(
|
||||
repo,
|
||||
new Signature("BetterGI", "auto@bettergi.com", DateTimeOffset.Now),
|
||||
new PullOptions());
|
||||
throw new Exception("未找到远程release分支");
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
updated = currentCommitSha != repo.Head.Tip.Sha;
|
||||
var remoteCommitSha = remoteBranch.Tip.Sha;
|
||||
|
||||
// 比较本地和远程commit
|
||||
if (currentCommitSha == remoteCommitSha)
|
||||
{
|
||||
_logger.LogInformation("本地仓库已是最新版本,无需更新");
|
||||
updated = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"检测到远程更新: 本地 {currentCommitSha[..7]} -> 远程 {remoteCommitSha[..7]},将重新克隆");
|
||||
|
||||
// commit不一致,删除本地仓库重新克隆
|
||||
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Git仓库更新失败");
|
||||
throw;
|
||||
UIDispatcherHelper.Invoke(() => Toast.Error("脚本仓库更新异常,直接删除后重新克隆\n原因:" + ex.Message));
|
||||
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果仓库有更新,则标记新repo.json中的更新节点
|
||||
if (updated)
|
||||
{
|
||||
@@ -214,12 +221,12 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
if (newRepoJsonPath != null)
|
||||
{
|
||||
var newRepoJsonContent = await File.ReadAllTextAsync(newRepoJsonPath);
|
||||
|
||||
|
||||
// 检查是否存在repo_update.json,如果存在则直接与它比对
|
||||
var parentDir = Path.GetDirectoryName(repoPath);
|
||||
var repoUpdateJsonPath = Path.Combine(parentDir!, "repo_update.json");
|
||||
string updatedContent;
|
||||
|
||||
|
||||
if (File.Exists(repoUpdateJsonPath))
|
||||
{
|
||||
try
|
||||
@@ -355,7 +362,6 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 解析lastUpdated时间戳
|
||||
/// </summary>
|
||||
@@ -384,6 +390,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
var options = new CloneOptions
|
||||
{
|
||||
BranchName = "release", // 指定分支
|
||||
Checkout = true,
|
||||
IsBare = false,
|
||||
RecurseSubmodules = false, // 不递归克隆子模块
|
||||
@@ -396,54 +403,45 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// 相当于 Repository.Clone(repoUrl, repoPath, options);
|
||||
/// 相当于 Repository.Clone(repoUrl, repoPath, options);
|
||||
/// 用这个方法可以无视本地代理
|
||||
/// </summary>
|
||||
/// <param name="repoUrl"></param>
|
||||
/// <param name="repoPath"></param>
|
||||
/// <param name="branchName"></param>
|
||||
/// <param name="onCheckoutProgress"></param>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private void CloneRepository(string repoUrl, string repoPath, CheckoutProgressHandler? onCheckoutProgress)
|
||||
private void CloneRepository(string repoUrl, string repoPath, string branchName, CheckoutProgressHandler? onCheckoutProgress)
|
||||
{
|
||||
// 1. 创建目录
|
||||
DirectoryHelper.DeleteReadOnlyDirectory(repoPath);
|
||||
Directory.CreateDirectory(repoPath);
|
||||
|
||||
// 2. 初始化 Git 仓库
|
||||
Repository.Init(repoPath);
|
||||
|
||||
using var repo = new Repository(repoPath);
|
||||
GitConfig(repo);
|
||||
|
||||
// 3. 添加远程源
|
||||
// 添加远程源
|
||||
Remote remote = repo.Network.Remotes.Add("origin", repoUrl);
|
||||
|
||||
// 4. 获取数据(使用浅克隆选项)
|
||||
// 只拉取指定分支
|
||||
var fetchOptions = new FetchOptions
|
||||
{
|
||||
TagFetchMode = TagFetchMode.None // 不获取标签
|
||||
TagFetchMode = TagFetchMode.None,
|
||||
ProxyOptions = { ProxyType = ProxyType.None }
|
||||
};
|
||||
fetchOptions.ProxyOptions.ProxyType = ProxyType.None;
|
||||
string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}";
|
||||
Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取");
|
||||
|
||||
// 5. 执行获取操作
|
||||
Commands.Fetch(repo, remote.Name, ["+refs/heads/*:refs/remotes/origin/*"], fetchOptions, "初始化拉取");
|
||||
|
||||
// 6. 创建本地分支并跟踪远程分支
|
||||
var remoteBranch = repo.Branches["refs/remotes/origin/main"] ?? repo.Branches["refs/remotes/origin/master"];
|
||||
// 获取远程分支
|
||||
var remoteBranch = repo.Branches[$"refs/remotes/origin/{branchName}"];
|
||||
if (remoteBranch == null)
|
||||
{
|
||||
throw new Exception("远程仓库中未找到 main 或 master 分支");
|
||||
}
|
||||
throw new Exception($"远程仓库中未找到 {branchName} 分支");
|
||||
|
||||
// 7. 创建并检出本地分支
|
||||
var localBranch = repo.CreateBranch(remoteBranch.FriendlyName, remoteBranch.Tip);
|
||||
|
||||
// 8. 设置本地分支跟踪远程分支
|
||||
// 创建并检出本地分支
|
||||
var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip);
|
||||
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
|
||||
|
||||
// 9. 检出分支
|
||||
CheckoutOptions checkoutOptions = new CheckoutOptions
|
||||
{
|
||||
OnCheckoutProgress = onCheckoutProgress
|
||||
};
|
||||
var checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress };
|
||||
Commands.Checkout(repo, localBranch, checkoutOptions);
|
||||
}
|
||||
|
||||
@@ -795,14 +793,14 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
var scriptPath = Path.Combine(repoPath, path);
|
||||
var destPath = Path.Combine(userPath, remainingPath);
|
||||
|
||||
|
||||
// 备份需要保存的文件
|
||||
List<string> backupFiles = new List<string>();
|
||||
if (first == "js") // 只对JS脚本进行备份
|
||||
{
|
||||
backupFiles = BackupScriptFiles(path, repoPath);
|
||||
}
|
||||
|
||||
|
||||
if (Directory.Exists(scriptPath))
|
||||
{
|
||||
if (Directory.Exists(destPath))
|
||||
@@ -827,7 +825,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
|
||||
File.Copy(scriptPath, destPath, true);
|
||||
}
|
||||
|
||||
|
||||
// 恢复备份的文件
|
||||
if (first == "js" && backupFiles.Count > 0) // 只对JS脚本进行恢复
|
||||
{
|
||||
@@ -891,7 +889,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
var jsonContent = File.ReadAllText(repoJsonPath);
|
||||
var jsonObj = JObject.Parse(jsonContent);
|
||||
|
||||
|
||||
if (jsonObj["indexes"] is JArray indexes)
|
||||
{
|
||||
// 递归收集所有路径
|
||||
@@ -915,7 +913,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CollectPaths(indexes, "");
|
||||
}
|
||||
}
|
||||
@@ -925,7 +923,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
// 构建父子关系映射,只记录直接子节点
|
||||
var parentChildMap = new Dictionary<string, List<string>>();
|
||||
|
||||
|
||||
// 遍历所有路径,找到每个节点的父节点
|
||||
foreach (var path in allAvailablePaths)
|
||||
{
|
||||
@@ -937,31 +935,31 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
parentChildMap[parentPath] = new List<string>();
|
||||
}
|
||||
|
||||
|
||||
if (!parentChildMap[parentPath].Contains(path))
|
||||
{
|
||||
parentChildMap[parentPath].Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 递归检查父节点,直到没有新的父节点需要添加
|
||||
bool hasNewPaths;
|
||||
do
|
||||
{
|
||||
hasNewPaths = false;
|
||||
var pathsToAdd = new HashSet<string>();
|
||||
|
||||
|
||||
// 检查每个父节点
|
||||
foreach (var kvp in parentChildMap)
|
||||
{
|
||||
var parentPath = kvp.Key;
|
||||
var directChildren = kvp.Value;
|
||||
|
||||
|
||||
// 检查所有直接子节点是否都已被订阅
|
||||
bool allDirectChildrenSubscribed = directChildren.All(child =>
|
||||
bool allDirectChildrenSubscribed = directChildren.All(child =>
|
||||
pathsToKeep.Contains(child));
|
||||
|
||||
|
||||
// 如果所有直接子节点都已被订阅,且父节点本身未被订阅,则添加父节点
|
||||
if (allDirectChildrenSubscribed && !pathsToKeep.Contains(parentPath))
|
||||
{
|
||||
@@ -969,7 +967,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
hasNewPaths = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 将需要添加的父节点加入订阅列表
|
||||
foreach (var pathToAdd in pathsToAdd)
|
||||
{
|
||||
@@ -1103,18 +1101,18 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
private List<string> GetMatchedFiles(string basePath, string pattern)
|
||||
{
|
||||
var matchedFiles = new List<string>();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// 检查是否是正则表达式(以^开头或包含特殊字符)
|
||||
bool isRegex = pattern.StartsWith("^") || pattern.Contains(".*") || pattern.Contains("\\d") || pattern.Contains("\\w");
|
||||
|
||||
|
||||
if (isRegex)
|
||||
{
|
||||
// 使用正则表达式匹配
|
||||
var regex = new System.Text.RegularExpressions.Regex(pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
var allFiles = Directory.GetFiles(basePath, "*", SearchOption.AllDirectories);
|
||||
|
||||
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(basePath, file);
|
||||
@@ -1129,7 +1127,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
// 使用通配符匹配
|
||||
var searchPattern = Path.GetFileName(pattern);
|
||||
var searchDir = Path.GetDirectoryName(pattern);
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(searchDir))
|
||||
{
|
||||
// 只在当前目录搜索
|
||||
@@ -1152,7 +1150,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
_logger.LogError(ex, $"获取匹配文件时发生错误: {pattern}");
|
||||
}
|
||||
|
||||
|
||||
return matchedFiles;
|
||||
}
|
||||
|
||||
@@ -1215,6 +1213,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
savedFile += "/";
|
||||
isDir = true;
|
||||
}
|
||||
|
||||
if (isDir)
|
||||
{
|
||||
var dirPath = Path.Combine(scriptUserPath, savedFile.TrimEnd('/', '\\'));
|
||||
@@ -1241,6 +1240,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
Directory.CreateDirectory(backupFileDir);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Copy(matchedFile, backupFilePath, true);
|
||||
@@ -1251,6 +1251,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
_logger.LogError(ex, $"备份文件失败: {matchedFile}");
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedFiles.Count == 0)
|
||||
{
|
||||
_logger.LogWarning($"没有找到匹配的文件: {savedFile}");
|
||||
@@ -1262,6 +1263,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
_logger.LogError(ex, $"备份脚本文件时发生错误: {scriptPath}");
|
||||
}
|
||||
|
||||
return backupFiles;
|
||||
}
|
||||
|
||||
@@ -1302,6 +1304,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
_logger.LogWarning($"未知的脚本路径映射: {scriptPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptUserPath = Path.Combine(userPath, remainingPath);
|
||||
|
||||
// 还原所有备份文件
|
||||
@@ -1316,6 +1319,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
{
|
||||
Directory.CreateDirectory(restoreDir);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Copy(file, restorePath, true);
|
||||
@@ -1349,4 +1353,4 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
|
||||
_logger.LogError(ex, $"恢复脚本文件时发生错误: {scriptPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage
|
||||
{
|
||||
/// <summary>
|
||||
@@ -34,5 +36,24 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage
|
||||
public int Level { get; private set; }
|
||||
|
||||
// PS:圣遗物的种类和品质在点击查看之前就可以通过识别图标获悉,所以不必在此模型类中获取
|
||||
|
||||
/// <summary>
|
||||
/// 生成一个手工拼接的成员结构示意字符串
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string ToStructuredString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append("Properties").Append('\n');
|
||||
sb.Append("├─").Append("Name: ").Append(this.Name).Append('\n');
|
||||
sb.Append("├─").Append("MainAffix: ").Append(this.MainAffix.Type).Append(", ").Append(this.MainAffix.Value).Append('\n');
|
||||
sb.Append("├─").Append("MinorAffixes: ").Append('\n');
|
||||
for (int i = 0; i < this.MinorAffixes.Length; i++)
|
||||
{
|
||||
sb.Append('│').Append('\t').Append(i == this.MinorAffixes.Length - 1 ? "└─" : "├─").Append($"[{i}]: ").Append(this.MinorAffixes[i].Type).Append(", ").Append(this.MinorAffixes[i].Value).Append('\n');
|
||||
}
|
||||
sb.Append("└─").Append("Level: ").Append(this.Level);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@ public partial class AutoArtifactSalvageConfig : ObservableObject
|
||||
{
|
||||
// JavaScript
|
||||
[ObservableProperty]
|
||||
private string _javaScript =
|
||||
@"(async function (artifact) {
|
||||
var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK');
|
||||
var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF');
|
||||
Output = hasATK && hasDEF;
|
||||
})(ArtifactStat);";
|
||||
private string _javaScript = @"(async function (artifact) {
|
||||
var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK');
|
||||
var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF');
|
||||
Output = hasATK && hasDEF;
|
||||
})(ArtifactStat);";
|
||||
|
||||
// JavaScript
|
||||
[ObservableProperty]
|
||||
private string _artifactSetFilter = "";
|
||||
|
||||
// 正则表达式
|
||||
[Obsolete]
|
||||
@@ -28,4 +31,8 @@ public partial class AutoArtifactSalvageConfig : ObservableObject
|
||||
// 最多检查多少个圣遗物
|
||||
[ObservableProperty]
|
||||
private int _maxNumToCheck = 100;
|
||||
|
||||
// 单次识别失败政策
|
||||
[ObservableProperty]
|
||||
private RecognitionFailurePolicy _recognitionFailurePolicy = RecognitionFailurePolicy.Skip;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using BetterGenshinImpact.GameTask.GetGridIcons;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
@@ -16,8 +17,10 @@ using Microsoft.ClearScript;
|
||||
using Microsoft.ClearScript.V8;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
@@ -35,7 +38,7 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
/// </summary>
|
||||
public class AutoArtifactSalvageTask : ISoloTask
|
||||
{
|
||||
private readonly ILogger logger = App.GetLogger<AutoArtifactSalvageTask>();
|
||||
private readonly ILogger logger;
|
||||
private readonly InputSimulator input = Simulation.SendInput;
|
||||
|
||||
private CancellationToken ct;
|
||||
@@ -50,19 +53,28 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
|
||||
private readonly string? javaScript;
|
||||
|
||||
private readonly string? artifactSetFilter;
|
||||
|
||||
private readonly int? maxNumToCheck;
|
||||
|
||||
private readonly RecognitionFailurePolicy? recognitionFailurePolicy;
|
||||
|
||||
private readonly bool returnToMainUi = true;
|
||||
|
||||
private readonly CultureInfo cultureInfo;
|
||||
private readonly CultureInfo? cultureInfo;
|
||||
|
||||
public AutoArtifactSalvageTask(int star, string? javaScript = null, int? maxNumToCheck = null)
|
||||
private readonly FrozenDictionary<ArtifactAffixType, string> artifactAffixStrDic;
|
||||
|
||||
public AutoArtifactSalvageTask(AutoArtifactSalvageTaskParam param, ILogger? logger = null)
|
||||
{
|
||||
this.star = star;
|
||||
this.javaScript = javaScript;
|
||||
this.maxNumToCheck = maxNumToCheck;
|
||||
IStringLocalizer<AutoArtifactSalvageTask> stringLocalizer = App.GetService<IStringLocalizer<AutoArtifactSalvageTask>>() ?? throw new NullReferenceException();
|
||||
this.cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
|
||||
this.star = param.Star;
|
||||
this.javaScript = param.JavaScript;
|
||||
this.artifactSetFilter = param.ArtifactSetFilter;
|
||||
this.maxNumToCheck = param.MaxNumToCheck;
|
||||
this.recognitionFailurePolicy = param.RecognitionFailurePolicy;
|
||||
this.logger = logger ?? App.GetLogger<AutoArtifactSalvageTask>();
|
||||
var stringLocalizer = param.StringLocalizer ?? App.GetService<IStringLocalizer<AutoArtifactSalvageTask>>() ?? throw new NullReferenceException();
|
||||
this.cultureInfo = param.GameCultureInfo;
|
||||
quickSelectLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "快速选择");
|
||||
numOfStarLocalizedString =
|
||||
[
|
||||
@@ -71,14 +83,11 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
stringLocalizer.WithCultureGet(cultureInfo, "3星圣遗物"),
|
||||
stringLocalizer.WithCultureGet(cultureInfo, "4星圣遗物")
|
||||
];
|
||||
|
||||
artifactAffixStrDic = ArtifactAffix.DefaultStrDic.Select(kvp => new KeyValuePair<ArtifactAffixType, string>(kvp.Key, stringLocalizer.WithCultureGet(cultureInfo, kvp.Value))).ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public AutoArtifactSalvageTask(int star, bool returnToMainUi) : this(star)
|
||||
{
|
||||
this.returnToMainUi = returnToMainUi;
|
||||
}
|
||||
|
||||
public static async Task OpenBag(GridScreenName gridScreenName, InputSimulator input, ILogger logger, CancellationToken ct)
|
||||
public static async Task OpenInventory(GridScreenName gridScreenName, InputSimulator input, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
RecognitionObject? recognitionObjectChecked;
|
||||
RecognitionObject? recognitionObjectUnchecked;
|
||||
@@ -175,7 +184,7 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
}
|
||||
|
||||
await OpenBag(GridScreenName.Artifacts, this.input, this.logger, this.ct);
|
||||
await OpenInventory(GridScreenName.Artifacts, this.input, this.logger, this.ct);
|
||||
|
||||
// 点击分解按钮打开分解界面
|
||||
using var ra2 = CaptureToRectArea();
|
||||
@@ -224,7 +233,11 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
}
|
||||
}
|
||||
|
||||
Bv.ClickWhiteConfirmButton(ra4);
|
||||
using var quickSelectConfirmBtn = ra4.Find(ElementAssets.Instance.BtnWhiteConfirm);
|
||||
if (quickSelectConfirmBtn.IsExist())
|
||||
{
|
||||
quickSelectConfirmBtn.Click();
|
||||
}
|
||||
await Delay(1500, ct);
|
||||
|
||||
|
||||
@@ -260,7 +273,39 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
// 分解5星
|
||||
if (javaScript != null)
|
||||
{
|
||||
await Salvage5Star(this.javaScript, this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空"));
|
||||
if (!string.IsNullOrWhiteSpace(this.artifactSetFilter))
|
||||
{
|
||||
// 其实是点击筛选按钮……快速选择确认的这个按钮正好和筛选按钮位置重合,摆烂直接用了
|
||||
quickSelectConfirmBtn.Click();
|
||||
await Delay(400, ct);
|
||||
// 点击所属套装
|
||||
ra5.ClickTo(315, 205);
|
||||
await Delay(1000, ct);
|
||||
// 遍历套装Grid勾选套装
|
||||
using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary<string, float[]> prototypes);
|
||||
ArtifactSetFilterScreen gridScreen = new ArtifactSetFilterScreen(new GridParams(new Rect(40, 100, 1300, 852), 2, 3, 40, 40, 0.024), this.logger, this.ct);
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
using Mat img125 = GetGridIconsTask.CropResizeArtifactSetFilterGridIcon(itemRegion);
|
||||
(string predName, _) = GridIconsAccuracyTestTask.Infer(img125, session, prototypes);
|
||||
if (this.artifactSetFilter.Contains(predName))
|
||||
{
|
||||
itemRegion.Click();
|
||||
await Delay(100, ct);
|
||||
}
|
||||
}
|
||||
// 点击确认筛选
|
||||
using var confirmFilterBtnRegion = CaptureToRectArea();
|
||||
Bv.ClickWhiteConfirmButton(confirmFilterBtnRegion);
|
||||
await Delay(1500, ct);
|
||||
// 点击确认
|
||||
using var confirmBtnRegion = CaptureToRectArea();
|
||||
Bv.ClickWhiteConfirmButton(confirmBtnRegion);
|
||||
await Delay(600, ct);
|
||||
}
|
||||
|
||||
// 逐一点选查看面板筛选
|
||||
await Salvage5Star();
|
||||
logger.LogInformation("筛选完毕,请复查并手动分解");
|
||||
}
|
||||
else
|
||||
@@ -274,14 +319,14 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Salvage5Star(string javaScript, int maxNumToCheck)
|
||||
private async Task Salvage5Star()
|
||||
{
|
||||
int count = maxNumToCheck;
|
||||
string javaScript = this.javaScript ?? throw new ArgumentException($"{nameof(this.javaScript)}不能为空");
|
||||
int count = this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空");
|
||||
RecognitionFailurePolicy recognitionFailurePolicy = this.recognitionFailurePolicy ?? throw new ArgumentException($"{nameof(this.recognitionFailurePolicy)}不能为空");
|
||||
|
||||
using var ra0 = CaptureToRectArea();
|
||||
GridScreenParams gridParams = GridScreenParams.Templates[GridScreenName.ArtifactSalvage];
|
||||
Rect gridRoi = gridParams.GetRect(ra0);
|
||||
GridScreen gridScreen = new GridScreen(gridRoi, gridParams, this.logger, this.ct); // 圣遗物分解Grid有4行9列
|
||||
GridParams gridParams = GridParams.Templates[GridScreenName.ArtifactSalvage];
|
||||
GridScreen gridScreen = new GridScreen(gridParams, this.logger, this.ct); // 圣遗物分解Grid有4行9列
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
Rect gridRect = itemRegion.ToRect();
|
||||
@@ -291,11 +336,31 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
await Delay(300, ct);
|
||||
|
||||
using var ra1 = CaptureToRectArea();
|
||||
using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridRoi.X, gridRoi.Y));
|
||||
using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridParams.Roi.X, gridParams.Roi.Y));
|
||||
if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected)
|
||||
{
|
||||
using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.24), (int)(ra1.Width * 0.29)));
|
||||
ArtifactStat artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, this.cultureInfo, out string allText);
|
||||
using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Height * 0.112), (int)(ra1.Width * 0.275), (int)(ra1.Height * 0.50)));
|
||||
|
||||
ArtifactStat artifact;
|
||||
try
|
||||
{
|
||||
artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, out string allText);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (recognitionFailurePolicy == RecognitionFailurePolicy.Skip)
|
||||
{
|
||||
logger.LogError("识别失败,跳过当前圣遗物:{msg}", e.Message);
|
||||
|
||||
itemRegion.Click(); // 反选取消
|
||||
await Delay(100, ct);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsMatchJavaScript(artifact, javaScript))
|
||||
{
|
||||
@@ -379,35 +444,78 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
return match.Success;
|
||||
}
|
||||
|
||||
public static ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, CultureInfo cultureInfo, out string allText)
|
||||
public ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, out string allText)
|
||||
{
|
||||
var ocrResult = ocrService.OcrResult(src);
|
||||
allText = ocrResult.Text;
|
||||
var lines = ocrResult.Text.Split('\n');
|
||||
using Mat gray = src.CvtColor(ColorConversionCodes.BGR2GRAY);
|
||||
Mat hatKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15)/*需根据实际文本大小调整*/); // 顶帽运算核
|
||||
|
||||
Mat nameRoi = gray.SubMat(new Rect(0, 0, src.Width, (int)(src.Height * 0.106)));
|
||||
//Cv2.ImShow("name", nameRoi);
|
||||
Mat typeRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.106), src.Width, (int)(src.Height * 0.106)));
|
||||
#region 主词条预处理 去除背景干扰
|
||||
Mat mainAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.22), (int)(src.Width * 0.55), (int)(src.Height * 0.30)));
|
||||
using Mat mainAffixRoiBottomHat = mainAffixRoi.MorphologyEx(MorphTypes.TopHat, hatKernel);
|
||||
using Mat mainAffixRoiThreshold = mainAffixRoiBottomHat.Threshold(30, 255, ThresholdTypes.Binary);
|
||||
//Cv2.ImShow("mainAffix", mainAffixRoiThreshold);
|
||||
#endregion
|
||||
#region 副词条预处理 还是不处理效果最好……
|
||||
Mat levelAndMinorAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.52), src.Width, (int)(src.Height * 0.48)));
|
||||
//using Mat levelAndMinorAffixRoiThreshold = new Mat();
|
||||
//double otsu = Cv2.Threshold(levelAndMinorAffixRoi, levelAndMinorAffixRoiThreshold, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
|
||||
// //using Mat levelAndMinorAffixRoiThreshold = levelAndMinorAffixRoi.Threshold(170, 255, ThresholdTypes.Binary);
|
||||
//Cv2.ImShow($"levelAndMinorAffixRoi = {otsu}", levelAndMinorAffixRoiThreshold);
|
||||
#endregion
|
||||
//Cv2.WaitKey();
|
||||
|
||||
var nameOcrResult = ocrService.OcrResult(nameRoi);
|
||||
var typeOcrResult = ocrService.OcrResult(typeRoi);
|
||||
var mainAffixOcrResult = ocrService.OcrResult(mainAffixRoiThreshold);
|
||||
string mainAffixText = string.Join("\n", mainAffixOcrResult.Regions.Where(r => r.Score > 0.5).OrderBy(r => r.Rect.Center.Y).ThenBy(r => r.Rect.Center.X).Select(r => r.Text));
|
||||
var mainAffixLines = mainAffixText.Split('\n');
|
||||
var levelAndMinorAffixOcrResult = ocrService.OcrResult(levelAndMinorAffixRoi);
|
||||
string levelAndMinorAffixText = string.Join("\n", levelAndMinorAffixOcrResult.Regions.Where(r => r.Score > 0.5)
|
||||
.Where(r => r.Rect.BoundingRect().Left < levelAndMinorAffixRoi.Width * 0.1) // 一定是贴着左边的,排除套装效果文字也存在类似+15%的情况
|
||||
.OrderBy(r => r.Rect.Center.Y).ThenBy(r => r.Rect.Center.X).Select(r => r.Text));
|
||||
var levelAndMinorAffixLines = levelAndMinorAffixText.Split('\n');
|
||||
|
||||
allText = String.Join('\n', nameOcrResult.Text, typeOcrResult.Text, mainAffixText, levelAndMinorAffixText);
|
||||
|
||||
string percentStr = "%";
|
||||
|
||||
// 名称
|
||||
string name = lines[0];
|
||||
string name = nameOcrResult.Text;
|
||||
|
||||
#region 主词条
|
||||
var defaultMainAffix = ArtifactAffix.DefaultStrDic.Select(kvp => kvp.Value).Distinct();
|
||||
string mainAffixTypeLine = lines.Single(l => defaultMainAffix.Contains(l));
|
||||
ArtifactAffixType mainAffixType = ArtifactAffix.DefaultStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key;
|
||||
string mainAffixValueLine = lines.Select(l =>
|
||||
var defaultMainAffix = this.artifactAffixStrDic.Select(kvp => kvp.Value).Distinct();
|
||||
string mainAffixTypeLine = mainAffixLines.SingleOrDefault(l => defaultMainAffix.Contains(l)) ?? throw new Exception($"未找到主词条对应的行:\n{mainAffixText}");
|
||||
ArtifactAffixType mainAffixType = this.artifactAffixStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key;
|
||||
string mainAffixValueLine = mainAffixLines.Select(l =>
|
||||
{
|
||||
string pattern = @"^(\d+\.?\d*)(%?)$";
|
||||
string pattern = @"^([\d., ]*)(%?)$";
|
||||
pattern = pattern.Replace("%", percentStr); // 这样一行一行写只是为了IDE能保持正则字符串高亮
|
||||
Match match = Regex.Match(l, pattern);
|
||||
if (match.Success)
|
||||
{
|
||||
if (mainAffixType == ArtifactAffixType.ATK && !String.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
mainAffixType = ArtifactAffixType.ATKPercent;
|
||||
}
|
||||
if (mainAffixType == ArtifactAffixType.DEF && !String.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
mainAffixType = ArtifactAffixType.DEFPercent;
|
||||
}
|
||||
if (mainAffixType == ArtifactAffixType.HP && !String.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
mainAffixType = ArtifactAffixType.HPPercent;
|
||||
}
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(l => l != null).Cast<string>().Single();
|
||||
if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, cultureInfo, out float value))
|
||||
}).Where(l => l != null).Cast<string>().SingleOrDefault() ?? throw new Exception($"未找到主词条数值对应的行:\n{mainAffixText}");
|
||||
if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, this.cultureInfo, out float value))
|
||||
{
|
||||
throw new Exception($"未识别的主词条数值:{mainAffixValueLine}");
|
||||
}
|
||||
@@ -415,15 +523,15 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
#endregion
|
||||
|
||||
#region 副词条
|
||||
ArtifactAffix[] minorAffixes = lines.Select(l =>
|
||||
ArtifactAffix[] minorAffixes = levelAndMinorAffixLines.Select(l =>
|
||||
{
|
||||
string pattern = @"^[•·]?([^+]+)\+(\d+\.?\d*)(%?)$";
|
||||
string pattern = @"^[•·]?([^+::]+)\+([\d., ]*)(%?)$";
|
||||
pattern = pattern.Replace("%", percentStr);
|
||||
Match match = Regex.Match(l, pattern);
|
||||
if (match.Success)
|
||||
{
|
||||
ArtifactAffixType artifactAffixType;
|
||||
var dic = ArtifactAffix.DefaultStrDic;
|
||||
var dic = this.artifactAffixStrDic;
|
||||
|
||||
if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ATK]))
|
||||
{
|
||||
@@ -493,7 +601,7 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
#endregion
|
||||
|
||||
#region 等级
|
||||
string levelLine = lines.Select(l =>
|
||||
string levelLine = levelAndMinorAffixLines.Select(l =>
|
||||
{
|
||||
string pattern = @"^\+(\d*)$";
|
||||
Match match = Regex.Match(l, pattern);
|
||||
@@ -505,7 +613,7 @@ public class AutoArtifactSalvageTask : ISoloTask
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(l => l != null).Cast<string>().Single();
|
||||
}).Where(l => l != null).Cast<string>().SingleOrDefault() ?? throw new Exception($"未找到等级对应的行:\n{levelAndMinorAffixText}");
|
||||
if (!int.TryParse(levelLine, out int level) || level < 0 || level > 20)
|
||||
{
|
||||
throw new Exception($"未识别的等级:{levelLine}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -129,7 +129,55 @@
|
||||
<data name="4星圣遗物" xml:space="preserve">
|
||||
<value>4-Star.*Artifacts</value>
|
||||
</data>
|
||||
<data name="元素充能效率" xml:space="preserve">
|
||||
<value>Energy Recharge</value>
|
||||
</data>
|
||||
<data name="元素精通" xml:space="preserve">
|
||||
<value>Elemental Mastery</value>
|
||||
</data>
|
||||
<data name="冰元素伤害加成" xml:space="preserve">
|
||||
<value>Cryo DMG Bonus</value>
|
||||
</data>
|
||||
<data name="岩元素伤害加成" xml:space="preserve">
|
||||
<value>Geo DMG Bonus</value>
|
||||
</data>
|
||||
<data name="快速选择" xml:space="preserve">
|
||||
<value>Quick.*Select</value>
|
||||
</data>
|
||||
<data name="攻击力" xml:space="preserve">
|
||||
<value>ATK</value>
|
||||
</data>
|
||||
<data name="暴击伤害" xml:space="preserve">
|
||||
<value>CRIT DMG</value>
|
||||
</data>
|
||||
<data name="暴击率" xml:space="preserve">
|
||||
<value>CRIT Rate</value>
|
||||
</data>
|
||||
<data name="水元素伤害加成" xml:space="preserve">
|
||||
<value>Hydro DMG Bonus</value>
|
||||
</data>
|
||||
<data name="治疗加成" xml:space="preserve">
|
||||
<value>Healing Bonus</value>
|
||||
</data>
|
||||
<data name="火元素伤害加成" xml:space="preserve">
|
||||
<value>Pyro DMG Bonus</value>
|
||||
</data>
|
||||
<data name="物理伤害加成" xml:space="preserve">
|
||||
<value>Physical DMG Bonus</value>
|
||||
</data>
|
||||
<data name="生命值" xml:space="preserve">
|
||||
<value>HP</value>
|
||||
</data>
|
||||
<data name="草元素伤害加成" xml:space="preserve">
|
||||
<value>Dendro DMG Bonus</value>
|
||||
</data>
|
||||
<data name="防御力" xml:space="preserve">
|
||||
<value>DEF</value>
|
||||
</data>
|
||||
<data name="雷元素伤害加成" xml:space="preserve">
|
||||
<value>Electro DMG Bonus</value>
|
||||
</data>
|
||||
<data name="风元素伤害加成" xml:space="preserve">
|
||||
<value>Anemo DMG Bonus</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -129,7 +129,55 @@
|
||||
<data name="4星圣遗物" xml:space="preserve">
|
||||
<value>Artéfact.*4★?</value>
|
||||
</data>
|
||||
<data name="元素充能效率" xml:space="preserve">
|
||||
<value>Recharge d'énergie</value>
|
||||
</data>
|
||||
<data name="元素精通" xml:space="preserve">
|
||||
<value>Maîtrise élémentaire</value>
|
||||
</data>
|
||||
<data name="冰元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Cryo</value>
|
||||
</data>
|
||||
<data name="岩元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Géo</value>
|
||||
</data>
|
||||
<data name="快速选择" xml:space="preserve">
|
||||
<value>Sélection.*rapide</value>
|
||||
</data>
|
||||
<data name="攻击力" xml:space="preserve">
|
||||
<value>ATQ</value>
|
||||
</data>
|
||||
<data name="暴击伤害" xml:space="preserve">
|
||||
<value>DGT CRIT</value>
|
||||
</data>
|
||||
<data name="暴击率" xml:space="preserve">
|
||||
<value>Taux CRIT</value>
|
||||
</data>
|
||||
<data name="水元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Hydro</value>
|
||||
</data>
|
||||
<data name="治疗加成" xml:space="preserve">
|
||||
<value>Bonus de soins</value>
|
||||
</data>
|
||||
<data name="火元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Pyro</value>
|
||||
</data>
|
||||
<data name="物理伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT physiques</value>
|
||||
</data>
|
||||
<data name="生命值" xml:space="preserve">
|
||||
<value>PV</value>
|
||||
</data>
|
||||
<data name="草元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Dendro</value>
|
||||
</data>
|
||||
<data name="防御力" xml:space="preserve">
|
||||
<value>DÉF</value>
|
||||
</data>
|
||||
<data name="雷元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Électro</value>
|
||||
</data>
|
||||
<data name="风元素伤害加成" xml:space="preserve">
|
||||
<value>Bonus de DGT Anémo</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -129,7 +129,55 @@
|
||||
<data name="4星圣遗物" xml:space="preserve">
|
||||
<value>4星圣遗物</value>
|
||||
</data>
|
||||
<data name="元素充能效率" xml:space="preserve">
|
||||
<value>元素充能效率</value>
|
||||
</data>
|
||||
<data name="元素精通" xml:space="preserve">
|
||||
<value>元素精通</value>
|
||||
</data>
|
||||
<data name="冰元素伤害加成" xml:space="preserve">
|
||||
<value>冰元素伤害加成</value>
|
||||
</data>
|
||||
<data name="岩元素伤害加成" xml:space="preserve">
|
||||
<value>岩元素伤害加成</value>
|
||||
</data>
|
||||
<data name="快速选择" xml:space="preserve">
|
||||
<value>快速选择</value>
|
||||
</data>
|
||||
<data name="攻击力" xml:space="preserve">
|
||||
<value>攻击力</value>
|
||||
</data>
|
||||
<data name="暴击伤害" xml:space="preserve">
|
||||
<value>暴击伤害</value>
|
||||
</data>
|
||||
<data name="暴击率" xml:space="preserve">
|
||||
<value>暴击率</value>
|
||||
</data>
|
||||
<data name="水元素伤害加成" xml:space="preserve">
|
||||
<value>水元素伤害加成</value>
|
||||
</data>
|
||||
<data name="治疗加成" xml:space="preserve">
|
||||
<value>治疗加成</value>
|
||||
</data>
|
||||
<data name="火元素伤害加成" xml:space="preserve">
|
||||
<value>火元素伤害加成</value>
|
||||
</data>
|
||||
<data name="物理伤害加成" xml:space="preserve">
|
||||
<value>物理伤害加成</value>
|
||||
</data>
|
||||
<data name="生命值" xml:space="preserve">
|
||||
<value>生命值</value>
|
||||
</data>
|
||||
<data name="草元素伤害加成" xml:space="preserve">
|
||||
<value>草元素伤害加成</value>
|
||||
</data>
|
||||
<data name="防御力" xml:space="preserve">
|
||||
<value>防御力</value>
|
||||
</data>
|
||||
<data name="雷元素伤害加成" xml:space="preserve">
|
||||
<value>雷元素伤害加成</value>
|
||||
</data>
|
||||
<data name="风元素伤害加成" xml:space="preserve">
|
||||
<value>风元素伤害加成</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -129,7 +129,55 @@
|
||||
<data name="4星圣遗物" xml:space="preserve">
|
||||
<value>四星聖遺物</value>
|
||||
</data>
|
||||
<data name="元素充能效率" xml:space="preserve">
|
||||
<value>元素充能效率</value>
|
||||
</data>
|
||||
<data name="元素精通" xml:space="preserve">
|
||||
<value>元素精通</value>
|
||||
</data>
|
||||
<data name="冰元素伤害加成" xml:space="preserve">
|
||||
<value>冰元素傷害加成</value>
|
||||
</data>
|
||||
<data name="岩元素伤害加成" xml:space="preserve">
|
||||
<value>岩元素傷害加成</value>
|
||||
</data>
|
||||
<data name="快速选择" xml:space="preserve">
|
||||
<value>快速選擇</value>
|
||||
</data>
|
||||
<data name="攻击力" xml:space="preserve">
|
||||
<value>攻擊力</value>
|
||||
</data>
|
||||
<data name="暴击伤害" xml:space="preserve">
|
||||
<value>暴擊傷害</value>
|
||||
</data>
|
||||
<data name="暴击率" xml:space="preserve">
|
||||
<value>暴擊率</value>
|
||||
</data>
|
||||
<data name="水元素伤害加成" xml:space="preserve">
|
||||
<value>水元素傷害加成</value>
|
||||
</data>
|
||||
<data name="治疗加成" xml:space="preserve">
|
||||
<value>治療加成</value>
|
||||
</data>
|
||||
<data name="火元素伤害加成" xml:space="preserve">
|
||||
<value>火元素傷害加成</value>
|
||||
</data>
|
||||
<data name="物理伤害加成" xml:space="preserve">
|
||||
<value>物理傷害加成</value>
|
||||
</data>
|
||||
<data name="生命值" xml:space="preserve">
|
||||
<value>生命值</value>
|
||||
</data>
|
||||
<data name="草元素伤害加成" xml:space="preserve">
|
||||
<value>草元素傷害加成</value>
|
||||
</data>
|
||||
<data name="防御力" xml:space="preserve">
|
||||
<value>防禦力</value>
|
||||
</data>
|
||||
<data name="雷元素伤害加成" xml:space="preserve">
|
||||
<value>雷元素傷害加成</value>
|
||||
</data>
|
||||
<data name="风元素伤害加成" xml:space="preserve">
|
||||
<value>風元素傷害加成</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,27 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage
|
||||
{
|
||||
public class AutoArtifactSalvageTaskParam : BaseTaskParam<AutoArtifactSalvageTask>
|
||||
{
|
||||
public AutoArtifactSalvageTaskParam(int star, string? javaScript, string? artifactSetFilter, int? maxNumToCheck, RecognitionFailurePolicy? recognitionFailurePolicy, CultureInfo? cultureInfo = null, IStringLocalizer<AutoArtifactSalvageTask>? stringLocalizer = null) : base(cultureInfo, stringLocalizer)
|
||||
{
|
||||
Star = star;
|
||||
JavaScript = javaScript;
|
||||
ArtifactSetFilter = artifactSetFilter;
|
||||
MaxNumToCheck = maxNumToCheck;
|
||||
RecognitionFailurePolicy = recognitionFailurePolicy;
|
||||
}
|
||||
|
||||
public int Star { get; set; }
|
||||
public string? JavaScript { get; set; }
|
||||
public string? ArtifactSetFilter { get; set; }
|
||||
public int? MaxNumToCheck { get; set; }
|
||||
public RecognitionFailurePolicy? RecognitionFailurePolicy { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage
|
||||
{
|
||||
public enum RecognitionFailurePolicy
|
||||
{
|
||||
[Description("跳过")]
|
||||
Skip,
|
||||
[Description("终止")]
|
||||
Abort
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoDomain;
|
||||
|
||||
public class AutoDomainParam : BaseTaskParam
|
||||
public class AutoDomainParam : BaseTaskParam<AutoDomainParam>
|
||||
{
|
||||
public int DomainRoundNum { get; set; }
|
||||
|
||||
@@ -15,7 +15,7 @@ public class AutoDomainParam : BaseTaskParam
|
||||
|
||||
// 需要刷取的副本名称
|
||||
public string DomainName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
// 需要刷取的副本名称
|
||||
public string SundaySelectedValue { get; set; } = string.Empty;
|
||||
|
||||
@@ -27,9 +27,9 @@ public class AutoDomainParam : BaseTaskParam
|
||||
public string MaxArtifactStar { get; set; } = "4";
|
||||
|
||||
public bool SpecifyResinUse { get; set; } = false;
|
||||
|
||||
|
||||
// 使用树脂优先级
|
||||
public List<string> ResinPriorityList { get; set; } =
|
||||
public List<string> ResinPriorityList { get; set; } =
|
||||
[
|
||||
"浓缩树脂",
|
||||
"原粹树脂"
|
||||
@@ -47,7 +47,7 @@ public class AutoDomainParam : BaseTaskParam
|
||||
// 使用脆弱树脂刷取副本次数
|
||||
public int FragileResinUseCount { get; set; } = 0;
|
||||
|
||||
public AutoDomainParam(int domainRoundNum, string path)
|
||||
public AutoDomainParam(int domainRoundNum, string path) : base(null, null)
|
||||
{
|
||||
DomainRoundNum = domainRoundNum;
|
||||
if (domainRoundNum == 0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Recognition.ONNX;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
@@ -18,8 +18,6 @@ using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -29,7 +27,6 @@ using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using BetterGenshinImpact.Service.Notification.Model.Enum;
|
||||
using Vanara.PInvoke;
|
||||
using static BetterGenshinImpact.GameTask.Common.TaskControl;
|
||||
using static Vanara.PInvoke.Kernel32;
|
||||
using static Vanara.PInvoke.User32;
|
||||
@@ -97,7 +94,7 @@ public class AutoDomainTask : ISoloTask
|
||||
this.rapidformationString = stringLocalizer.WithCultureGet(cultureInfo, "快速编队");
|
||||
this.limitedFullyString = stringLocalizer.WithCultureGet(cultureInfo, "限时全开");
|
||||
}
|
||||
|
||||
|
||||
private static RecognitionObject GetConfirmRa(params string[] targetText)
|
||||
{
|
||||
var screenArea = CaptureToRectArea();
|
||||
@@ -163,18 +160,18 @@ public class AutoDomainTask : ISoloTask
|
||||
|
||||
// 前置进入秘境
|
||||
await EnterDomain();
|
||||
|
||||
|
||||
var combatScenes = new CombatScenes();
|
||||
for (var i = 0; i < _taskParam.DomainRoundNum; i++)
|
||||
{
|
||||
// 0. 关闭秘境提示
|
||||
Logger.LogDebug("0. 关闭秘境提示");
|
||||
await CloseDomainTip();
|
||||
|
||||
|
||||
//0.5. 初始化队伍,只执行一次
|
||||
if (i == 0)
|
||||
{
|
||||
combatScenes = new CombatScenes().InitializeTeam(CaptureToRectArea());
|
||||
combatScenes = new CombatScenes().InitializeTeam(CaptureToRectArea());
|
||||
}
|
||||
RetryTeamInit(combatScenes);// 队伍没初始化成功则重试
|
||||
|
||||
@@ -218,7 +215,7 @@ public class AutoDomainTask : ISoloTask
|
||||
{
|
||||
TaskTriggerDispatcher.Instance().AddTrigger("AutoEat", null);
|
||||
}
|
||||
|
||||
|
||||
if (_config.SpecifyResinUse)
|
||||
{
|
||||
Logger.LogInformation("→ {Text} 指定使用树脂", "自动秘境,");
|
||||
@@ -273,13 +270,13 @@ public class AutoDomainTask : ISoloTask
|
||||
var menuFound = false;
|
||||
if ("芬德尼尔之顶".Equals(_taskParam.DomainName))
|
||||
{
|
||||
menuFound = await NewRetry.WaitForElementAppear(
|
||||
AutoPickAssets.Instance.PickRo,
|
||||
() => Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyDown),
|
||||
_ct,
|
||||
20,
|
||||
500
|
||||
);
|
||||
menuFound = await NewRetry.WaitForElementAppear(
|
||||
AutoPickAssets.Instance.PickRo,
|
||||
() => Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyDown),
|
||||
_ct,
|
||||
20,
|
||||
500
|
||||
);
|
||||
Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyUp);
|
||||
}
|
||||
else if ("无妄引咎密宫".Equals(_taskParam.DomainName))
|
||||
@@ -290,13 +287,13 @@ public class AutoDomainTask : ISoloTask
|
||||
|
||||
menuFound = await NewRetry.WaitForElementAppear(
|
||||
AutoPickAssets.Instance.PickRo,
|
||||
() => Simulation.SendInput.SimulateAction(GIActions.MoveLeft, KeyType.KeyDown),
|
||||
() => Simulation.SendInput.SimulateAction(GIActions.MoveLeft, KeyType.KeyDown),
|
||||
_ct,
|
||||
20,
|
||||
500
|
||||
);
|
||||
Simulation.SendInput.SimulateAction(GIActions.MoveLeft, KeyType.KeyUp);
|
||||
|
||||
|
||||
}
|
||||
else if ("太山府".Equals(_taskParam.DomainName))
|
||||
{
|
||||
@@ -319,12 +316,12 @@ public class AutoDomainTask : ISoloTask
|
||||
);
|
||||
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
|
||||
}
|
||||
|
||||
|
||||
if (!menuFound)
|
||||
{
|
||||
throw new Exception("请检查是否在秘境门前");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var menu = await NewRetry.WaitForElementAppear(
|
||||
GetConfirmRa("单人挑战"),
|
||||
() => Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk),
|
||||
@@ -336,7 +333,7 @@ public class AutoDomainTask : ISoloTask
|
||||
{
|
||||
throw new Exception("请检查是否已进入秘境页面");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -366,7 +363,7 @@ public class AutoDomainTask : ISoloTask
|
||||
private async Task EnterDomain()
|
||||
{
|
||||
var fightAssets = AutoFightAssets.Instance;
|
||||
|
||||
|
||||
var menuFound = await NewRetry.WaitForElementAppear(
|
||||
GetConfirmRa("单人挑战"),
|
||||
() => Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk),
|
||||
@@ -378,7 +375,7 @@ public class AutoDomainTask : ISoloTask
|
||||
{
|
||||
Logger.LogWarning("单人挑战 按键未出现,请检查是否已进入秘境页面");
|
||||
}
|
||||
|
||||
|
||||
using var limitedFullyStringRa = CaptureToRectArea();
|
||||
var limitedFullyStringRaocrList =
|
||||
limitedFullyStringRa.FindMulti(RecognitionObject.Ocr(0, 0, limitedFullyStringRa.Width * 0.5,
|
||||
@@ -390,7 +387,7 @@ public class AutoDomainTask : ISoloTask
|
||||
{
|
||||
Logger.LogInformation("自动秘境:{Text}", "检测到秘境限时全开");
|
||||
}
|
||||
|
||||
|
||||
DateTime now = DateTime.Now;
|
||||
if ((now.DayOfWeek == DayOfWeek.Sunday && now.Hour >= 4 || now.DayOfWeek == DayOfWeek.Monday && now.Hour < 4) || limitedFullyStringRaocrListdone != null)
|
||||
{
|
||||
@@ -479,13 +476,13 @@ public class AutoDomainTask : ISoloTask
|
||||
10,
|
||||
1000
|
||||
);
|
||||
|
||||
|
||||
// 等待队伍选择界面出现
|
||||
var teamUiFound = await NewRetry.WaitForElementAppear(
|
||||
ElementAssets.Instance.PartyBtnChooseView,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInformation("自动秘境:进入 {Text}", "队伍选择界面");
|
||||
Logger.LogInformation("自动秘境:进入 {Text}", "队伍选择界面");
|
||||
},
|
||||
_ct,
|
||||
10,
|
||||
@@ -494,18 +491,21 @@ public class AutoDomainTask : ISoloTask
|
||||
if (!teamUiFound)
|
||||
{
|
||||
Logger.LogWarning("队伍选择界面未出现,跳过切换队伍。");
|
||||
}else
|
||||
}
|
||||
else
|
||||
{
|
||||
await SwitchParty(_taskParam.PartyName);
|
||||
}
|
||||
|
||||
|
||||
// 点击开始挑战确认并等待“开始挑战”文字消失
|
||||
var startFightFound = await NewRetry.WaitForElementDisappear(
|
||||
GetConfirmRa("开始挑战"),
|
||||
screen => {
|
||||
screen.Find(fightAssets.ConfirmRa, ra => {
|
||||
ra.Click();
|
||||
ra.Dispose();
|
||||
screen =>
|
||||
{
|
||||
screen.Find(fightAssets.ConfirmRa, ra =>
|
||||
{
|
||||
ra.Click();
|
||||
ra.Dispose();
|
||||
Logger.LogInformation("自动秘境:点击 {Text}", "开始挑战");
|
||||
});
|
||||
},
|
||||
@@ -527,11 +527,11 @@ public class AutoDomainTask : ISoloTask
|
||||
var domainTipFound = await NewRetry.WaitForAction(() =>
|
||||
{
|
||||
using var ra = CaptureToRectArea();
|
||||
|
||||
|
||||
var ocrList = ra.FindMulti(RecognitionObject.Ocr(0, ra.Height * 0.2, ra.Width, ra.Height * 0.6));
|
||||
var ocrListLeft = ra.Find(AutoFightAssets.Instance.AbnormalIconRa);
|
||||
return (ocrList.Any(t => t.Text.Contains(leyLineDisorderLocalizedString) ||
|
||||
t.Text.Contains(clickanywheretocloseLocalizedString)))|| ocrListLeft.IsExist();
|
||||
return (ocrList.Any(t => t.Text.Contains(leyLineDisorderLocalizedString) ||
|
||||
t.Text.Contains(clickanywheretocloseLocalizedString))) || ocrListLeft.IsExist();
|
||||
}, _ct, 20, 500);
|
||||
if (!domainTipFound)
|
||||
{
|
||||
@@ -561,10 +561,10 @@ public class AutoDomainTask : ISoloTask
|
||||
if (!leftBottomFound)
|
||||
{
|
||||
//尝试随意点击一下右下角
|
||||
GameCaptureRegion.GameRegion1080PPosClick(1515,892);
|
||||
GameCaptureRegion.GameRegion1080PPosClick(1515, 892);
|
||||
Logger.LogWarning("秘境提示未出现或未能点击。");
|
||||
}
|
||||
|
||||
|
||||
await Delay(500, _ct);
|
||||
}
|
||||
|
||||
@@ -754,7 +754,7 @@ public class AutoDomainTask : ISoloTask
|
||||
|
||||
if (!IsTakeFood())
|
||||
{
|
||||
Logger.LogInformation("未装备 “{Tool}”,不启用红血自动吃药功能", "便携营养袋");
|
||||
Logger.LogInformation("未装备 “{Tool}”,不启用红血自动吃药功能", "便携营养袋");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1111,7 +1111,7 @@ public class AutoDomainTask : ISoloTask
|
||||
await ExitDomain();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool resinUsed = false;
|
||||
if (resinStatus.CondensedResinCount > 0)
|
||||
{
|
||||
@@ -1123,7 +1123,7 @@ public class AutoDomainTask : ISoloTask
|
||||
resinUsed = PressUseResin(ra3, "原粹树脂");
|
||||
resinStatus.OriginalResinCount -= 20;
|
||||
}
|
||||
|
||||
|
||||
if (!resinUsed)
|
||||
{
|
||||
Logger.LogWarning("自动秘境:未找到可用的树脂,可能是{Msg1} 或者 {Msg2}。", "树脂不足", "OCR 识别失败");
|
||||
@@ -1209,7 +1209,7 @@ public class AutoDomainTask : ISoloTask
|
||||
confirmRectArea.Click();
|
||||
await Delay(60, _ct); // 双击
|
||||
confirmRectArea.Click();
|
||||
|
||||
|
||||
if (!chooseResinPrompt)
|
||||
{
|
||||
// 真没树脂了还有提示兜底
|
||||
@@ -1312,6 +1312,6 @@ public class AutoDomainTask : ISoloTask
|
||||
star = 4;
|
||||
}
|
||||
|
||||
await new AutoArtifactSalvageTask(star).Start(_ct);
|
||||
await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star, javaScript: null, artifactSetFilter: null, maxNumToCheck: null, recognitionFailurePolicy: null)).Start(_ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public partial class AutoEatConfig : ObservableObject
|
||||
/// 默认的攻击类料理名称
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string? _defaultAtkBoostingDishName;
|
||||
private string? _defaultAtkBoostingDishName = "炸萝卜丸子";
|
||||
|
||||
/// <summary>
|
||||
/// 默认的冒险类料理名称
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoEat;
|
||||
|
||||
/// <summary>
|
||||
/// 自动吃药任务参数
|
||||
/// </summary>
|
||||
public class AutoEatParam : BaseTaskParam
|
||||
public class AutoEatParam : BaseTaskParam<AutoEatTask>
|
||||
{
|
||||
public AutoEatParam() : base(null, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示通知
|
||||
/// </summary>
|
||||
|
||||
@@ -12,6 +12,7 @@ using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using Fischless.WindowsInput;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
@@ -24,7 +25,7 @@ namespace BetterGenshinImpact.GameTask.AutoEat;
|
||||
/// 自动吃药任务
|
||||
/// 检测红血自动使用便携营养袋
|
||||
/// </summary>
|
||||
public class AutoEatTask : BaseIndependentTask, ISoloTask
|
||||
public class AutoEatTask : BaseIndependentTask, ISoloTask<int?>
|
||||
{
|
||||
public string Name => "自动吃药";
|
||||
|
||||
@@ -40,7 +41,12 @@ public class AutoEatTask : BaseIndependentTask, ISoloTask
|
||||
_config = TaskContext.Instance().Config.AutoEatConfig;
|
||||
}
|
||||
|
||||
public async Task Start(CancellationToken ct)
|
||||
async Task ISoloTask.Start(CancellationToken ct)
|
||||
{
|
||||
await Start(ct);
|
||||
}
|
||||
|
||||
public async Task<int?> Start(CancellationToken ct)
|
||||
{
|
||||
_ct = ct;
|
||||
|
||||
@@ -52,7 +58,7 @@ public class AutoEatTask : BaseIndependentTask, ISoloTask
|
||||
if (!IsTakeFood())
|
||||
{
|
||||
_logger.LogWarning("未装备 \"{Tool}\",无法启用自动吃药功能", "便携营养袋");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -68,45 +74,62 @@ public class AutoEatTask : BaseIndependentTask, ISoloTask
|
||||
{
|
||||
_logger.LogInformation("自动吃药任务结束");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("打开背包寻找{name}……", _taskParam.FoodName);
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
await AutoArtifactSalvageTask.OpenBag(GridScreenName.Food, _input, _logger, _ct);
|
||||
await AutoArtifactSalvageTask.OpenInventory(GridScreenName.Food, _input, _logger, _ct);
|
||||
|
||||
using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary<string, float[]> prototypes);
|
||||
|
||||
using var ra0 = CaptureToRectArea();
|
||||
GridScreenParams gridParams = GridScreenParams.Templates[GridScreenName.Food];
|
||||
var gridRoi = gridParams.GetRect(ra0);
|
||||
GridScreen gridScreen = new GridScreen(gridRoi, gridParams, _logger, _ct);
|
||||
bool isAte = false;
|
||||
GridScreen gridScreen = new GridScreen(GridParams.Templates[GridScreenName.Food], _logger, _ct);
|
||||
int? count = null;
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
var result = GridIconsAccuracyTestTask.Infer(itemRegion.SrcMat, session, prototypes);
|
||||
using Mat icon = itemRegion.SrcMat.GetGridIcon();
|
||||
var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes);
|
||||
string predName = result.Item1;
|
||||
if (predName == _taskParam.FoodName)
|
||||
{
|
||||
// 点击item
|
||||
itemRegion.Click();
|
||||
|
||||
#region 识别数量
|
||||
string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle);
|
||||
if (int.TryParse(numStr, out int num))
|
||||
{
|
||||
count = num - 1; // 算上吃掉的1个
|
||||
}
|
||||
else
|
||||
{
|
||||
count = -2;
|
||||
_logger.LogWarning("无法识别食物数量:{text},依然尝试使用", numStr);
|
||||
}
|
||||
#endregion
|
||||
|
||||
await Delay(300, ct);
|
||||
// 点击确定
|
||||
using var ra0 = CaptureToRectArea();
|
||||
using var ra = ra0.Find(ElementAssets.Instance.BtnWhiteConfirm);
|
||||
if (ra.IsExist())
|
||||
{
|
||||
ra.Click();
|
||||
}
|
||||
_logger.LogInformation("吃了一份{name},真香!", predName);
|
||||
isAte = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isAte)
|
||||
if (count == null)
|
||||
{
|
||||
count = -1;
|
||||
_logger.LogInformation("没有找到{name}", _taskParam.FoodName);
|
||||
}
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFight;
|
||||
|
||||
@@ -7,21 +7,21 @@ namespace BetterGenshinImpact.GameTask.AutoFight;
|
||||
|
||||
|
||||
|
||||
public class AutoFightParam : BaseTaskParam
|
||||
public class AutoFightParam : BaseTaskParam<AutoFightTask>
|
||||
{
|
||||
public class FightFinishDetectConfig
|
||||
public class FightFinishDetectConfig
|
||||
{
|
||||
public string BattleEndProgressBarColor { get; set; }= "";
|
||||
public string BattleEndProgressBarColor { get; set; } = "";
|
||||
|
||||
public string BattleEndProgressBarColorTolerance { get; set; }= "";
|
||||
public string BattleEndProgressBarColorTolerance { get; set; } = "";
|
||||
public bool FastCheckEnabled = false;
|
||||
public string FastCheckParams = "";
|
||||
public string CheckEndDelay = "";
|
||||
public string BeforeDetectDelay = "";
|
||||
public bool RotateFindEnemyEnabled = false;
|
||||
}
|
||||
|
||||
public AutoFightParam(string path, AutoFightConfig autoFightConfig)
|
||||
|
||||
public AutoFightParam(string path, AutoFightConfig autoFightConfig) : base(null, null)
|
||||
{
|
||||
CombatStrategyPath = path;
|
||||
Timeout = autoFightConfig.Timeout;
|
||||
@@ -30,21 +30,21 @@ public class AutoFightParam : BaseTaskParam
|
||||
PickDropsAfterFightSeconds = autoFightConfig.PickDropsAfterFightSeconds;
|
||||
KazuhaPickupEnabled = autoFightConfig.KazuhaPickupEnabled;
|
||||
ActionSchedulerByCd = autoFightConfig.ActionSchedulerByCd;
|
||||
|
||||
|
||||
FinishDetectConfig.FastCheckEnabled = autoFightConfig.FinishDetectConfig.FastCheckEnabled;
|
||||
FinishDetectConfig.FastCheckParams = autoFightConfig.FinishDetectConfig.FastCheckParams;
|
||||
FinishDetectConfig.CheckEndDelay = autoFightConfig.FinishDetectConfig.CheckEndDelay;
|
||||
FinishDetectConfig.BeforeDetectDelay = autoFightConfig.FinishDetectConfig.BeforeDetectDelay;
|
||||
FinishDetectConfig.RotateFindEnemyEnabled = autoFightConfig.FinishDetectConfig.RotateFindEnemyEnabled;
|
||||
|
||||
|
||||
|
||||
|
||||
KazuhaPartyName = autoFightConfig.KazuhaPartyName;
|
||||
OnlyPickEliteDropsMode = autoFightConfig.OnlyPickEliteDropsMode;
|
||||
BattleThresholdForLoot = autoFightConfig.BattleThresholdForLoot ?? BattleThresholdForLoot;
|
||||
//下面参数固定,只取自动战斗里面的
|
||||
FinishDetectConfig.BattleEndProgressBarColor = TaskContext.Instance().Config.AutoFightConfig.FinishDetectConfig.BattleEndProgressBarColor;
|
||||
FinishDetectConfig.BattleEndProgressBarColorTolerance = TaskContext.Instance().Config.AutoFightConfig.FinishDetectConfig.BattleEndProgressBarColorTolerance;
|
||||
|
||||
|
||||
GuardianAvatar = autoFightConfig.GuardianAvatar;
|
||||
GuardianCombatSkip = autoFightConfig.GuardianCombatSkip;
|
||||
SkipModel = autoFightConfig.SkipModel;
|
||||
@@ -64,7 +64,7 @@ public class AutoFightParam : BaseTaskParam
|
||||
public bool KazuhaPickupEnabled = true;
|
||||
public string ActionSchedulerByCd = "";
|
||||
public string KazuhaPartyName;
|
||||
public string OnlyPickEliteDropsMode="";
|
||||
public string OnlyPickEliteDropsMode = "";
|
||||
public string GuardianAvatar { get; set; } = " ";
|
||||
public bool GuardianCombatSkip { get; set; } = false;
|
||||
public bool SkipModel = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Core.Script.Dependence;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.Core.Simulator.Extensions;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Config;
|
||||
@@ -834,7 +835,7 @@ public class Avatar
|
||||
|
||||
public void MoveBy(int x, int y)
|
||||
{
|
||||
Simulation.SendInput.Mouse.MoveMouseBy(x, y);
|
||||
GlobalMethod.MoveMouseBy(x, y);
|
||||
}
|
||||
|
||||
public void KeyDown(string key)
|
||||
@@ -969,4 +970,4 @@ public class Avatar
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ public class CombatScriptParser
|
||||
|
||||
private static List<CombatCommand> ParseLine(string line, HashSet<string> combatAvatarNames, bool validate = true)
|
||||
{
|
||||
line = line.Trim();
|
||||
var oneLineCombatCommands = new List<CombatCommand>();
|
||||
// 以空格分隔角色和指令 截取第一个空格前的内容为角色名称,后面的为指令
|
||||
// 20241116更新 不输入角色名称时,直接以当前角色为准
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,30 +1,35 @@
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BehaviourTree;
|
||||
using BehaviourTree.Composites;
|
||||
using BehaviourTree.FluentBuilder;
|
||||
using BehaviourTree;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.View.Drawable;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Assets;
|
||||
using Vanara.PInvoke;
|
||||
using System.Linq;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using Fischless.WindowsInput;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.Core.Recognition.ONNX;
|
||||
using static Vanara.PInvoke.User32;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Recognition.ONNX;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using BetterGenshinImpact.GameTask.GetGridIcons;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using BetterGenshinImpact.View.Drawable;
|
||||
using Compunet.YoloSharp;
|
||||
using Fischless.WindowsInput;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Vanara.PInvoke;
|
||||
using static Vanara.PInvoke.User32;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
@@ -51,10 +56,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
this._ct = ct;
|
||||
|
||||
IOcrService ocrService = OcrFactory.Paddle;
|
||||
|
||||
IStringLocalizer<AutoFishingImageRecognition> stringLocalizer =
|
||||
App.GetService<IStringLocalizer<AutoFishingImageRecognition>>() ??
|
||||
throw new NullReferenceException(nameof(stringLocalizer));
|
||||
using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary<string, float[]> prototypes);
|
||||
|
||||
Blackboard blackboard = new Blackboard(_predictor, this.Sleep, AutoFishingAssets.Instance);
|
||||
|
||||
@@ -68,7 +70,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
.PushLeaf(() => new TurnAround("转圈圈调整视角", blackboard, _logger, param.SaveScreenshotOnKeyTick, input))
|
||||
.PushLeaf(() => new FindFishTimeout("找到鱼", 20, blackboard, _logger, param.SaveScreenshotOnKeyTick))
|
||||
.End()
|
||||
.PushLeaf(() => new EnterFishingMode("进入钓鱼模式", blackboard, _logger, param.SaveScreenshotOnKeyTick, input, cultureInfo: param.GameCultureInfo))
|
||||
.PushLeaf(() => new EnterFishingMode("进入钓鱼模式", blackboard, _logger, param.SaveScreenshotOnKeyTick, input, session, prototypes, cultureInfo: param.GameCultureInfo, stringLocalizer: param.StringLocalizer))
|
||||
.UntilFailed(@"\")
|
||||
.Sequence("一直钓鱼直到没鱼")
|
||||
.AlwaysSucceed(@"\")
|
||||
@@ -83,7 +85,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
.End()
|
||||
.PushLeaf(() => new FindFishTimeout("确认初始状态和找到鱼", 10, blackboard, _logger, param.SaveScreenshotOnKeyTick))
|
||||
.End()
|
||||
.PushLeaf(() => new ChooseBait("选择鱼饵", blackboard, _logger, param.SaveScreenshotOnKeyTick, TaskContext.Instance().SystemInfo, input))
|
||||
.PushLeaf(() => new ChooseBait("选择鱼饵", blackboard, _logger, param.SaveScreenshotOnKeyTick, TaskContext.Instance().SystemInfo, input, session, prototypes))
|
||||
.MySimpleParallel("抛竿直到成功或出错", policy: SimpleParallelPolicy.OnlyOneMustSucceed)
|
||||
.UntilSuccess("重复抛竿")
|
||||
.Sequence("-")
|
||||
@@ -98,7 +100,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
.End()
|
||||
.MySimpleParallel("下杆中", SimpleParallelPolicy.OnlyOneMustSucceed)
|
||||
.PushLeaf(() => new CheckThrowRod("检查抛竿结果", blackboard, _logger, param.SaveScreenshotOnKeyTick)) // todo 后面串联一个召回率高的下杆中检测方法
|
||||
.PushLeaf(() => new FishBite("自动提竿", blackboard, _logger, param.SaveScreenshotOnKeyTick, input, ocrService, cultureInfo: param.GameCultureInfo, stringLocalizer: stringLocalizer))
|
||||
.PushLeaf(() => new FishBite("自动提竿", blackboard, _logger, param.SaveScreenshotOnKeyTick, input, ocrService, cultureInfo: param.GameCultureInfo, stringLocalizer: param.StringLocalizer))
|
||||
.PushLeaf(() => new FishBiteTimeout("下杆超时检查", param.ThrowRodTimeOutTimeoutSeconds, _logger, param.SaveScreenshotOnKeyTick, input))
|
||||
.End()
|
||||
.MySimpleParallel("拉条中", policy: SimpleParallelPolicy.OnlyOneMustSucceed)
|
||||
@@ -116,7 +118,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
.End()
|
||||
.PushLeaf(() => new WholeProcessTimeout("检查整体超时", param.WholeProcessTimeoutSeconds, _logger, param.SaveScreenshotOnKeyTick))
|
||||
.End()
|
||||
.PushLeaf(() => new QuitFishingMode("退出钓鱼模式", blackboard, _logger, param.SaveScreenshotOnKeyTick, input, param.GameCultureInfo))
|
||||
.PushLeaf(() => new QuitFishingMode("退出钓鱼模式", blackboard, _logger, param.SaveScreenshotOnKeyTick, input, param.GameCultureInfo, param.StringLocalizer))
|
||||
.End()
|
||||
.Build();
|
||||
// @formatter:on
|
||||
@@ -349,6 +351,8 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
private readonly IInputSimulator input;
|
||||
private readonly Blackboard blackboard;
|
||||
private readonly InferenceSession session;
|
||||
private readonly Dictionary<string, float[]> prototypes;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private DateTimeOffset? pressFWaitEndTime;
|
||||
private DateTimeOffset? clickWhiteConfirmButtonWaitEndTime;
|
||||
@@ -356,16 +360,15 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
private readonly string fishingLocalizedString;
|
||||
|
||||
public EnterFishingMode(string name, Blackboard blackboard, ILogger logger, bool saveScreenshotOnTerminate,
|
||||
IInputSimulator input, TimeProvider? timeProvider = null, CultureInfo? cultureInfo = null) : base(name,
|
||||
IInputSimulator input, InferenceSession session, Dictionary<string, float[]> prototypes, TimeProvider? timeProvider = null, CultureInfo? cultureInfo = null, IStringLocalizer? stringLocalizer = null) : base(name,
|
||||
logger, saveScreenshotOnTerminate)
|
||||
{
|
||||
this.blackboard = blackboard;
|
||||
this.input = input;
|
||||
this.session = session;
|
||||
this.prototypes = prototypes;
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
IStringLocalizer<AutoFishingImageRecognition> stringLocalizer =
|
||||
App.GetService<IStringLocalizer<AutoFishingImageRecognition>>() ??
|
||||
throw new NullReferenceException(nameof(stringLocalizer));
|
||||
this.fishingLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "钓鱼");
|
||||
this.fishingLocalizedString = stringLocalizer == null ? "钓鱼" : stringLocalizer.WithCultureGet(cultureInfo, "钓鱼");
|
||||
}
|
||||
|
||||
protected override BehaviourStatus Update(ImageRegion imageRegion)
|
||||
@@ -387,7 +390,17 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
clickWhiteConfirmButtonWaitEndTime < timeProvider.GetLocalNow()) &&
|
||||
Bv.ClickWhiteConfirmButton(imageRegion))
|
||||
{
|
||||
logger.LogInformation("点击开始钓鱼");
|
||||
Mat subMat = imageRegion.SrcMat.SubMat(new Rect((int)(0.824 * imageRegion.Width), (int)(0.669 * imageRegion.Height), (int)(0.065 * imageRegion.Width), (int)(0.065 * imageRegion.Width)));
|
||||
using Mat resized = subMat.Resize(new Size(125, 125));
|
||||
(string predName, _) = GridIconsAccuracyTestTask.Infer(resized, this.session, this.prototypes);
|
||||
if (predName.TryGetEnumValueFromDescription(out this.blackboard.selectedBait))
|
||||
{
|
||||
logger.LogInformation("点击开始钓鱼,当前鱼饵为{bait}", this.blackboard.selectedBait.Value.GetDescription());
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("点击开始钓鱼,当前鱼饵未识别");
|
||||
}
|
||||
|
||||
this.blackboard.pitchReset = true;
|
||||
|
||||
@@ -423,14 +436,11 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
private readonly string fishingLocalizedString;
|
||||
|
||||
public QuitFishingMode(string name, Blackboard blackboard, ILogger logger, bool saveScreenshotOnTerminate,
|
||||
IInputSimulator input, CultureInfo? cultureInfo = null) : base(name, logger, saveScreenshotOnTerminate)
|
||||
IInputSimulator input, CultureInfo? cultureInfo = null, IStringLocalizer? stringLocalizer = null) : base(name, logger, saveScreenshotOnTerminate)
|
||||
{
|
||||
this.blackboard = blackboard;
|
||||
this.input = input;
|
||||
IStringLocalizer<AutoFishingImageRecognition> stringLocalizer =
|
||||
App.GetService<IStringLocalizer<AutoFishingImageRecognition>>() ??
|
||||
throw new NullReferenceException(nameof(stringLocalizer));
|
||||
this.fishingLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "钓鱼");
|
||||
this.fishingLocalizedString = stringLocalizer == null ? "钓鱼" : stringLocalizer.WithCultureGet(cultureInfo, "钓鱼");
|
||||
}
|
||||
|
||||
protected override BehaviourStatus Update(ImageRegion imageRegion)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using Microsoft.ClearScript;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using TorchSharp;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
public class AutoFishingTaskParam : BaseTaskParam
|
||||
public class AutoFishingTaskParam : BaseTaskParam<AutoFishingTask>
|
||||
{
|
||||
public AutoFishingTaskParam(int wholeProcessTimeoutSeconds, int throwRodTimeOutTimeoutSeconds, FishingTimePolicy fishingTimePolicy, bool saveScreenshotOnKeyTick, CultureInfo? cultureInfo, bool useTorch) : base(cultureInfo)
|
||||
public AutoFishingTaskParam(int wholeProcessTimeoutSeconds, int throwRodTimeOutTimeoutSeconds, FishingTimePolicy fishingTimePolicy, bool saveScreenshotOnKeyTick, bool useTorch, CultureInfo? cultureInfo, IStringLocalizer<AutoFishingTask>? stringLocalizer) : base(cultureInfo, stringLocalizer)
|
||||
{
|
||||
WholeProcessTimeoutSeconds = wholeProcessTimeoutSeconds;
|
||||
ThrowRodTimeOutTimeoutSeconds = throwRodTimeOutTimeoutSeconds;
|
||||
@@ -63,7 +64,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
useTorch = false;
|
||||
}
|
||||
|
||||
return new AutoFishingTaskParam(wholeProcessTimeoutSeconds, throwRodTimeOutTimeoutSeconds, fishingTimePolicy, saveScreenshotOnKeyTick, null, useTorch);
|
||||
return new AutoFishingTaskParam(wholeProcessTimeoutSeconds, throwRodTimeOutTimeoutSeconds, fishingTimePolicy, saveScreenshotOnKeyTick, useTorch, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,7 +90,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
useTorch = false;
|
||||
}
|
||||
return new AutoFishingTaskParam(config.WholeProcessTimeoutSeconds, config.AutoThrowRodTimeOut, config.FishingTimePolicy, saveScreenshotOnKeyTick, cultureInfo, useTorch);
|
||||
return new AutoFishingTaskParam(config.WholeProcessTimeoutSeconds, config.AutoThrowRodTimeOut, config.FishingTimePolicy, saveScreenshotOnKeyTick, useTorch, cultureInfo, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BehaviourTree;
|
||||
using BehaviourTree;
|
||||
using BehaviourTree.FluentBuilder;
|
||||
using BehaviourTree.Composites;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
@@ -52,9 +52,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
AutoFishingTaskParam autoFishingTaskParam =
|
||||
AutoFishingTaskParam.BuildFromConfig(TaskContext.Instance().Config.AutoFishingConfig);
|
||||
IOcrService ocrService = OcrFactory.Paddle;
|
||||
IStringLocalizer<AutoFishingImageRecognition> stringLocalizer =
|
||||
App.GetService<IStringLocalizer<AutoFishingImageRecognition>>() ??
|
||||
throw new NullReferenceException(nameof(stringLocalizer));
|
||||
|
||||
this.blackboard = new Blackboard(_predictor, this.Sleep, AutoFishingAssets.Instance);
|
||||
|
||||
BehaviourTreeLaTiao = FluentBuilder.Create<ImageRegion>()
|
||||
@@ -63,7 +61,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
.UntilSuccess("拉条循环")
|
||||
.Sequence("拉条")
|
||||
.PushLeaf(() => new FishBite("自动提竿", blackboard, _logger, false, input, ocrService,
|
||||
cultureInfo: autoFishingTaskParam.GameCultureInfo, stringLocalizer: stringLocalizer))
|
||||
cultureInfo: autoFishingTaskParam.GameCultureInfo, stringLocalizer: autoFishingTaskParam.StringLocalizer))
|
||||
.PushLeaf(() => new GetFishBoxArea("等待拉条出现", blackboard, _logger, false))
|
||||
.PushLeaf(() => new Fishing("钓鱼拉条", blackboard, _logger, false, input))
|
||||
.End()
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
using BehaviourTree;
|
||||
using BehaviourTree;
|
||||
using BetterGenshinImpact.Core.Recognition;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
using BetterGenshinImpact.GameTask.GetGridIcons;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using BetterGenshinImpact.View.Drawable;
|
||||
using Compunet.YoloSharp;
|
||||
using Fischless.WindowsInput;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using static Vanara.PInvoke.User32;
|
||||
using Color = System.Drawing.Color;
|
||||
using Pen = System.Drawing.Pen;
|
||||
using System.Linq;
|
||||
using Fischless.WindowsInput;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System.Globalization;
|
||||
using Compunet.YoloSharp;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
@@ -66,11 +70,11 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
blackboard.fishpond = fishpond;
|
||||
|
||||
string[] chooseBaitfailuresIgnoredBaits = blackboard.chooseBaitFailures.GroupBy(f => f).Where(g => g.Count() >= ChooseBait.MAX_FAILED_TIMES).Select(g => g.Key).ToArray();
|
||||
string[] throwRodNoTargetFishfailuresIgnoredBaits = blackboard.throwRodNoBaitFishFailures.GroupBy(f => f).Where(g => g.Count() >= ThrowRod.MAX_NO_BAIT_FISH_TIMES).Select(g => g.Key).ToArray();
|
||||
BaitType[] chooseBaitfailuresIgnoredBaits = blackboard.chooseBaitFailures.GroupBy(f => f).Where(g => g.Count() >= ChooseBait.MAX_FAILED_TIMES).Select(g => g.Key).ToArray();
|
||||
BaitType[] throwRodNoTargetFishfailuresIgnoredBaits = blackboard.throwRodNoBaitFishFailures.GroupBy(f => f).Where(g => g.Count() >= ThrowRod.MAX_NO_BAIT_FISH_TIMES).Select(g => g.Key).ToArray();
|
||||
|
||||
logger.LogInformation("定位到鱼塘:" + string.Join('、', fishpond.Fishes.GroupBy(f => f.FishType)
|
||||
.Select(g => $"{g.Key.ChineseName}{g.Count()}条" + ((chooseBaitfailuresIgnoredBaits.Contains(g.Key.BaitName) || throwRodNoTargetFishfailuresIgnoredBaits.Contains(g.Key.BaitName)) ? "(忽略)" : ""))
|
||||
.Select(g => $"{g.Key.ChineseName}{g.Count()}条" + ((chooseBaitfailuresIgnoredBaits.Contains(g.Key.BaitType) || throwRodNoTargetFishfailuresIgnoredBaits.Contains(g.Key.BaitType)) ? "(忽略)" : ""))
|
||||
));
|
||||
int i = 0;
|
||||
foreach (var fish in fishpond.Fishes)
|
||||
@@ -80,8 +84,8 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
blackboard.Sleep(1000);
|
||||
drawContent.ClearAll();
|
||||
if (blackboard.fishpond.Fishes.Any(f =>
|
||||
!chooseBaitfailuresIgnoredBaits.Contains(f.FishType.BaitName)
|
||||
&& !throwRodNoTargetFishfailuresIgnoredBaits.Contains(f.FishType.BaitName)))
|
||||
!chooseBaitfailuresIgnoredBaits.Contains(f.FishType.BaitType)
|
||||
&& !throwRodNoTargetFishfailuresIgnoredBaits.Contains(f.FishType.BaitType)))
|
||||
{
|
||||
return BehaviourStatus.Succeeded;
|
||||
}
|
||||
@@ -100,6 +104,8 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
private readonly ISystemInfo systemInfo;
|
||||
private readonly IInputSimulator input;
|
||||
private readonly InferenceSession session;
|
||||
private readonly Dictionary<string, float[]> prototypes;
|
||||
private readonly Blackboard blackboard;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private DateTimeOffset? chooseBaitUIOpenWaitEndTime; // 等待选鱼饵界面出现并尝试找鱼饵的结束时间
|
||||
@@ -110,11 +116,13 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="autoFishingTrigger"></param>
|
||||
public ChooseBait(string name, Blackboard blackboard, ILogger logger, bool saveScreenshotOnTerminat, ISystemInfo systemInfo, IInputSimulator input, TimeProvider? timeProvider = null) : base(name, logger, saveScreenshotOnTerminat)
|
||||
public ChooseBait(string name, Blackboard blackboard, ILogger logger, bool saveScreenshotOnTerminat, ISystemInfo systemInfo, IInputSimulator input, InferenceSession session, Dictionary<string, float[]> prototypes, TimeProvider? timeProvider = null) : base(name, logger, saveScreenshotOnTerminat)
|
||||
{
|
||||
this.blackboard = blackboard;
|
||||
this.systemInfo = systemInfo;
|
||||
this.input = input;
|
||||
this.session = session;
|
||||
this.prototypes = prototypes;
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
@@ -122,7 +130,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
if (this.Status == BehaviourStatus.Ready)
|
||||
{
|
||||
if (blackboard.fishpond.Fishes.Any(f => f.FishType.BaitName == blackboard.selectedBaitName)) // 如果该种鱼没钓完就不用换饵
|
||||
if (blackboard.fishpond.Fishes.Any(f => f.FishType.BaitType == blackboard.selectedBait)) // 如果该种鱼没钓完就不用换饵
|
||||
{
|
||||
return BehaviourStatus.Succeeded;
|
||||
}
|
||||
@@ -136,72 +144,86 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
return BehaviourStatus.Running;
|
||||
}
|
||||
|
||||
blackboard.selectedBaitName = blackboard.fishpond.Fishes.GroupBy(f => f.FishType.BaitName)
|
||||
blackboard.selectedBait = blackboard.fishpond.Fishes.GroupBy(f => f.FishType.BaitType)
|
||||
.Where(b => !blackboard.chooseBaitFailures.GroupBy(f => f).Where(g => g.Count() >= MAX_FAILED_TIMES).Any(g => g.Key == b.Key)) // 不能是已经失败两次的饵
|
||||
.OrderByDescending(g => g.Count()).First().Key; // 选择最多鱼吃的饵料
|
||||
logger.LogInformation("选择鱼饵 {Text}", BaitType.FromName(blackboard.selectedBaitName).ChineseName);
|
||||
logger.LogInformation("选择鱼饵 {Text}", blackboard.selectedBait.GetDescription());
|
||||
|
||||
// 寻找鱼饵
|
||||
var ro = new RecognitionObject
|
||||
{
|
||||
Name = "ChooseBait",
|
||||
RecognitionType = RecognitionTypes.TemplateMatch,
|
||||
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFishing", $"bait\\{blackboard.selectedBaitName}.png", systemInfo),
|
||||
Threshold = 0.8,
|
||||
Use3Channels = true,
|
||||
DrawOnWindow = false
|
||||
}.InitTemplate();
|
||||
using ImageRegion singleRowGrid = imageRegion.DeriveCrop(0.28 * imageRegion.Width, 0.37 * imageRegion.Height, 0.45 * imageRegion.Width, 0.22 * imageRegion.Height);
|
||||
using Mat grey = singleRowGrid.SrcMat.CvtColor(ColorConversionCodes.BGR2GRAY);
|
||||
using Mat canny = grey.Canny(20, 40);
|
||||
|
||||
using var resRa = imageRegion.Find(ro);
|
||||
if (resRa.IsEmpty())
|
||||
{
|
||||
if (timeProvider.GetLocalNow() >= chooseBaitUIOpenWaitEndTime)
|
||||
Cv2.FindContours(canny, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null);
|
||||
contours = contours
|
||||
.Where(c =>
|
||||
{
|
||||
logger.LogWarning("没有找到目标鱼饵");
|
||||
input.Keyboard.KeyPress(VK.VK_ESCAPE);
|
||||
Rect r = Cv2.BoundingRect(c);
|
||||
if (r.Width < 0.065 * imageRegion.Width * 0.80) // 剔除太小的
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (r.Height == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Math.Abs((float)r.Width / r.Height - 0.81) < 0.05; // 按形状筛选
|
||||
}).ToArray();
|
||||
IEnumerable<Rect> boxes = contours.Select(Cv2.BoundingRect);
|
||||
|
||||
foreach (Rect box in boxes)
|
||||
{
|
||||
using ImageRegion resRa = singleRowGrid.DeriveCrop(box);
|
||||
using Mat img125 = resRa.SrcMat.GetGridIcon();
|
||||
(string predName, _) = GridIconsAccuracyTestTask.Infer(img125, this.session, this.prototypes);
|
||||
if (predName == blackboard.selectedBait.GetDescription())
|
||||
{
|
||||
resRa.Click();
|
||||
blackboard.Sleep(700);
|
||||
// 可能重复点击,所以固定界面点击下
|
||||
imageRegion.ClickTo((int)(imageRegion.Width * 0.675), (int)(imageRegion.Height / 3d));
|
||||
blackboard.Sleep(200);
|
||||
// 点击确定
|
||||
using var ra = imageRegion.Find(new RecognitionObject
|
||||
{
|
||||
Name = "BtnWhiteConfirm",
|
||||
RecognitionType = RecognitionTypes.TemplateMatch,
|
||||
TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_white_confirm.png", systemInfo),
|
||||
Use3Channels = true
|
||||
}.InitTemplate());
|
||||
if (ra.IsExist())
|
||||
{
|
||||
ra.Click();
|
||||
}
|
||||
blackboard.chooseBaitUIOpening = false;
|
||||
logger.LogInformation("退出换饵界面");
|
||||
blackboard.Sleep(500); // 等待界面切换
|
||||
|
||||
blackboard.chooseBaitFailures.Add(blackboard.selectedBaitName);
|
||||
if (blackboard.chooseBaitFailures.Count(f => f == blackboard.selectedBaitName) >= MAX_FAILED_TIMES)
|
||||
{
|
||||
logger.LogWarning($"本次将忽略{BaitType.FromName(blackboard.selectedBaitName).ChineseName}");
|
||||
}
|
||||
|
||||
blackboard.selectedBaitName = string.Empty;
|
||||
|
||||
return BehaviourStatus.Failed;
|
||||
return BehaviourStatus.Succeeded;
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
if (timeProvider.GetLocalNow() >= chooseBaitUIOpenWaitEndTime)
|
||||
{
|
||||
logger.LogWarning("没有找到目标鱼饵");
|
||||
input.Keyboard.KeyPress(VK.VK_ESCAPE);
|
||||
blackboard.chooseBaitUIOpening = false;
|
||||
logger.LogInformation("退出换饵界面");
|
||||
|
||||
blackboard.chooseBaitFailures.Add(blackboard.selectedBait.Value);
|
||||
if (blackboard.chooseBaitFailures.Count(f => f == blackboard.selectedBait) >= MAX_FAILED_TIMES)
|
||||
{
|
||||
return BehaviourStatus.Running;
|
||||
logger.LogWarning($"本次将忽略{blackboard.selectedBait.GetDescription()}");
|
||||
}
|
||||
|
||||
blackboard.selectedBait = null;
|
||||
|
||||
return BehaviourStatus.Failed;
|
||||
}
|
||||
else
|
||||
{
|
||||
resRa.Click();
|
||||
blackboard.Sleep(700);
|
||||
// 可能重复点击,所以固定界面点击下
|
||||
imageRegion.ClickTo((int)(imageRegion.Width * 0.675), (int)(imageRegion.Height / 3d));
|
||||
blackboard.Sleep(200);
|
||||
// 点击确定
|
||||
using var ra = imageRegion.Find(new RecognitionObject
|
||||
{
|
||||
Name = "BtnWhiteConfirm",
|
||||
RecognitionType = RecognitionTypes.TemplateMatch,
|
||||
TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_white_confirm.png", systemInfo),
|
||||
Use3Channels = true
|
||||
}.InitTemplate());
|
||||
if (ra.IsExist())
|
||||
{
|
||||
ra.Click();
|
||||
}
|
||||
blackboard.chooseBaitUIOpening = false;
|
||||
logger.LogInformation("退出换饵界面");
|
||||
blackboard.Sleep(500); // 等待界面切换
|
||||
return BehaviourStatus.Running;
|
||||
}
|
||||
|
||||
return BehaviourStatus.Succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,10 +396,10 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
|
||||
// 找到落点最近的鱼
|
||||
currentFish = null;
|
||||
string[] ignoredBaits = blackboard.throwRodNoBaitFishFailures.GroupBy(f => f).Where(g => g.Count() >= MAX_NO_BAIT_FISH_TIMES).Select(g => g.Key).ToArray();
|
||||
BaitType[] ignoredBaits = blackboard.throwRodNoBaitFishFailures.GroupBy(f => f).Where(g => g.Count() >= MAX_NO_BAIT_FISH_TIMES).Select(g => g.Key).ToArray();
|
||||
var list = fishpond.Fishes
|
||||
.Where(f => !ignoredBaits.Contains(f.FishType.BaitName)) // 不能是已经失败两次的饵;
|
||||
.Where(f => f.FishType.BaitName == blackboard.selectedBaitName).OrderByDescending(f => f.Confidence)
|
||||
.Where(f => !ignoredBaits.Contains(f.FishType.BaitType)) // 不能是已经失败两次的饵;
|
||||
.Where(f => f.FishType.BaitType == blackboard.selectedBait).OrderByDescending(f => f.Confidence)
|
||||
.ToList();
|
||||
if (list.Count > 0)
|
||||
{
|
||||
@@ -393,13 +415,17 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
// 没有找到鱼饵适用鱼,重新选择鱼饵
|
||||
blackboard.throwRodNoBaitFish = true;
|
||||
blackboard.throwRodNoBaitFishFailures.Add(blackboard.selectedBaitName);
|
||||
if (blackboard.throwRodNoBaitFishFailures.Count(f => f == blackboard.selectedBaitName) >= MAX_NO_BAIT_FISH_TIMES)
|
||||
if (blackboard.selectedBait == null)
|
||||
{
|
||||
logger.LogWarning($"本次将忽略{BaitType.FromName(blackboard.selectedBaitName).ChineseName}");
|
||||
throw new NullReferenceException();
|
||||
}
|
||||
blackboard.throwRodNoBaitFishFailures.Add(blackboard.selectedBait.Value);
|
||||
if (blackboard.throwRodNoBaitFishFailures.Count(f => f == blackboard.selectedBait) >= MAX_NO_BAIT_FISH_TIMES)
|
||||
{
|
||||
logger.LogWarning("本次将忽略{bait}", blackboard.selectedBait.GetDescription());
|
||||
}
|
||||
|
||||
blackboard.selectedBaitName = string.Empty;
|
||||
blackboard.selectedBait = null;
|
||||
logger.LogInformation("没有找到鱼饵适用鱼");
|
||||
input.Mouse.LeftButtonUp();
|
||||
blackboard.Sleep(2000);
|
||||
@@ -684,7 +710,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
private readonly DrawContent drawContent;
|
||||
private readonly IOcrService ocrService;
|
||||
private readonly string getABiteLocalizedString;
|
||||
public FishBite(string name, Blackboard blackboard, ILogger logger, bool saveScreenshotOnTerminat, IInputSimulator input, IOcrService ocrService, DrawContent? drawContent = null, CultureInfo? cultureInfo = null, IStringLocalizer<AutoFishingImageRecognition>? stringLocalizer = null) : base(name, logger, saveScreenshotOnTerminat)
|
||||
public FishBite(string name, Blackboard blackboard, ILogger logger, bool saveScreenshotOnTerminat, IInputSimulator input, IOcrService ocrService, DrawContent? drawContent = null, CultureInfo? cultureInfo = null, IStringLocalizer? stringLocalizer = null) : base(name, logger, saveScreenshotOnTerminat)
|
||||
{
|
||||
this.blackboard = blackboard;
|
||||
this.input = input;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BetterGenshinImpact.Core.Recognition.ONNX;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Assets;
|
||||
@@ -18,9 +18,9 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
public bool abort = false;
|
||||
|
||||
/// <summary>
|
||||
/// 已选择的鱼饵名
|
||||
/// 已选择的鱼饵类型
|
||||
/// </summary>
|
||||
public string selectedBaitName = string.Empty;
|
||||
public BaitType? selectedBait = null;
|
||||
|
||||
/// <summary>
|
||||
/// 鱼塘
|
||||
@@ -44,9 +44,9 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
|
||||
/// <summary>
|
||||
/// 抛竿无目标鱼失败列表
|
||||
/// 失败一次就加入一次鱼饵名,列表中同名鱼饵的数量代表该种失败了几次
|
||||
/// 失败一次就加入一次鱼饵类型,列表中同名鱼饵的数量代表该种失败了几次
|
||||
/// </summary>
|
||||
public List<string> throwRodNoBaitFishFailures = new List<string>();
|
||||
public List<BaitType> throwRodNoBaitFishFailures = new List<BaitType>();
|
||||
|
||||
/// <summary>
|
||||
/// 拉条位置的识别框
|
||||
@@ -61,9 +61,9 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
|
||||
/// <summary>
|
||||
/// 选鱼饵失败列表
|
||||
/// 失败一次就加入一次鱼饵名,列表中同名鱼饵的数量代表该种失败了几次
|
||||
/// 失败一次就加入一次鱼饵类型,列表中同名鱼饵的数量代表该种失败了几次
|
||||
/// </summary>
|
||||
public List<string> chooseBaitFailures = new List<string>();
|
||||
public List<BaitType> chooseBaitFailures = new List<BaitType>();
|
||||
|
||||
/// <summary>
|
||||
/// 镜头俯仰是否被行为重置
|
||||
@@ -104,12 +104,12 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
|
||||
{
|
||||
abort = false;
|
||||
throwRodNoTargetTimes = 0;
|
||||
throwRodNoBaitFishFailures = new List<string>();
|
||||
throwRodNoBaitFishFailures = new List<BaitType>();
|
||||
fishBoxRect = default;
|
||||
chooseBaitUIOpening = false;
|
||||
chooseBaitFailures = new List<string>();
|
||||
chooseBaitFailures = new List<BaitType>();
|
||||
pitchReset = true;
|
||||
selectedBaitName = string.Empty;
|
||||
selectedBait = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
|
||||
public class BaitType
|
||||
public enum BaitType
|
||||
{
|
||||
|
||||
public static readonly BaitType FruitPasteBait = new("fruit paste bait", "果酿饵");
|
||||
public static readonly BaitType RedrotBait = new("redrot bait", "赤糜饵");
|
||||
public static readonly BaitType FalseWormBait = new("false worm bait", "蠕虫假饵");
|
||||
public static readonly BaitType FakeFlyBait = new("fake fly bait", "飞蝇假饵");
|
||||
public static readonly BaitType SugardewBait = new("sugardew bait", "甘露饵");
|
||||
public static readonly BaitType SourBait = new("sour bait", "酸桔饵");
|
||||
public static readonly BaitType FlashingMaintenanceMekBait = new("flashing maintenance mek bait", "维护机关频闪诱饵");
|
||||
public static readonly BaitType SpinelgrainBait = new("spinelgrain bait", "澄晶果粒饵");
|
||||
public static readonly BaitType EmberglowBait = new("emberglow bait", "温火饵");
|
||||
|
||||
public static IEnumerable<BaitType> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return FruitPasteBait;
|
||||
yield return RedrotBait;
|
||||
yield return FalseWormBait;
|
||||
yield return FakeFlyBait;
|
||||
yield return SugardewBait;
|
||||
yield return SourBait;
|
||||
yield return FlashingMaintenanceMekBait;
|
||||
yield return SpinelgrainBait;
|
||||
yield return EmberglowBait;
|
||||
}
|
||||
}
|
||||
public string Name { get; private set; }
|
||||
public string ChineseName { get; private set; }
|
||||
|
||||
private BaitType(string name, string chineseName)
|
||||
{
|
||||
Name = name;
|
||||
ChineseName = chineseName;
|
||||
}
|
||||
|
||||
public static BaitType FromName(string name)
|
||||
{
|
||||
foreach (var type in Values)
|
||||
{
|
||||
if (type.Name == name)
|
||||
{
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException($"BaitType {name} not found");
|
||||
}
|
||||
[Description("果酿饵")]
|
||||
FruitPasteBait,
|
||||
[Description("赤糜饵")]
|
||||
RedrotBait,
|
||||
[Description("蠕虫假饵")]
|
||||
FalseWormBait,
|
||||
[Description("飞蝇假饵")]
|
||||
FakeFlyBait,
|
||||
[Description("甘露饵")]
|
||||
SugardewBait,
|
||||
[Description("酸桔饵")]
|
||||
SourBait,
|
||||
[Description("维护机关频闪诱饵")]
|
||||
FlashingMaintenanceMekBait,
|
||||
[Description("澄晶果粒饵")]
|
||||
SpinelgrainBait,
|
||||
[Description("温火饵")]
|
||||
EmberglowBait
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
@@ -9,27 +9,27 @@ namespace BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
/// </summary>
|
||||
public class BigFishType
|
||||
{
|
||||
public static readonly BigFishType Medaka = new("medaka", "fruit paste bait", "花鳉", 0);
|
||||
public static readonly BigFishType LargeMedaka = new("large medaka", "fruit paste bait", "大花鳉", 1);
|
||||
public static readonly BigFishType Stickleback = new("stickleback", "redrot bait", "棘鱼", 2);
|
||||
public static readonly BigFishType Koi = new("koi", "fake fly bait", "假龙", 3);
|
||||
public static readonly BigFishType KoiHead = new("koi head", "fake fly bait", "假龙头", 3);
|
||||
public static readonly BigFishType Butterflyfish = new("butterflyfish", "false worm bait", "蝶鱼", 4);
|
||||
public static readonly BigFishType Pufferfish = new("pufferfish", "fake fly bait", "炮鲀", 5);
|
||||
public static readonly BigFishType Medaka = new("medaka", BaitType.FruitPasteBait, "花鳉", 0);
|
||||
public static readonly BigFishType LargeMedaka = new("large medaka", BaitType.FruitPasteBait, "大花鳉", 1);
|
||||
public static readonly BigFishType Stickleback = new("stickleback", BaitType.RedrotBait, "棘鱼", 2);
|
||||
public static readonly BigFishType Koi = new("koi", BaitType.FakeFlyBait, "假龙", 3);
|
||||
public static readonly BigFishType KoiHead = new("koi head", BaitType.FakeFlyBait, "假龙头", 3);
|
||||
public static readonly BigFishType Butterflyfish = new("butterflyfish", BaitType.FalseWormBait, "蝶鱼", 4);
|
||||
public static readonly BigFishType Pufferfish = new("pufferfish", BaitType.FakeFlyBait, "炮鲀", 5);
|
||||
|
||||
public static readonly BigFishType Ray = new("ray", "fake fly bait", "鳐", 6);
|
||||
public static readonly BigFishType Ray = new("ray", BaitType.FakeFlyBait, "鳐", 6);
|
||||
|
||||
// public static readonly BigFishType FormaloRay = new("formalo ray", "fake fly bait", "佛玛洛鳐");
|
||||
// public static readonly BigFishType DivdaRay = new("divda ray", "fake fly bait", "迪芙妲鳐");
|
||||
public static readonly BigFishType Angler = new("angler", "sugardew bait", "角鲀", 7);
|
||||
public static readonly BigFishType AxeMarlin = new("axe marlin", "sugardew bait", "斧枪鱼", 8);
|
||||
public static readonly BigFishType HeartfeatherBass = new("heartfeather bass", "sour bait", "心羽鲈", 9);
|
||||
public static readonly BigFishType MaintenanceMek = new("maintenance mek", "flashing maintenance mek bait", "维护机关", 10);
|
||||
public static readonly BigFishType Unihornfish = new("unihornfish", "spinelgrain bait", "独角鱼", 10);
|
||||
public static readonly BigFishType Sunfish = new("sunfish", "spinelgrain bait", "翻车鲀", 7);
|
||||
public static readonly BigFishType Rapidfish = new("rapidfish", "spinelgrain bait", "斗士急流鱼", 9);
|
||||
public static readonly BigFishType PhonyUnihornfish = new("phony unihornfish", "emberglow bait", "燃素独角鱼", 10);
|
||||
public static readonly BigFishType MagmaRapidfish = new("magma rapidfish", "emberglow bait", "炽岩斗士急流鱼", 9);
|
||||
// public static readonly BigFishType FormaloRay = new("formalo ray", "飞蝇假饵", "佛玛洛鳐");
|
||||
// public static readonly BigFishType DivdaRay = new("divda ray", "飞蝇假饵", "迪芙妲鳐");
|
||||
public static readonly BigFishType Angler = new("angler", BaitType.SugardewBait, "角鲀", 7);
|
||||
public static readonly BigFishType AxeMarlin = new("axe marlin", BaitType.SugardewBait, "斧枪鱼", 8);
|
||||
public static readonly BigFishType HeartfeatherBass = new("heartfeather bass", BaitType.SourBait, "心羽鲈", 9);
|
||||
public static readonly BigFishType MaintenanceMek = new("maintenance mek", BaitType.FlashingMaintenanceMekBait, "维护机关", 10);
|
||||
public static readonly BigFishType Unihornfish = new("unihornfish", BaitType.SpinelgrainBait, "独角鱼", 10);
|
||||
public static readonly BigFishType Sunfish = new("sunfish", BaitType.SpinelgrainBait, "翻车鲀", 7);
|
||||
public static readonly BigFishType Rapidfish = new("rapidfish", BaitType.SpinelgrainBait, "斗士急流鱼", 9);
|
||||
public static readonly BigFishType PhonyUnihornfish = new("phony unihornfish", BaitType.EmberglowBait, "燃素独角鱼", 10);
|
||||
public static readonly BigFishType MagmaRapidfish = new("magma rapidfish", BaitType.EmberglowBait, "炽岩斗士急流鱼", 9);
|
||||
|
||||
|
||||
public static IEnumerable<BigFishType> Values
|
||||
@@ -59,15 +59,15 @@ public class BigFishType
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
public string BaitName { get; private set; }
|
||||
public BaitType BaitType { get; private set; }
|
||||
public string ChineseName { get; private set; }
|
||||
|
||||
public int NetIndex { get; private set; }
|
||||
|
||||
private BigFishType(string name, string baitName, string chineseName, int netIndex)
|
||||
private BigFishType(string name, BaitType baitType, string chineseName, int netIndex)
|
||||
{
|
||||
Name = name;
|
||||
BaitName = baitName;
|
||||
BaitType = baitType;
|
||||
ChineseName = chineseName;
|
||||
NetIndex = netIndex;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation;
|
||||
|
||||
public class GeniusInvokationTaskParam(string strategyContent) : BaseTaskParam
|
||||
public class GeniusInvokationTaskParam : BaseTaskParam<AutoGeniusInvokationTask>
|
||||
{
|
||||
public string StrategyContent { get; set; } = strategyContent;
|
||||
public GeniusInvokationTaskParam(string strategyContent) : base(null, null)
|
||||
{
|
||||
this.StrategyContent = strategyContent;
|
||||
}
|
||||
|
||||
public string StrategyContent { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoMusicGame;
|
||||
|
||||
public class AutoMusicGameParam : BaseTaskParam;
|
||||
public class AutoMusicGameParam : BaseTaskParam<AutoMusicGameTask>
|
||||
{
|
||||
public AutoMusicGameParam() : base(null, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoSkip.Model;
|
||||
|
||||
public class AutoTrackParam : BaseTaskParam
|
||||
public class AutoTrackParam : BaseTaskParam<AutoTrackTask>
|
||||
{
|
||||
public AutoTrackParam() : base(null, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BetterGenshinImpact.Core.BgiVision;
|
||||
using BetterGenshinImpact.Core.Recognition;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.Core.Simulator.Extensions;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.AutoDomain;
|
||||
using BetterGenshinImpact.GameTask.AutoDomain.Model;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Model;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Script;
|
||||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
|
||||
using BetterGenshinImpact.GameTask.AutoPick.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoTrackPath;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||||
@@ -29,6 +19,12 @@ using BetterGenshinImpact.Helpers.Extensions;
|
||||
using BetterGenshinImpact.Service.Notification;
|
||||
using BetterGenshinImpact.Service.Notification.Model.Enum;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static BetterGenshinImpact.GameTask.Common.TaskControl;
|
||||
using static Vanara.PInvoke.User32;
|
||||
|
||||
@@ -95,7 +91,7 @@ public class AutoStygianOnslaughtTask : ISoloTask
|
||||
{
|
||||
throw new Exception("幽境危战进入秘境失败!");
|
||||
}
|
||||
|
||||
|
||||
await Delay(1500, _ct); // 开始的三秒计时
|
||||
|
||||
// 队伍没初始化成功则重试
|
||||
@@ -203,7 +199,7 @@ public class AutoStygianOnslaughtTask : ISoloTask
|
||||
|
||||
// F5 打开活动
|
||||
Simulation.SendInput.SimulateAction(GIActions.OpenTheEventsMenu);
|
||||
await page.GetByText("活动一览").WithRoi(r => r.CutLeftTop(0.3,0.2)).WaitFor();
|
||||
await page.GetByText("活动一览").WithRoi(r => r.CutLeftTop(0.3, 0.2)).WaitFor();
|
||||
await Delay(500, _ct);
|
||||
|
||||
if (page.GetByText("幽境危战").WithRoi(r => r.CutRight(0.5)).IsExist())
|
||||
@@ -613,7 +609,7 @@ public class AutoStygianOnslaughtTask : ISoloTask
|
||||
star = 4;
|
||||
}
|
||||
|
||||
await new AutoArtifactSalvageTask(star).Start(_ct);
|
||||
await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star, javaScript: null, artifactSetFilter: null, maxNumToCheck: null, recognitionFailurePolicy: null)).Start(_ct);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoTrackPath;
|
||||
|
||||
public class AutoTrackPathParam : BaseTaskParam
|
||||
[Obsolete]
|
||||
public class AutoTrackPathParam
|
||||
{
|
||||
}
|
||||
|
||||
@@ -529,7 +529,7 @@ public partial class AutoWoodTask : ISoloTask
|
||||
if (!ra.IsEmpty())
|
||||
{
|
||||
clickCnt++;
|
||||
GameCaptureRegion.GameRegion1080PPosClick(955, 666);
|
||||
GameCaptureRegion.GameRegion1080PPosClick(960, 630);
|
||||
Debug.WriteLine("[AutoWood] Click entry");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -8,13 +6,8 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Vanara.PInvoke;
|
||||
using static BetterGenshinImpact.GameTask.Common.TaskControl;
|
||||
using BetterGenshinImpact.Core.Recognition;
|
||||
using Fischless.GameCapture;
|
||||
using OpenCvSharp;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoWood.Utils;
|
||||
|
||||
@@ -110,64 +103,28 @@ internal sealed class Login3rdParty
|
||||
{
|
||||
if (Process.GetProcessesByName("YuanShen").FirstOrDefault() is Process process)
|
||||
{
|
||||
// 使用新的B服登录逻辑
|
||||
// B服登录
|
||||
var (loginWindow, windowType) = GetBiliLoginWindow(process);
|
||||
if (loginWindow != IntPtr.Zero)
|
||||
{
|
||||
if (windowType.Contains("协议"))
|
||||
{
|
||||
ImageRegion screen;
|
||||
try
|
||||
{
|
||||
screen = CaptureWindowToRectArea(loginWindow);
|
||||
}
|
||||
catch
|
||||
{
|
||||
screen = CaptureToRectArea();
|
||||
}
|
||||
var ocrList = screen.FindMulti(RecognitionObject.OcrThis);
|
||||
var agreeRegion = ocrList.FirstOrDefault(r =>
|
||||
r.Text.Contains("同意") && !r.Text.Contains("不同意"));
|
||||
if (agreeRegion != null)
|
||||
{
|
||||
ClickRegionCenterBy1080(agreeRegion);
|
||||
// 记录协议窗口点击位置
|
||||
var (centerDesktopX, centerDesktopY) = agreeRegion.ConvertPositionToDesktopRegion(agreeRegion.Width / 2, agreeRegion.Height / 2);
|
||||
var captureRect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
|
||||
var inCaptureX = centerDesktopX - captureRect.X;
|
||||
var inCaptureY = centerDesktopY - captureRect.Y;
|
||||
var scale = TaskContext.Instance().SystemInfo.ScaleTo1080PRatio;
|
||||
lastAgreementClickPos = (inCaptureX / scale, inCaptureY / scale);
|
||||
Debug.WriteLine("[AutoWood] Click protocol window for Bilibili using OCR");
|
||||
}
|
||||
|
||||
Thread.Sleep(2000);
|
||||
GameCaptureRegion.GameRegion1080PPosClick(1030, 615);
|
||||
|
||||
// 检查窗口是否还存在
|
||||
var (remainingWindow, remainingType) = GetBiliLoginWindow(process);
|
||||
if (remainingWindow == IntPtr.Zero || !remainingType.Contains("协议"))
|
||||
{
|
||||
// 协议窗口已消失,继续等待登录窗口
|
||||
return false; // 继续循环等待登录窗口
|
||||
return false;
|
||||
}
|
||||
|
||||
return false; // 协议窗口仍然存在,继续尝试
|
||||
// 协议窗口仍然存在,继续尝试
|
||||
return false;
|
||||
}
|
||||
if (windowType.Contains("登录"))
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
// 使用协议窗口坐标或默认坐标点击登录
|
||||
if (lastAgreementClickPos.HasValue)
|
||||
{
|
||||
GameCaptureRegion.GameRegion1080PPosClick(lastAgreementClickPos.Value.x1080, lastAgreementClickPos.Value.y1080);
|
||||
Debug.WriteLine("[AutoWood] Click login window for Bilibili");
|
||||
}
|
||||
else
|
||||
{
|
||||
GameCaptureRegion.GameRegion1080PPosClick(960, 620);
|
||||
Debug.WriteLine("[AutoWood] Click login window for Bilibili");
|
||||
}
|
||||
|
||||
GameCaptureRegion.GameRegion1080PPosClick(960, 630);
|
||||
Thread.Sleep(2000);
|
||||
|
||||
// 检查窗口是否还存在
|
||||
@@ -262,99 +219,4 @@ internal sealed class Login3rdParty
|
||||
|
||||
return (bHWnd, windowType);
|
||||
}
|
||||
|
||||
private ImageRegion CaptureWindowToRectArea(IntPtr hWnd)
|
||||
{
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentException("无效的窗口句柄", nameof(hWnd));
|
||||
}
|
||||
|
||||
// BitBlt 方式
|
||||
try
|
||||
{
|
||||
using var bitblt = GameCaptureFactory.Create(CaptureModes.BitBlt);
|
||||
bitblt.Start(hWnd, new Dictionary<string, object> { { "autoFixWin11BitBlt", true } });
|
||||
var img = GrabOneFrame(bitblt);
|
||||
if (img == null) throw new Exception("BitBlt 无帧");
|
||||
Debug.WriteLine($"[AutoWood] BitBlt 捕获窗口成功,尺寸 {img.Width}x{img.Height}");
|
||||
return BuildWindowClientRegion(hWnd, img);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine($"[AutoWood] BitBlt 捕获失败: {e.Message}");
|
||||
}
|
||||
|
||||
// DwmSharedSurface方式
|
||||
try
|
||||
{
|
||||
using var dwm = GameCaptureFactory.Create(CaptureModes.DwmGetDxSharedSurface);
|
||||
dwm.Start(hWnd);
|
||||
var img = GrabOneFrame(dwm);
|
||||
if (img == null) throw new Exception("Dwm 无帧");
|
||||
Debug.WriteLine($"[AutoWood] DwmSharedSurface 捕获窗口成功,尺寸 {img.Width}x{img.Height}");
|
||||
return BuildWindowClientRegion(hWnd, img);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine($"[AutoWood] DwmSharedSurface 捕获失败,准备回退: {e.Message}");
|
||||
}
|
||||
|
||||
// 全部失败,抛出异常由调用方回退到游戏截图
|
||||
throw new Exception("针对句柄的窗口截图失败");
|
||||
}
|
||||
|
||||
private ImageRegion BuildWindowClientRegion(IntPtr hWnd, Mat img)
|
||||
{
|
||||
// 以窗口客户区的屏幕坐标为锚点,构造相对桌面的区域,使 Region.Click 映射到正确位置
|
||||
if (!User32.GetClientRect(hWnd, out var clientRect))
|
||||
{
|
||||
// 回退:用窗口矩形
|
||||
User32.GetWindowRect(hWnd, out var wr);
|
||||
return TaskContext.Instance().SystemInfo.DesktopRectArea.Derive(img, wr.left, wr.top);
|
||||
}
|
||||
POINT pt = default;
|
||||
User32.ClientToScreen(hWnd, ref pt);
|
||||
return TaskContext.Instance().SystemInfo.DesktopRectArea.Derive(img, pt.X, pt.Y);
|
||||
}
|
||||
|
||||
private Mat? GrabOneFrame(IGameCapture capture, int retry = 8, int delayMs = 40)
|
||||
{
|
||||
for (int i = 0; i < retry; i++)
|
||||
{
|
||||
var img = capture.Capture();
|
||||
if (img != null)
|
||||
{
|
||||
return img;
|
||||
}
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Region 中心点映射到 1080P 坐标系并点击。
|
||||
/// </summary>
|
||||
private void ClickRegionCenterBy1080(Region region)
|
||||
{
|
||||
// 计算区域中心的桌面坐标
|
||||
var (centerDesktopX, centerDesktopY) = region.ConvertPositionToDesktopRegion(region.Width / 2, region.Height / 2);
|
||||
|
||||
// 转换到游戏捕获区域坐标
|
||||
var captureRect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
|
||||
var inCaptureX = centerDesktopX - captureRect.X;
|
||||
var inCaptureY = centerDesktopY - captureRect.Y;
|
||||
if (inCaptureX < 0 || inCaptureY < 0)
|
||||
{
|
||||
// 不在游戏捕获区域内,直接回退为桌面点击
|
||||
DesktopRegion.DesktopRegionClick(centerDesktopX, centerDesktopY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 映射为 1080P 坐标
|
||||
var scale = TaskContext.Instance().SystemInfo.ScaleTo1080PRatio;
|
||||
var x1080 = inCaptureX / scale;
|
||||
var y1080 = inCaptureY / scale;
|
||||
GameCaptureRegion.GameRegion1080PPosClick(x1080, y1080);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoWood;
|
||||
|
||||
public class WoodTaskParam : BaseTaskParam
|
||||
public class WoodTaskParam : BaseTaskParam<AutoWoodTask>
|
||||
{
|
||||
public int WoodRoundNum { get; set; }
|
||||
public int WoodDailyMaxCount { get; set; }
|
||||
|
||||
public WoodTaskParam(int woodRoundNum, int woodDailyMaxCount)
|
||||
public WoodTaskParam(int woodRoundNum, int woodDailyMaxCount) : base(null, null)
|
||||
{
|
||||
WoodRoundNum = woodRoundNum;
|
||||
if (woodRoundNum == 0)
|
||||
|
||||
@@ -59,6 +59,45 @@ public static partial class Bv
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否在秘境中
|
||||
/// </summary>
|
||||
/// <param name="captureRa"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsInDomain(ImageRegion captureRa)
|
||||
{
|
||||
using var matchRegion = captureRa.Find(ElementAssets.Instance.InDomainRo);
|
||||
if (matchRegion.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsWhite(int r, int g, int b)
|
||||
{
|
||||
return (r >= 240 && r <= 255) &&
|
||||
(g >= 240 && g <= 255) &&
|
||||
(b >= 240 && b <= 255);
|
||||
}
|
||||
|
||||
// 若全部为白色则视为不在秘境中
|
||||
var samplePoints = new[]
|
||||
{
|
||||
new Point(matchRegion.X + matchRegion.Width / 2, matchRegion.Y + matchRegion.Height / 2),
|
||||
new Point(matchRegion.X + matchRegion.Width / 4, matchRegion.Y + matchRegion.Height / 4),
|
||||
new Point(matchRegion.X + matchRegion.Width * 3 / 4, matchRegion.Y + matchRegion.Height / 4),
|
||||
new Point(matchRegion.X + matchRegion.Width / 4, matchRegion.Y + matchRegion.Height * 3 / 4),
|
||||
new Point(matchRegion.X + matchRegion.Width * 3 / 4, matchRegion.Y + matchRegion.Height * 3 / 4)
|
||||
};
|
||||
|
||||
bool allWhite = samplePoints.All(pt =>
|
||||
{
|
||||
var v = captureRa.SrcMat.At<Vec3b>(pt.Y, pt.X);
|
||||
return IsWhite(v.Item2, v.Item1, v.Item0);
|
||||
});
|
||||
|
||||
return !allWhite && !IsInRevivePrompt(captureRa);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在任意可以关闭的UI界面(识别关闭按钮)
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -15,6 +15,7 @@ public class ElementAssets : BaseAssets<ElementAssets>
|
||||
public RecognitionObject BtnOnlineYes;
|
||||
public RecognitionObject BtnOnlineNo;
|
||||
public Lazy<RecognitionObject> BtnExitDoor;
|
||||
public RecognitionObject InDomainRo;
|
||||
|
||||
public RecognitionObject PaimonMenuRo;
|
||||
public RecognitionObject BlueTrackPoint;
|
||||
@@ -136,6 +137,16 @@ public class ElementAssets : BaseAssets<ElementAssets>
|
||||
TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_exit_door.png"),
|
||||
DrawOnWindow = false
|
||||
}.InitTemplate());
|
||||
|
||||
// 秘境退出图标
|
||||
InDomainRo = new RecognitionObject
|
||||
{
|
||||
Name = "InDomain",
|
||||
RecognitionType = RecognitionTypes.TemplateMatch,
|
||||
TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "in_domain.png"),
|
||||
RegionOfInterest = new Rect(0, 0, CaptureRect.Width / 4, CaptureRect.Height / 4),
|
||||
DrawOnWindow = false
|
||||
}.InitTemplate();
|
||||
|
||||
// 派蒙菜单
|
||||
// 此图38x40 小地图210x210 小地图左上角位置 24,-15
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.GetGridIcons;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using Fischless.WindowsInput;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Common.Job
|
||||
{
|
||||
internal class CountInventoryItem : ISoloTask<int>
|
||||
{
|
||||
public string Name => "背包数物品";
|
||||
|
||||
private readonly ILogger logger = App.GetLogger<CountInventoryItem>();
|
||||
private readonly InputSimulator input = Simulation.SendInput;
|
||||
private CancellationToken ct;
|
||||
private readonly GridScreenName gridScreenName;
|
||||
private readonly string foodName;
|
||||
|
||||
public CountInventoryItem(GridScreenName gridScreenName, string foodName)
|
||||
{
|
||||
this.gridScreenName = gridScreenName;
|
||||
this.foodName = foodName;
|
||||
}
|
||||
|
||||
public async Task<int> Start(CancellationToken ct)
|
||||
{
|
||||
this.ct = ct;
|
||||
|
||||
logger.LogInformation("打开背包并在{grid}寻找{name}……", this.gridScreenName, this.foodName);
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
await AutoArtifactSalvageTask.OpenInventory(this.gridScreenName, input, logger, this.ct);
|
||||
|
||||
using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary<string, float[]> prototypes);
|
||||
|
||||
using var ra = TaskControl.CaptureToRectArea();
|
||||
GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], logger, ct);
|
||||
int? count = null;
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
using Mat icon = itemRegion.SrcMat.GetGridIcon();
|
||||
var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes);
|
||||
string predName = result.Item1;
|
||||
if (predName == this.foodName)
|
||||
{
|
||||
string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle);
|
||||
if (int.TryParse(numStr, out int num))
|
||||
{
|
||||
count = num;
|
||||
}
|
||||
else
|
||||
{
|
||||
count = -2;
|
||||
logger.LogWarning("无法识别数量:{text}", numStr);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (count == null)
|
||||
{
|
||||
count = -1;
|
||||
logger.LogInformation("没有找到{name}", this.foodName);
|
||||
}
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
|
||||
return count.Value;
|
||||
}
|
||||
|
||||
async Task ISoloTask.Start(CancellationToken ct)
|
||||
{
|
||||
await Start(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,9 +102,16 @@ public class GoToCraftingBenchTask
|
||||
{
|
||||
// 图像下方就是脆弱树脂数量
|
||||
var countArea = ra.DeriveCrop(fragileResinCountRa.X, fragileResinCountRa.Y + fragileResinCountRa.Height,
|
||||
fragileResinCountRa.Width, fragileResinCountRa.Height/3);
|
||||
var count = OcrFactory.Paddle.Ocr(countArea.CacheGreyMat);
|
||||
fragileResinCount = StringUtils.TryParseInt(count);
|
||||
fragileResinCountRa.Width, fragileResinCountRa.Height);
|
||||
var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.SrcMat);
|
||||
// Logger.LogInformation("识别原粹树脂数量:{Count}", count);
|
||||
var match = System.Text.RegularExpressions.Regex.Match(count, @"(\d+)\s*[/17]\s*(4|40)");
|
||||
if (match.Success)
|
||||
{
|
||||
var numericPart = match.Groups[1].Value;
|
||||
fragileResinCount = StringUtils.TryParseInt(numericPart);
|
||||
Logger.LogInformation("提取到的原粹树脂数量:{fragileResinCount}", fragileResinCount);
|
||||
}
|
||||
}
|
||||
var condensedResinCountRa = ra.Find(ElementAssets.Instance.CondensedResinCount);
|
||||
if (!condensedResinCountRa.IsEmpty())
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.GameTask.GameLoading.Assets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BetterGenshinImpact.Genshin.Paths;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Win32;
|
||||
using System.Windows.Documents;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using Vanara.PInvoke;
|
||||
using BetterGenshinImpact.Core.Recognition;
|
||||
using Fischless.GameCapture;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.GameLoading;
|
||||
|
||||
@@ -37,6 +27,9 @@ public class GameLoadingTrigger : ITaskTrigger
|
||||
|
||||
public bool IsExclusive => false;
|
||||
|
||||
public bool IsBiliJudged = false;
|
||||
public bool IsBili = false;
|
||||
|
||||
public bool IsBackgroundRunning => true;
|
||||
|
||||
private readonly GameLoadingAssets _assets;
|
||||
@@ -244,43 +237,47 @@ public class GameLoadingTrigger : ITaskTrigger
|
||||
IsEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Bv.IsInMainUi(content.CaptureRectArea) || Bv.IsInAnyClosableUi(content.CaptureRectArea))
|
||||
// 成功进入游戏判断
|
||||
if (Bv.IsInMainUi(content.CaptureRectArea) || Bv.IsInAnyClosableUi(content.CaptureRectArea) || Bv.IsInDomain(content.CaptureRectArea))
|
||||
{
|
||||
_logger.LogInformation("已进入游戏");
|
||||
IsEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// B服判断逻辑
|
||||
bool isBili = false;
|
||||
try
|
||||
// B服判断
|
||||
if (!IsBiliJudged)
|
||||
{
|
||||
var exePath = _config.InstallPath;
|
||||
if (!string.IsNullOrEmpty(exePath))
|
||||
try
|
||||
{
|
||||
var configIni = Path.Combine(Path.GetDirectoryName(exePath)!, "config.ini");
|
||||
if (File.Exists(configIni))
|
||||
var exePath = _config.InstallPath;
|
||||
if (!string.IsNullOrEmpty(exePath))
|
||||
{
|
||||
var lines = File.ReadAllLines(configIni);
|
||||
foreach (var line in lines)
|
||||
var configIni = Path.Combine(Path.GetDirectoryName(exePath)!, "config.ini");
|
||||
if (File.Exists(configIni))
|
||||
{
|
||||
var kv = line.Trim();
|
||||
if (kv.StartsWith("channel=") && kv.EndsWith("14"))
|
||||
var lines = File.ReadAllLines(configIni);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
isBili = true;
|
||||
break;
|
||||
var kv = line.Trim();
|
||||
if (kv.StartsWith("channel=") && kv.EndsWith("14"))
|
||||
{
|
||||
IsBili = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskControl.Logger.LogWarning("B服判断异常: " + ex.Message);
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskControl.Logger.LogWarning("B服判断异常: " + ex.Message);
|
||||
}
|
||||
IsBiliJudged = true;
|
||||
}
|
||||
|
||||
// 官服流程:先识别并点击顶号或切号的后一次“进入游戏”弹窗按钮
|
||||
if (!isBili)
|
||||
if (!IsBili)
|
||||
{
|
||||
var extraEnterGameBtn = content.CaptureRectArea.Find(_assets.ChooseEnterGameRo);
|
||||
if (!extraEnterGameBtn.IsEmpty())
|
||||
@@ -290,7 +287,7 @@ public class GameLoadingTrigger : ITaskTrigger
|
||||
}
|
||||
}
|
||||
|
||||
// 官服流程:点击进入游戏按钮(作为外层包装)
|
||||
// 点击进入游戏按钮
|
||||
var ra = content.CaptureRectArea.Find(_assets.EnterGameRo);
|
||||
|
||||
if (!ra.IsEmpty())
|
||||
@@ -301,7 +298,7 @@ public class GameLoadingTrigger : ITaskTrigger
|
||||
}
|
||||
|
||||
// 只有在"进入游戏"按钮未出现时,才进行B服登录处理
|
||||
if (isBili && !biliLoginClicked)
|
||||
if (IsBili && !biliLoginClicked)
|
||||
{
|
||||
// B服流程:处理登录窗口
|
||||
var process = Process.GetProcessesByName("YuanShen").FirstOrDefault();
|
||||
@@ -311,89 +308,28 @@ public class GameLoadingTrigger : ITaskTrigger
|
||||
{
|
||||
if (windowType.Contains("协议"))
|
||||
{
|
||||
ImageRegion screen;
|
||||
try
|
||||
{
|
||||
screen = CaptureWindowToRectArea(loginWindow);
|
||||
}
|
||||
catch
|
||||
{
|
||||
screen = TaskControl.CaptureToRectArea();
|
||||
}
|
||||
|
||||
var ocrList = screen.FindMulti(RecognitionObject.OcrThis);
|
||||
var agreeRegion = ocrList.FirstOrDefault(r =>
|
||||
r.Text.Contains("同意") && !r.Text.Contains("不同意"));
|
||||
if (agreeRegion != null)
|
||||
{
|
||||
ClickRegionCenterBy1080(agreeRegion);
|
||||
// 记录协议窗口点击位置
|
||||
var (centerDesktopX, centerDesktopY) =
|
||||
agreeRegion.ConvertPositionToDesktopRegion(agreeRegion.Width / 2,
|
||||
agreeRegion.Height / 2);
|
||||
var captureRect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
|
||||
var inCaptureX = centerDesktopX - captureRect.X;
|
||||
var inCaptureY = centerDesktopY - captureRect.Y;
|
||||
var scale = TaskContext.Instance().SystemInfo.ScaleTo1080PRatio;
|
||||
lastAgreementClickPos = (inCaptureX / scale, inCaptureY / scale);
|
||||
SystemControl.FocusWindow(TaskContext.Instance().GameHandle);
|
||||
}
|
||||
|
||||
Thread.Sleep(2000);
|
||||
GameCaptureRegion.GameRegion1080PPosClick(1030, 615);
|
||||
}
|
||||
|
||||
if (windowType.Contains("登录"))
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
// 使用协议窗口坐标或默认坐标点击登录
|
||||
if (lastAgreementClickPos.HasValue)
|
||||
{
|
||||
GameCaptureRegion.GameRegion1080PPosClick(lastAgreementClickPos.Value.x1080,
|
||||
lastAgreementClickPos.Value.y1080);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameCaptureRegion.GameRegion1080PPosClick(960, 620);
|
||||
}
|
||||
|
||||
GameCaptureRegion.GameRegion1080PPosClick(960, 630);
|
||||
Thread.Sleep(2000);
|
||||
|
||||
// 检查窗口是否还存在
|
||||
var (remainingWindow, remainingType) = GetBiliLoginWindow(process);
|
||||
if (remainingWindow == IntPtr.Zero)
|
||||
{
|
||||
_logger.LogInformation("B服登录完成,准备进入游戏");
|
||||
// 添加延时确保窗口完全消失
|
||||
Thread.Sleep(2000);
|
||||
// 点击屏幕尝试找回焦点
|
||||
TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground();
|
||||
biliLoginClicked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在B服登录过程中,每次循环都检查是否出现"进入游戏"按钮
|
||||
ra = content.CaptureRectArea.Find(_assets.EnterGameRo);
|
||||
if (!ra.IsEmpty())
|
||||
{
|
||||
_logger.LogInformation("检测到进入游戏按钮,直接点击");
|
||||
TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground();
|
||||
biliLoginClicked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.Sleep(1000);
|
||||
|
||||
|
||||
// 检查是否成功登录
|
||||
if (biliLoginClicked)
|
||||
{
|
||||
_logger.LogInformation("B服登录完成,等待后尝试点击进入游戏");
|
||||
Thread.Sleep(5000);
|
||||
ClickEnterGameButton();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!isBili)
|
||||
{
|
||||
// 官服流程:直接点击进入游戏按钮
|
||||
ClickEnterGameButton();
|
||||
return;
|
||||
}
|
||||
|
||||
var wmRa = content.CaptureRectArea.Find(_assets.WelkinMoonRo);
|
||||
@@ -485,108 +421,4 @@ public class GameLoadingTrigger : ITaskTrigger
|
||||
|
||||
return (bHWnd, windowType);
|
||||
}
|
||||
|
||||
private void ClickEnterGameButton()
|
||||
{
|
||||
TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground();
|
||||
}
|
||||
|
||||
private ImageRegion CaptureWindowToRectArea(IntPtr hWnd)
|
||||
{
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentException("无效的窗口句柄", nameof(hWnd));
|
||||
}
|
||||
|
||||
// BitBlt 方式
|
||||
try
|
||||
{
|
||||
using var bitblt = GameCaptureFactory.Create(CaptureModes.BitBlt);
|
||||
bitblt.Start(hWnd, new Dictionary<string, object> { { "autoFixWin11BitBlt", true } });
|
||||
var img = GrabOneFrame(bitblt);
|
||||
if (img == null) throw new Exception("BitBlt 无帧");
|
||||
_logger.LogDebug("BitBlt 捕获窗口成功,尺寸 {W}x{H}", img.Width, img.Height);
|
||||
return BuildWindowClientRegion(hWnd, img);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning("BitBlt 捕获失败: {Msg}", e.Message);
|
||||
}
|
||||
|
||||
// DwmSharedSurface方式
|
||||
try
|
||||
{
|
||||
using var dwm = GameCaptureFactory.Create(CaptureModes.DwmGetDxSharedSurface);
|
||||
dwm.Start(hWnd);
|
||||
var img = GrabOneFrame(dwm);
|
||||
if (img == null) throw new Exception("Dwm 无帧");
|
||||
_logger.LogDebug("DwmSharedSurface 捕获窗口成功,尺寸 {W}x{H}", img.Width, img.Height);
|
||||
return BuildWindowClientRegion(hWnd, img);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogDebug("DwmSharedSurface 捕获失败,准备回退: {Msg}", e.Message);
|
||||
}
|
||||
|
||||
// 全部失败,抛出异常由调用方回退到游戏截图
|
||||
throw new Exception("针对句柄的窗口截图失败");
|
||||
}
|
||||
|
||||
private ImageRegion BuildWindowClientRegion(IntPtr hWnd, Mat img)
|
||||
{
|
||||
// 以窗口客户区的屏幕坐标为锚点,构造相对桌面的区域,使 Region.Click 映射到正确位置
|
||||
if (!User32.GetClientRect(hWnd, out var clientRect))
|
||||
{
|
||||
// 回退:用窗口矩形
|
||||
User32.GetWindowRect(hWnd, out var wr);
|
||||
return TaskContext.Instance().SystemInfo.DesktopRectArea.Derive(img, wr.left, wr.top);
|
||||
}
|
||||
|
||||
POINT pt = default;
|
||||
User32.ClientToScreen(hWnd, ref pt);
|
||||
return TaskContext.Instance().SystemInfo.DesktopRectArea.Derive(img, pt.X, pt.Y);
|
||||
}
|
||||
|
||||
private Mat? GrabOneFrame(IGameCapture capture, int retry = 8, int delayMs = 40)
|
||||
{
|
||||
for (int i = 0; i < retry; i++)
|
||||
{
|
||||
var img = capture.Capture();
|
||||
if (img != null)
|
||||
{
|
||||
return img;
|
||||
}
|
||||
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Region 中心点映射到 1080P 坐标系并点击。
|
||||
/// </summary>
|
||||
private void ClickRegionCenterBy1080(Region region)
|
||||
{
|
||||
// 计算区域中心的桌面坐标
|
||||
var (centerDesktopX, centerDesktopY) =
|
||||
region.ConvertPositionToDesktopRegion(region.Width / 2, region.Height / 2);
|
||||
|
||||
// 转换到游戏捕获区域坐标
|
||||
var captureRect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
|
||||
var inCaptureX = centerDesktopX - captureRect.X;
|
||||
var inCaptureY = centerDesktopY - captureRect.Y;
|
||||
if (inCaptureX < 0 || inCaptureY < 0)
|
||||
{
|
||||
// 不在游戏捕获区域内,直接回退为桌面点击
|
||||
DesktopRegion.DesktopRegionClick(centerDesktopX, centerDesktopY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 映射为 1080P 坐标
|
||||
var scale = TaskContext.Instance().SystemInfo.ScaleTo1080PRatio;
|
||||
var x1080 = inCaptureX / scale;
|
||||
var y1080 = inCaptureY / scale;
|
||||
GameCaptureRegion.GameRegion1080PPosClick(x1080, y1080);
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
@@ -48,6 +50,10 @@ public class GetGridIconsTask : ISoloTask
|
||||
{
|
||||
this.ct = ct;
|
||||
|
||||
int count = this.maxNumToGet ?? int.MaxValue;
|
||||
string directory = Path.Combine(AppContext.BaseDirectory, "log/gridIcons", $"{this.gridScreenName}{DateTime.Now:yyyyMMddHHmmss}");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
switch (this.gridScreenName)
|
||||
{
|
||||
case GridScreenName.Weapons:
|
||||
@@ -60,23 +66,23 @@ public class GetGridIconsTask : ISoloTask
|
||||
case GridScreenName.PreciousItems:
|
||||
case GridScreenName.Furnishings:
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
await AutoArtifactSalvageTask.OpenBag(this.gridScreenName, this.input, this.logger, this.ct);
|
||||
await AutoArtifactSalvageTask.OpenInventory(this.gridScreenName, this.input, this.logger, this.ct);
|
||||
break;
|
||||
case GridScreenName.ArtifactSetFilter:
|
||||
logger.LogInformation("{name}暂不支持自动打开,请提前手动打开界面", gridScreenName.GetDescription());
|
||||
await GetArtifactSetFilterGridIcons(count, directory);
|
||||
return;
|
||||
default:
|
||||
logger.LogInformation("{name}暂不支持自动打开,请提前手动打开界面", gridScreenName.GetDescription());
|
||||
break;
|
||||
}
|
||||
|
||||
using var ra0 = CaptureToRectArea();
|
||||
GridScreenParams gridParams = GridScreenParams.Templates[this.gridScreenName];
|
||||
Rect gridRoi = gridParams.GetRect(ra0);
|
||||
await GetInventoryGridIcons(count, directory);
|
||||
}
|
||||
|
||||
int count = this.maxNumToGet ?? int.MaxValue;
|
||||
|
||||
string directory = Path.Combine(AppContext.BaseDirectory, "log/gridIcons", DateTime.Now.ToString("yyyyMMddHHmmss"));
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
GridScreen gridScreen = new GridScreen(gridRoi, gridParams, this.logger, this.ct);
|
||||
private async Task GetInventoryGridIcons(int count, string directory)
|
||||
{
|
||||
GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], this.logger, this.ct);
|
||||
HashSet<string> fileNames = new HashSet<string>();
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
@@ -130,6 +136,106 @@ public class GetGridIconsTask : ISoloTask
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetArtifactSetFilterGridIcons(int count, string directory)
|
||||
{
|
||||
ArtifactSetFilterScreen gridScreen = new ArtifactSetFilterScreen(new GridParams(new Rect(40, 100, 1300, 852), 2, 3, 40, 40, 0.024), this.logger, this.ct);
|
||||
HashSet<string> fileNames = new HashSet<string>();
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
itemRegion.Click();
|
||||
await Delay(300, ct);
|
||||
|
||||
static bool tryGetFlower(out string flowerName)
|
||||
{
|
||||
using var ra1 = CaptureToRectArea();
|
||||
using ImageRegion nameRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.714), (int)(ra1.Width * 0.284), (int)(ra1.Width * 0.256), (int)(ra1.Width * 0.208)));
|
||||
var ocrResult = OcrFactory.Paddle.OcrResult(nameRegion.SrcMat);
|
||||
|
||||
var flowerWithGlyph = ocrResult.Regions.OrderBy(r => r.Rect.Center.Y).SkipWhile(r => !r.Text.Contains("套装包含")).Skip(1).FirstOrDefault();
|
||||
if (flowerWithGlyph == default)
|
||||
{
|
||||
nameRegion.Move();
|
||||
flowerName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
// 可能带有花形符号
|
||||
Rect flowerWithGlyphRect = flowerWithGlyph.Rect.BoundingRect();
|
||||
// 费解的是,原图识别没问题,但为了排除名称前的花形符号,无论裁切还是不裁切只是将符号涂白,都会把一些花名识别出旧体字
|
||||
// 花形符号往往还被识别为空格,导致无法用识别框位置来区分
|
||||
|
||||
// 截取没有符号的区域再识别一次
|
||||
Rect flowerWithoutGlyph = new Rect((int)(ra1.Width * 0.028), (int)(flowerWithGlyphRect.Y - flowerWithGlyphRect.Height * 0), (int)(ra1.Width * 0.228), (int)(flowerWithGlyphRect.Height * 1));
|
||||
Mat roi = nameRegion.SrcMat.SubMat(flowerWithoutGlyph);
|
||||
var whiteOcrResult = OcrFactory.Paddle.OcrResult(roi);
|
||||
flowerName = whiteOcrResult.Text;
|
||||
// 所以只好识别两次,Trim后根据字数取原截图OCR的结果……
|
||||
flowerName = flowerWithGlyph.Text.Trim().Substring(flowerWithGlyph.Text.Trim().Length - flowerName.Trim().Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!tryGetFlower(out string flowerName))
|
||||
{
|
||||
await TaskControl.Delay(100, this.ct);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
this.input.Mouse.VerticalScroll(-2);
|
||||
await TaskControl.Delay(40, this.ct);
|
||||
}
|
||||
await TaskControl.Delay(300, this.ct);
|
||||
if (!tryGetFlower(out flowerName))
|
||||
{
|
||||
throw new Exception("尝试获取生之花失败");
|
||||
//flowerName = $"识别失败{nameRegion.GetHashCode()}";
|
||||
}
|
||||
}
|
||||
|
||||
string fileName = flowerName;
|
||||
if (fileNames.Add(fileName))
|
||||
{
|
||||
string filePath = Path.Combine(directory, $"{fileName}.png");
|
||||
Thread saveThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
using Mat img125 = CropResizeArtifactSetFilterGridIcon(itemRegion);
|
||||
img125.ToBitmap().Save(fs, System.Drawing.Imaging.ImageFormat.Png);
|
||||
}
|
||||
logger.LogInformation("图片保存成功:{Text}", fileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "图片保存失败:{Text}", fileName);
|
||||
}
|
||||
});
|
||||
saveThread.IsBackground = true; // 设置为后台线程
|
||||
saveThread.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("重复的物品:{Text}", fileName);
|
||||
}
|
||||
|
||||
count--;
|
||||
if (count <= 0)
|
||||
{
|
||||
logger.LogInformation("检查次数已耗尽");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static Mat CropResizeArtifactSetFilterGridIcon(ImageRegion itemRegion, ISystemInfo? systemInfo = null)
|
||||
{
|
||||
double scale = (systemInfo ?? TaskContext.Instance().SystemInfo).AssetScale;
|
||||
double width = 60;
|
||||
double height = 60; // 宽高缩放似乎不一致,似乎在2.05:2.15之间,但不知道怎么测定
|
||||
Rect iconRect = new Rect((int)(itemRegion.Width / 2 - 237 * scale - width / 2), (int)(itemRegion.Height / 2 - height / 2), (int)width, (int)height);
|
||||
Mat crop = itemRegion.SrcMat.SubMat(iconRect);
|
||||
return crop.Resize(new Size(125, 125));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCR检测★字符很不稳定,因此用cv
|
||||
/// 非常简陋的色彩检测,请传入聚焦的图像,勿带入可能的干扰
|
||||
|
||||
@@ -51,7 +51,7 @@ public class GridIconsAccuracyTestTask : ISoloTask
|
||||
public static InferenceSession LoadModel(out Dictionary<string, float[]> prototypes)
|
||||
{
|
||||
#region 加载model
|
||||
var session = new InferenceSession(@".\GameTask\GetGridIcons\gridIcon.onnx"); // todo 所有数据炼好后放到onnx统一存放的位置去
|
||||
var session = new InferenceSession(@".\Assets\Model\Item\gridIcon.onnx");
|
||||
|
||||
var metadata = session.ModelMetadata;
|
||||
|
||||
@@ -62,7 +62,7 @@ public class GridIconsAccuracyTestTask : ISoloTask
|
||||
List<string> prefixList = System.Text.Json.JsonSerializer.Deserialize<List<string>>(prefixListJson) ?? throw new Exception(); // 不预测前缀
|
||||
#endregion
|
||||
#region 加载原型向量
|
||||
var allLines = File.ReadLines(@".\GameTask\GetGridIcons\训练集原型特征.csv").Skip(1); // 跳过首行列名
|
||||
var allLines = File.ReadLines(@".\Assets\Model\Item\items.csv").Skip(1); // 跳过首行列名
|
||||
prototypes = new Dictionary<string, float[]>();
|
||||
foreach (string line in allLines)
|
||||
{
|
||||
@@ -93,7 +93,7 @@ public class GridIconsAccuracyTestTask : ISoloTask
|
||||
case GridScreenName.PreciousItems:
|
||||
case GridScreenName.Furnishings:
|
||||
await new ReturnMainUiTask().Start(ct);
|
||||
await AutoArtifactSalvageTask.OpenBag(this.gridScreenName, this.input, this.logger, this.ct);
|
||||
await AutoArtifactSalvageTask.OpenInventory(this.gridScreenName, this.input, this.logger, this.ct);
|
||||
break;
|
||||
default:
|
||||
logger.LogInformation("{name}暂不支持自动打开,请提前手动打开界面", gridScreenName.GetDescription());
|
||||
@@ -102,25 +102,21 @@ public class GridIconsAccuracyTestTask : ISoloTask
|
||||
|
||||
using InferenceSession session = LoadModel(out Dictionary<string, float[]> prototypes);
|
||||
|
||||
using var ra0 = CaptureToRectArea();
|
||||
GridScreenParams gridParams = GridScreenParams.Templates[this.gridScreenName];
|
||||
Rect gridRoi = gridParams.GetRect(ra0);
|
||||
|
||||
int count = this.maxNumToTest ?? int.MaxValue;
|
||||
double total_acc = 0.0;
|
||||
double total_count = 0;
|
||||
|
||||
GridScreen gridScreen = new GridScreen(gridRoi, gridParams, this.logger, this.ct);
|
||||
GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], this.logger, this.ct);
|
||||
await foreach (ImageRegion itemRegion in gridScreen)
|
||||
{
|
||||
itemRegion.Click();
|
||||
Task task1 = Delay(300, ct);
|
||||
var sadf = task1.Status;
|
||||
|
||||
// 用模型推理得到的结果
|
||||
Task<(string, int)> task2 = Task.Run(() =>
|
||||
{
|
||||
return Infer(itemRegion.SrcMat, session, prototypes);
|
||||
using Mat icon = itemRegion.SrcMat.GetGridIcon();
|
||||
return Infer(icon, session, prototypes);
|
||||
}, ct);
|
||||
|
||||
await Task.WhenAll(task1, task2);
|
||||
@@ -158,11 +154,21 @@ public class GridIconsAccuracyTestTask : ISoloTask
|
||||
}
|
||||
}
|
||||
|
||||
// todo: 单元测试
|
||||
/// <summary>
|
||||
/// 请自行裁剪缩放到125*125尺寸
|
||||
/// </summary>
|
||||
/// <param name="mat"></param>
|
||||
/// <param name="session"></param>
|
||||
/// <param name="prototypes"></param>
|
||||
/// <returns>(预测名称, 预测星级)</returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static (string, int) Infer(Mat mat, InferenceSession session, Dictionary<string, float[]> prototypes)
|
||||
{
|
||||
using Mat resized = mat.Resize(new Size(125, 153));
|
||||
using Mat rgb = resized.CvtColor(ColorConversionCodes.BGR2RGB);
|
||||
if (mat.Size().Width != 125 || mat.Size().Height != 125)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(mat), "输入图像尺寸应为125*125");
|
||||
}
|
||||
using Mat rgb = mat.CvtColor(ColorConversionCodes.BGR2RGB);
|
||||
var tensor = new DenseTensor<float>(new[] { 1, 3, rgb.Height, rgb.Width }); // todo 放到BgiOnnxFactory那边去做个Mat->NamedOnnxValue的通用方法?
|
||||
for (int y = 0; y < rgb.Height; y++)
|
||||
{
|
||||
@@ -173,7 +179,7 @@ public class GridIconsAccuracyTestTask : ISoloTask
|
||||
tensor[0, 2, y, x] = rgb.At<Vec3b>(y, x)[2] / 255f;
|
||||
}
|
||||
}
|
||||
var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", tensor) };
|
||||
var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_image", tensor) };
|
||||
using var results = session.Run(inputs);
|
||||
float[] feature_matrix = results[0].AsEnumerable<float>().ToArray();
|
||||
string? pred_name = null;
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
标注名称,特征向量
|
||||
方块戏法,XpMYQGBCksAkDUjAlNVxwMnysECojt0+9ZluwJ7Ij8AYE5xAYquCwDWfB8GCpbi/XSGRwMH6T8AuQQFBf6APwNEFiED4ipVAUxQLQYjiocCwv4xAmhUoQH5yJ79LQPS/LdEVQa6MlsDeL81AFhihQGfS8z8Si1g+VTnCwEPgGsBisMZABH6NwE9Uj0Cb0gNBRdaUwGjldMBTvBRAFt5QQGhV3sDa1RXBzIAQvg/X0j9GAh/AT+2iwM3bvT8kRklAm+QOwRSFaD9T+K8/H0aFQKNGT0B7ENpAFEJMP9WAlEAEsEbA+cQDwNzimr1OkMNAo0WlQJTfBD6G+nRA6sNrwA==
|
||||
绝对不是下酒菜,FKe5wH/MFMBcTJo9sNj2v+qiwkB8rK1AkGRYPBYI/r+H2hNAKP37v199v8AibxHACp4HwPIAjj+04TVBcG2ewPTAB0CRfkJAS9LdQGUYSD8gv7+8Cpq5QFmMe0CaIy6+ZApCQfoPCcDRCjFA7b0dQLyMBUH14ls/ZENrwOtpSb8iRpBAxeIgwAPOUED4661A8NgDwBZcecCsQRS/pCkfPYKGCUBD/xPACEcdQCtAkEAVVdM+050VQPRYtL8kEErAW206wcVWikAhyUFAcVHjQCuIi0BXfr5Av/qYPPAfiT02avXAD7wxwSbHrsANOINAz68gQDVpiEAlm0DA6J8mvw==
|
||||
黄金乐曲的变奏,DzorP+aKxsCKTQDAQnjpP3603z9ikVPANFFmwBF9uMCDVKRAAA3dwLNPDcHoWtrAZnWqwB74dcDgm61A6qsOwd0OtMCdnDxARNUSQRSz0cDcmKtA+Vo0QK6LXUADMC8/f3iYQRi3ycDkZfZAd9CYQEZs5T/RfI5AXqi8wAPZoMDHgsZAVDK2wDhEd0D8UwZBx1KawHqN0sBnlIdAsrS/P1g650A0mmTBVJ2EwYiFA0Ecvxo/uUaGwDjoob9WB6m/0rs0wfzuAcDOv71AmgQHQbr5XkFTpjtButdPQGDrqj/ObSjB3ARswQhsn8A+aOdAnGWwQDqNJkB4GexA2Dl8wA==
|
||||
黄金飞鸟的落羽,8DumPB1Dp8CSdTvAn9E9QJApAUBY+1s/KmdRwIvjlsDoVaxA7F+ZwIYQDsHTovq/aXyTwADPZsDLrLdA52WfwIbSXMD4oHJAWNoOQTJymMAKAIpAWC46QEsMFkDgtZq/0ts9QS39ksCTAuFADpqQQFg8gD9doKA/iLG2wO9xoMDLPspAHBmzwKAee0AeVwRB6wWNwCxXvb82dZtAykwrQCRACr+vqTDBNvNIwa+/iEDNpAHAZLvowC+Vdz988spAfv75wH3s/j9BOwJAoEWUQMjvy0CoNpFArHCOwD3S8z/UiIfA7J3YwFQcEsCJNMxAhPqcQIYCQcB3OoJAUKN6wA==
|
||||
枫达,pC2IwFdWkr97ffM/ZdvAP218/T/CbaBAOx+EP+/ZTr9sKOE/1W2iv+OlwcDBrA8/8pViwLw6zj122/w+g/68wJ5dZ7/XFhBAyMrVQOZeK0D6iba/tyYMP0Bc8T/qMSC/ULE0QZdXn7/c6SlAoSj1P1dyy0BFLV8/uR10wJvVs0CYOXlAkS4wwGNrlj8cAKtASrgMwFkblr8m3FvAKP/hP1rXNcB+UtU+j3LIwF6csT5EGRq/SG6RwG5tF75AlAk/HywuwSwgNT/6iPA/VPugQDVoX7/4zxVBE4hQv36fHj8jk3PAjq+nwGzGAsBFl05A2dX6P78tJEBQxE/AwuQXwA==
|
||||
常胜传说,4EacwK2s9r/srus/k8Ssvzc5iD/IzmdAlsQmP2LcAMD2S6U/L1YiwH2htcCvPWDAnHxIwIRiUD+6jVJAixUDwUN1EMBza7o/hJnJQFHjMT/Wg7E+nG0yQGmVXEA6Pi2/2J6QQdHG8L/wCCxA5XzOP8Bi/0Dpg2hAh3VdwDCmXcCRvHtAv0ANwEjYcj9CKaBAKHkCwHSJ0MAh7c++IOtXv8b+C0BluMzAQLyUwGD3X0CJd98/kPexwBoq/7/wr128D+BZwc+o8D/m6aBAbVH3QFYvbEAU1lhBbjQxv/IRFEB+sw7BQos0wShEf8CTMm1A4CEEQMENIkCxLWY/zHBRvw==
|
||||
乾坤摩拉肉,TFJ/wIOPKsCqSKE/bjrdP0kAzb6OYYJAJIMKPYm7U8A7MxtAM95owO6DzcBi2izAgA0QwN66Bj4v0HhAPrjSwA/n1sAvSY0/XsvaQL0oxb7O7Ys++B1TQCm/iUBeZCS/YJ11QQeiAsCboTdAe+D9P0cGtEBeFNU/S2ZmwCuHrz+GZpFAVExlwA5IkD+eKrNApQUvwGoVXcAzIOW/UM3SPkYwpb+g4PnAzMJ2wJKClkAeODE/fOW+P6LTEsBId1k/zI8uwWZkoT9fTHVAG1MZQTAL0EBiqhFBIGRovjS0+r66J/7AMllmwdvvocC57IlABBoZQAYdwr7y7am+L2Kcvw==
|
||||
魔女的炎之花,qPIjwNTZhMAA5Ly/2g1MPywenUAoZfU/kyBBwLvsLsBsk5BA7W5QwLZUEsEFRe0965e0wIY+asAYceE/u2XCwIpyY7+odqpAhtURQfExmL+QgUJAq7zLP7y2Ij+nawS9e60uQR3Me8CMMs9A83mTQEBEbUCdJaw+L2W6wPh7/r7AOdZASnyKwA6zhkAvtgZBvlKFwDQSBcAu3F1AvY9mQLpU879x5mjAGr//wDTUcz7w/TTAYyIFwW7gKkB6dohAdcEmwfp0GkDSx4s/A8zXP5hRAz/II15A1u+RwCbrIEA489q/KCJVwPTZ479j67tAN42fQN7Cirz2woC+y6eNwA==
|
||||
蒜香面包棍,wNUNwFM+mL9V4gBAuUCSP+zBWkDySUBAlg8cPx94Zb+Yn50/50kFwG99psDU7dO/2Ms5wLFY974XfRdADubfwJ1gWsBhNaQ/AUO6QKfsrD+CCJq+zwLLP4NKJ0D2+RY+thV2QRcI4b89KUdAsECuP0PV2ECGGi1Ag2tgwJF8v7/1UVxA5MX7v/cBDD+Zyp1AeEgHwCIgycAg8z7AoCRYP62Wkz/VKWjAgPwJwW7QB0CE/T8/oS0HwchJZr8YLkI/aWs9wfAPVj+y0WRAAiuwQLtVUUAcND1BXhGWv08M5D93qtHARk6ewH5GMsC3S0JAYVnwP621AUAVl8c/6Pnnvw==
|
||||
双果清露,tsvRv+xdlMD2hhc+gCz/Pzc/40BmHBtAju4EwDsjfcCkRWVAcNaPwIgT1sDYK7zAqwO5wIaeqb839yJBQsYLwe456L8WmkFAg3fnQBCTYMCOCFBA05XtQN7VaECGMn6/srWXQe5GocDlQ6pAE76HQFq60kDM94JAyjmbwN56k79mupxASeYIwBZW+z/mo9JAoKx2wDKQDMEoxhNAcRIPP6ZxIUEodfDAPPqXwQY+8UAaR70/2KyWwK5a6r+wee8/A+ZEwQraEL+VxqNA1+f8QOBwLUEcBRRB0oYlQAOzjUBw6RDBNqY0wchdwcD0krdA4rKOQCO3AkBs3U9ALozYvw==
|
||||
磐陀裂生之花,84DgwA0QPsCcSfE9APuAwPaHUUDW2gFAMU+Vvzqz5L+ijUNA8p0MwNQYCcEYH/m/ywOkwL/p979m6oa9gL0mwYiYlr/eFYRAkbcKQS/SXz9AGqU/83PVP49pA0C9qpE/hyciQUgiUcC0waBAtNp0QIkcBEHMH0hAGBGawEqtQb/aS7dA69s3wEyMV0DMsPBAm/dbwEaLKcCRiANATiukP/4LEsBjwbnAgdEbwZf6B0AAUES/tU+jwNqmkT++5p69rQokwdxmNkAIE1ZAETqkQJDeLEAM7No/dTGTPzwtBkAGh0XAoarUwFtwkcB7FqhA0U5wQGMcl0DbTC5ASw5DwA==
|
||||
炸肉排三明治,w4/Mv29cxb/7uoo/vdgnvg/uuUCEm/8/wkdFPx5+tb8QYFY/a3UQwPz2YsDDCJzApV3ov8DrgDuOkBRBVT2owCVxh79AN7u7SpiIQOMqBL/byhQ/9EWyQNj1hkAtHIG+C9GFQe6s3b9rsCFA5kosP5vZAUFf9nZA4Gjfv5KiNsEk9Pc/L2STv2bjDz8YE0VA/k4wv88QDcFQcJk/hTyTvyq2sEAjJRTBlJ0swIkfpEBIA/Q/ZRy+wOaY4780Bbi/rQ49wUiciUC19JFAK77QQA/OGkE7kShBUHMMwFLDVkCYZwbBM6sOwTGpZcCnsBpAHgB4P33BAEBtZKdAMkglvg==
|
||||
教官的胸花,Jkxsv3IYaMATN4+/egkMQAM29T+qAPo/B1zZvyWibMAdzY9ACK+FwP5XB8E0GlDAkAebwMSnQ8CkJcxALJ8FwXtk4L+RVFBAXEEQQS/XY8CbeSxA7GRsQCZ5vz8fLOK/4q5kQRjhZMDkv7tAcOeAQN7/bEBIAT5AHw2twGJ/pcBOFL9AnjOOwDGwPkB3AgBB1KCAwND2csBhhj1AVDobQPLmcD8uOj/B3XBjwVHw40Ck6pm/eNHuv07LT77UcUpAZR0xwfjUgr8FvGJAQ4XgQFPDt0B72etAxuipQPnthkBWsb7AQIbswKb5SMDIcsNA6uyDQCumX7+Dq7VAJeJnwA==
|
||||
守护束带,uOb7QDJNX8HukdfATEZHvn6MYcBwcD/B5yhAwWRmNsHmFz5BiutMwXvYj8FWkWpAPDdAwYzrTMGABxvAk1udwI0kwsA3BytBJaeEQSQaFsHG5FlBHBziwHoGh8C0aiRAMBufQXJrPsEkOHFBeHk3QYRGkcBxA6XASVpXwUxaXT86EWNBpsI0wZyPE0FoLohBkuwiwfV3bz8bCVpAjMMaQWSkH8GLvPbAhKI9QB710MC0YuHAU4X6wHfkGkH4Kw3ApvRkwcr3oj8gHFDABnXMv2JlQEEl0ypBvtpcwQYVlcBuEzPARoIuwCoRuD8Cj1hB6lNKQXvFtz80F7hAS2E/wQ==
|
||||
沉重号角,auRPwOuUs78J/em+xC/XvqsLa0Ci2R5A0mm6PRerO7/mWPs/KbmDv6B2zMD/JMi+EBQJwGRw0b8+Z8E//n2VwNYYGz/HuTZAhFzYQDdpgD+QumG9BEFpvjRulz8gs4k6l6rRQHzfxr+FL3FA1UYfQKRynEDGB58/ELZ4wENlh8APt4JAX1g9wErKNkCeT7xArFgPwBJ14L/czUw/J3f8PyhS7L9fZYLA/BqcwNk0MkBOv8S/KuXKwPi+Mz9EnoA/OUvlwCsBJ0CuHsY/DmsAQGadAcBRSJdA1zSlv/QoH0C6sA7AJoT6v/wJ+r2cSmxAKkYYQAzLOEAwh+I/SpAzwA==
|
||||
超级至尊披萨,qPQ4v/4iIcAJYEY/IAn7vtgeQkDqFjZAmKWNv7KWK8BNUw1APs8wwGiEqsBODwXAMDCFwBTVvb64ycFARsmcwI5q3D9UFdI/eie3QKY8o78Wcs8/Xup6QPF4xT+XdOO/DXluQTZjPcDIEFRALEIZQIxBxUC2Ng9AFg9xwJxSJ8CWEHlAmev4vyjyHz8g0Z5A0T4ywD/Qr8Aroq0+iM/6Pqz6YcCGsvXAYDanv5H2Cz1+3bI+sxTfwGgZib7HIClAEy1BwfpDID+OPENAQW+6QL52uUAoJUVBVFnCv7R9mECsPr3AFoqYwAVtF8CDlWxAyA4pQBsJ7r+YKiZAip2kvw==
|
||||
烤肉排,7FqywGu9NsBIwX9AbvXevxmCnMB0VTBA9uJtv5uDB8D0Is0/pvcswP6QrcB1zXA/N/mtwNJ5KT9pe7bAdHbTwFmL4MCaCVk/kBepQG2TE0AgxoQ85gFmv1By4T8OeFm/1r6KQcl39b/YvKY/klfKP7jf0UBNoAY/uGhiwMoRMkHDaYRAsowOwIgycb/zLYxAmuFCwEgi3r428FXAX3o8v7RpgsECTdHA+t7gQK1DuMAS+ZE/XsUIQEyMnr3mLQxAt/BbwSgUYT4O2zJA5AsPQTSj8EByOi1BlOmtwOBZZ771l43A8Pk1wbPBT8Bk80dASMAXQBIxQ8BE6VrA+GQOPQ==
|
||||
鸟蛋烧,1DaFwAnQJr88JQFAb+AEQMZhsUDOMs1A9FsHQGpFl7/S5ZU/noHXv2zmocBUi1HAgTnvv9nJvr4aATxBeBELwR3R/sBjJfk+YcXMQNZunLwEEq6/JmbpQL+YZUBWQkq/kOl/QTjyBb5D2RZAzyedP3Rx4UAm7Z0/yMLkvxHRKj4P5jpAYM0IwDgMFT+qynhA6lFOv2m32cA6htC/HtKFPxD+oT5OewjB4Dguwd0SwEABX+E+a2nGvvfMA8ALjcpAd5k1wd13O0CJcFBADpsfQbxw2kCYXrBAXtcUPzfZpj8eob/AlUE2wQhGzsC8xBBADi0FP2MYHsBUDx5AnMKBvw==
|
||||
异国之盏,ltWSQDJZ08B91LXAMLAEQK6YsECvpf+/XvWXwD2Qz8Cpp+FADDbVwNI5JcGaKWTAVY5/wChXs8CIOjlBbNtYwAmNGj8QeadAGN8rQaz8CsFKb81AuFeIQOKT6D9OLnO/5PUxQU6ptsDRmBJB5ZW4QDDg7r+RGdg+vwTUwPCCKcFOv+lAjpHnwPqPykBCkR5BLpybwFcWQsBomOBAP8iBQDMYVkBSVmrBjAz2wLhJ/UBJ9VvAdtNXwIadsj8G2tc/7y8JwYZsT0ADd9s/orqGQAK36EAbe4BA4kgcP6fvIUDItYnAdlTSwOVsgr/NuvpAhmXBQJh68r00qfZAMYatwA==
|
||||
教官的茶杯,gbqLv6+ct8AUogLAwCxjQLJ3X0Aw2R6++clwwGD5mcCnM6lAc8i2wDy/EcHFMIXA6Uu5wJJmgcDWNshAePoDwTUBz8AD1IBAmK8WQXgqncDHp5VAsad3QPXEIEDO5EA+fH2IQZb2q8DSheNAHy+cQKTSPkAUajpAYr68wF5nJsDB3NJASKyewDTHd0DEoAhBITabwN9HocC3d2ZAG+4nQKRcXEAVYzPBkCp/wTQq3UBUIGe/gllYwDi5Er2payhAUysywbyE/L+zfXxAF/XDQA/PGUFOc+9An8sBQDAW5z9j/ePAlRM4wXobnsBJjuJAh8SxQC8QR72LJ5ZAV1SAwA==
|
||||
历经风雪的思念,jvCbQSsNksGyqlLByBvsPhrObb9+er7BQzSDwQMXicFQG4RBqYaWwcipsMHKqFY9rn47wUJRmsGW6d5AthkUwCqNUMA8Sk5B0XWsQblqp8E3VJ1BqoTzwC1JscDEHINAZw/HQTLbfcG7nrBB4RpWQeAUYsEm8AHBRml2wSRP5cBPE4dBLtKNwQLfckFvX6lBDNkywbcM/z/xNyhBBaNcQVviIL96Lo7BQGo4P3+KGj/h7zLBB19OwN3pPEFGdO3AOzGAwfxWsj8wcIPAKy2fP93FtEECHBhB/bK5wPMf0MA7abDAyMkmwXJwE0BxO4hBHwZ7QaYglECr5FhB0OiIwQ==
|
||||
日落果,JfXavz7THMBrNaa/fgYOwE0TcUA6giBATH/5vkkhZsAHuGNAkVJIwPlI7MDLzEPAqaH6v9Xjqb9APAZBCVOSwI/OFUBb4yRAqh0JQXPKMcBmPIY/QCppQAjjxj+maiQ+e24iQTN9LsCbKYpAJM0+QOiqiEBPbdk/w42RwDdhQcAWZ6RAiMSIwPmGeUD/l9xAdPJKwMo/icDFMpu/yjUGQDyf7TyYJxHB7bQtwEu2/UB3mJy/AseFQDamdL98VXzAB6EewftkE7/pNl9AWF8MQabPaUChaJRAB9YuQQJqGUCaQt3APv4XwcPiWMD9H6lArq9OQJXIl0CFHRtA5vofwA==
|
||||
守护之花,hDAfQaDTAcGiTgDBPg7Zv9VsJEGE2czA/HfUwOmv7sAaHAdBohgFwehQQ8HSFILAdZKGwMjyDcEy83ZBTSXWv3ibKj8rvstAJT1TQQJhOsFS9BJBax9AQOBcmr9ND50/8NJfQUHK0cBQgTVBYL3hQMJBsMCLkQ3AGBL4wBfXJcECUwJBr/sQwT5PDkHkwTpBFwKawJ+X4MAkCKlA/JnWQEQfhj8sDnjBqGFpP+Qq1UAdbqzASp+lwNkaYUBq/Na/xWMrwaBHhUC2quk+MbEwQIO4G0GkEwFB8nekv7Qfiz8GVJjAvmaVwEJtpD8KcwxBSoXsQJ5y8T+aiyJBwKABwQ==
|
||||
流放者怀表,Zqy9QCMgBcGZyMjANOjTvljEBT8qLsTAqpfjwEDg9MBsAwNBUer6wFzSRcGirHg+dBy0wNJW9MBWWLRAIj4LwIbazLymtNlACX9FQSa4C8HYrQNBzXWwvx72jL/Cgi8/SnRBQUrr58AozSVBP0jQQLG5gsAHngPAA1gIwbhPq79EihJBSswOwZBa8kBQWz5BltLFwE60xD/Nf3VA5uPIQNIG78Bz1C/BS3kCQPkpdb8ZjqnAw/adwIfjjkBswTg/digZwRgFrT7vKKC+478bQHRiBkFlWgpBaYihwPeW1b/ZSmXAc7yUwEERkj7FGBBBG4D2QGcyTL9ktppA/UX8wA==
|
||||
混沌装置,1+SxwAckZr881i8/ceyQwESKFUCyWIRACqEZPzxaF75UeK8/EOQBvleryMCpU98+pt0awEhWID+LUg8/nJ+ewIU6l0AmijhA2TfVQIXWLkB+FIe/ITgqv8TuxT9ZLA4920zHQB7plb+uICdAX2sIQEp42UD8WII/RxyBwDajmT9fRnxAedIbwAC9JEB+obRAaAgMwIaPMb9MiR3AMqSHP/TJq8C6qCG/0w5DQE5Mpj7k2T2/lMC3wCQKjj30Aue/JoMOwRI0OkCTtgRARX94QO3el8DsbfJAzNYVvxs1+T9pgmjAJtXwv7Q8hr/3ylJAcgUPQGdBtUBjbyTAhOcDwA==
|
||||
祝圣油膏,oKVHPv7T0sCNtWjAQX8EQAfwzr721EHAx2+jwN6sucAJcMxALiO9wKarKMEuQG++ONnCwCIksMC3Qis/WQqMwOebXcDa4alASV8kQaZAmsAiKbFAOUwvv4CQIz8cjTc/YuMpQQa8y8B/YQRBhhiwQLYrET6q9Gk9W/vfwJpWBECWVvlA7hDRwMhkpUDscCBBReq2wBn4qD+allxAjCqUQCqA1MBI0AbBfBsjwVJ1nT6Krl/AUu+YwLZWQUA4az5ArkbrwAke0L9dz0M/ktVJQEJ940Aw1GlAD3ElwGCVHb3yZU7AumPlwKErE8CuCPZATFHQQHuyX79t0TlAh426wA==
|
||||
炸萝卜丸子,q4dUwHC/ir8wNtQ+fMQ2wIJV2UBuxI5Arr9hPzjDYL8ed+I/9HCNv8ymqMAQvSbAw8wEwBsayT7e1RRBuACswIeJhEBlPAtALwzDQHbLHj867lO+WMZ+QPxVMUC4wDu/09QdQasou7/DIjlAaMsIQBnCAEHhwBhAlJdMwBeFDcE7sllAw8S8v7hLCkDGl59AjO7Nv6/RssBomN09NsGFPfiaM0AkyqrARhwVwMOE1UAL69k+ytYawEB7rL8wnR/AVnYdwRXOnUCIHVNAI3a6QH8YxT8nWKtAQAcxQEhehkAzybfAAU6FwHD4EMC/Z11AgHjzP+6Gn0CAyjFAerx3vw==
|
||||
故人之心,BDpaQQZlWMHPMETBGP5aPpEtRUFUCn/BqpA1wUbgL8FN7zZBu9hIwUpNgcE2SaDAv9O+wDHCXMFSXnZB/d6Rv+7oAMFNghFB3qqAQS7ngsFQ82xBL421P3BHCL+EzatA+rqQQSqlOsEgnYVBa5scQXTTFcGSWHnAD8YmwXlx5MBonC1BupRFwUxZSUHg43hB9jz0wA32ysBxDvBABF8hQRSWUEDZG1fB6LM4wBWTfUCSB93AElbswNAav0AA3C3ApAkJwYKrKEByLJ2/kvzCv+u1lEHyLP9A5jAuwS8fl8D8NMDA7LA3wUdKhr5WnkRBIQI4QVwNWUCWET9BmkQ7wQ==
|
||||
奇迹之杯,/+keQE8m2sD6FnvAS1HgPzZZ3z9O1GrAx5SRwMAHzsCkRMZAcMHTwAxyGMERlDLAAmWMwPIKmcAiLtdATnRiwKzWRr4cLoNAKqcXQVILzcCb+cFAmXWhP5jFEUAN+hI/44lIQRxA0sAo3ANBmL+oQKLVrT44wpQ/LGHTwPRZC8HUAt5AWCbOwO4On0CR7xZB/N2jwJgsx78Sv45APOxHQGrDQUA0rDTBaOTuwBixxUAUNArAWCtrwGZFpj96mxvA5hb+wGa4rD9qxRRAjoCLQGJlK0GALsFA6ZcLvz5fQj/0DOPA6g0HwdMsB8CZZPtAwyjJQKXOA0CFF85Aov6fwA==
|
||||
苹果,BIuQvzaGEsB0kvS/+m5cwDrgTj9Shx9Azauavzf2KsBbgFpA7sHgv+MG7sA96hhAdE4FwPAyub++LyhAmmmyv3HibkCxi2xAkaABQdsbSb5L9HE/Tbusv8Oylb+tcvm+3eGnQAvF5r9D429AVzkxQEpWNECAzAHA+hOYwMotUz/NE7FAEiSOwEXggEBn49ZACyo8wDsTpj81cz3AQxlNQIpTN8GdpUbAC3YRQWbF0L/CBk7AFlniv16S+z9Uusc/LVzfwLMJCkCPXMK+Azl3QJJfScCT/a1AbxzPv5pDRz+v+fG/BAZBv/XfvD7evIhA8JtJQAQ7YT5LwUK/obxhwA==
|
||||
学士的时钟,A0+EPzMdtMA4sYDABPgTQC4/70Ax+ay/gYRhwCcKlMAhcLZAuUKswDyVGcHiaoHAo4WRwJvipsBI9gxBDPa7wGZwh8DL7pZAClwhQeyvqsDOS5xARZ50QE2EC0DqZSE/phRNQVihosBPzgRBO7arQAOzHT8FTZc/bC+8wGZk7MDFZc1AM3fDwKK6skAynBBBRIOIwPbWsMA4lKNAvP12QMVdyEAqLhXBiCCJwerHAEH8MB7AU/i9wMx3kj/UbxBArZEAwU68M0CmoAdA77VyQCgY4UD60jdAEuhqv8aOmT+Q+YvAhKgKwRZcLMAAVtxAC8KzQC32yT/PYNJAtImewA==
|
||||
鎏金殿堂,3NyfwIdJi7+v8rU/OLBzP6xlrUCheKFAlGX/PnFXGb8o+kc/jHSbv4ZmfcDwHlXAfL8UwI5RpD8ujJRAlfy/wLqVnjwbN4Y/caWNQCHc6D/ZMAG/sMmeQIMji0ASsD6/noBLQcdMz7+O8gJAquZjP9vEEkEbhmZAzbwYwLjxg8C3uSBA9Ih4v/thED83ymRAJ6CVv9Bn1MBEkDA/PBt+v+Kg1z/HylfAnyjhwCi5wD9a5d8/50MYwRpD3L+2GkhAWb8fwXgXY0Bp9IJAWXG8QKU7W0DmexJBsWWSwMLyU0D3U8DAnnPdwJWBeMDa5hRAH9CtP6z2Cr42eZ++UtFIvg==
|
||||
提瓦特焦蛋,JqE9wHwNU8DO2RU+n8kjPiaVi0B+enJAt8Zdvv/SSMBZflNAkSWDwKMQBsEWtDbAHgwewEcd6b8FnyxB4HAEwWd3qcCzWThAQPgPQdP8Hr+quYo/kIiGQOv6fEALwrk+w/R3QUcGCcDf4ZRAXIdlQDSS00B1SUI/Dw2XwHJ7BsFL+L1Aqnx4wJwgQEDQSfhAINRCwHXIvMCI9pC/8ujQP+gZsUAEvADB1WJgwINvGkFQckS/ciGQPwxxm7/XxAjABtgvwcAnxEBSBTtAS8UBQcnuzECvcNRAalEPP9jiMr6l/vHAg6A4wYLBncCoobRAG4FUQMp1hkDl5n1AyiIlwA==
|
||||
冒险家蛋堡,dStFwMCqQMA20CFA4Xfnv9aGFkC6ACBAdxDDv57nDMAs+Oo/vyhKwD2hpMAM1j3AnHejwBErrr5kQJc/xNnawNre6zzkbtE/HIOnQHPTH76cJ/M/vqpnQN/tQUDwPb2/9vWiQbqLV8Dfg0hAKs4OQFn2B0HhyYBAURN6wAyJx8AgeH5AJxeIv84Blb3q4qBAsOA2wJzS8MCyMNg/TRaEv5i5ZT+cjvfAcMktPXsmXz+FUdc/cRbCwPxZE7/yCVO/NOx4wWxDOUBEoJlAMKHiQLoiBEHAa01Bf/5FwLClmEDkUObAVswQwb+qOsCw8oJApMtAQFUGuD9cKsU/MGSuPA==
|
||||
祝圣交响乐,4jJxQB0FvMCz2WvA1M/Kv9xRg0CO4ce/3LmZwMkgr8CLkLRAHSiowKnFGMG3Fv6+FciawHWVjMDaMs5Ajvjqv4ghAkB2iZdAU3QXQRMopMDq6q9AX+nUP2hMN7+GTmq/oVozQTzyqsDRz9xAzEGkQDp1vD9uwI+/B5vUwCz5ML/YAd5AcSGpwEd1jEBQfRRBqrOawBJNI8BEL+U/as+CQMZzH8EgCBLBZLuuQHit9r8MCVbAGf2qwPBCMEB2QhxAh68SwYJLpT9luMA96Oc2QMNutUCqhBlBc1upwAieU0DpGjrAqm4swEL9Mj6k1tpAIF+5QPVI7L+mg3BA3+KbwA==
|
||||
祭火礼冠,zhVpPzqOucCNfQDAtFoJP4cmTcDKwuS/3XOPwC40qsDnz75AcF64wPr8JMFCLvs/OJzLwHiymMA4Hy7A4C+DwKZcOMDYVJdAxsIgQa44UcBFtJRARPRXwEwIh7+KknW+zjIpQdYBo8DRKd1AzZaoQLKcRb9a2Ha//A7pwIsXo0AER/FAKsjbwIRngkBNGxlBD761wFmp5z+SfBM/eHCUQHF5VcGQGvjApoxXPXI5VMCK7mfAjtWZwKY8VUD+rpY/H+AZwTiuCMDd3Qg/TpRdQGnXXkDsGBxBT+JgwKPpTL/MLhrAiBFawLhzSr3gbuBAIcvBQKsbA8Cw7pQ/H5a9wA==
|
||||
摩拉急速来,6nYhv1apTcDeiw+/IFKZu6n/tUDLxj9Aa3fIv/1iUsBORVFArwhVwPrY2cDXaj/A+mtZwKJWab86sA9BWMmewGwdOUAS5jpAAufrQK8REMAYlwVAsJuiQGkzCkA5Cb2/ovxnQTDFWcDCdpBAyJZGQMG2n0DWswRAlX+ZwCcsbMBkDp9AX+NEwPjUIUBRktVA6/NMwBD9usDQr+E/cRitP5FSRkAKa8LAYp/RwEi6cUCORQC/r5iXwDQABb9gwTU/D2BEwVBYyz+KrFFAuxPJQBDuiUBW/CVBN56hP3mIh0CCw9/AowziwKoHMsCU159AInlrQDkRbT8wc8k/NksawA==
|
||||
蒙德烤鱼,XIePwLT1Q7wu8jlAXxlAwOGgeUAclKdARN8HQGYEk74o7Es//BpNvw58s8B3LIu+Ra/2vzkB0r7mGOZAHNUCwe9Ik8AuBZ4/6fXgQIZUA0DmS9W/oGY/QGQu2j9CPyc/6piFQXYiED/JYPw/AikjP1DW/EAP7aW+a3j+v5hklD+TUVRAln3yv/YUGD+R6oxA0M7XvbgVs8AsFnHAfKthP5TN9cCxFfnAivUWQPbyuz4/06K+XJ+NvyT3qj2AZwdARbxWwXZqrkBvSBJAFOASQR/zr0AJ+9dAQXkLwNNH3T+Nl5TAgrr1wH9miMDC6+c/k7qZPtgDnb3lD/0//G2kvw==
|
||||
「美梦」,OTEuvy/XhcDHHPk+VLIoQIIciUBcnwS/h1ucv2FUWMCuDitAwW+fwLppy8AbN67ApmxiwKqwB8BP7OxAhsUCwcgB5cBfrJI/bcXbQNXJKMCghDJAJDWAQBcNh0AyhLE/ILKpQZ4qgcDVMJxACks2QH+RyUDKWoFAYbyLwJoKrsDu/YNA/Xs5wFMpwT8YvshAOJQ9wJoJEsH5Gde+xGEtP3p5DkGi1xjBBRxGwZ518EAtpcM/gRuSwLqY7L/glzzADVlNwRBmpr63LqpAMfTmQHiVVEFdw1NBsLxUP04KYj+XjS/BSq5WwUjsqcD4A6tAQfVrQDPbXUAkfqxAArMAwA==
|
||||
守护座钟,UzrlP/cM/8BE42fAJOdNvZHxf0BohdXAjoSkwMI7yMBjp9pACzX0wFGfMsFw0WTANGPkwFtc48B+selAqITmwK4ANsBYZMBAyRYvQVv0x8CzVelAj4yLP24x2z/hLQxAHUaAQRBx88BpbSJBieXXQLHDmz/1TU4/1zzmwL1jMMGo+fxA1RnNwKIMy0AfOC5Bv72zwBpySMDrELZAZqeOQFhzFkHIqxvBIhkXwbE+DEElbRrAEucLwGfbLUA5vLDA+zEbwYk1P0BwQ+s/LNYqQMHvYkHZVHtAIP+Dv9VPhL8sot3A7AUVwe8xa8CGBg5Bc7/oQF4y2UAoRwRBO//GwA==
|
||||
翡玉什锦袋,GtKOwPBlIsB2kHVApRLDv9P/6z+Wqn1AhnJrv/GgBMBuqdA/O2xIwFA5vsAa4HzA3eq5wAAFFL9zmw0/kPcwwQA7O7/fcNI//tvPQMsGnz7ce5U/fhSTQPN6KkDTrvO/jVvIQSBmRMDnzVhA8mcIQHfDIUFshp9AAyKBwCweiMDJhotAnE+fv/ra2r5YzadABBIewKJoGMGqCxZAOLCRv/oDxj9LfB/BqPr5wNsgST8O098/v0bjwLysCb+AYa4/vzWdwQBW/z/5V8JAYMMgQfsOB0H8W3FBAClpvoD21kAkBgDB14g3wSa8c8DLNIBALLsqQNoAnz6V3xJAYZPbvg==
|
||||
轻策家常菜,LLZnv8WOc8A6Ovc/lG+Bv2lW9r94yhzAw8i4vxWkasBSu0dAb0KZwKXhBcGPC7G+lFKfwM1WScDCdPC/2mL2wFEWnL/MyStAC5wAQQPWIL/L+w5AWjgAwGxToj+LV9E+eiOKQRxzlsDLJL9Af3BkQGNXsEBmHt8/RQ+4wHUkoMBsT71A4ZyPwOBd8j8O1gFBhKR8wLZAM8BPX8U+FaIRQDxi5b5M+/PA78CbwMwgAED2BVi/4qVXwPoW0T+Q65rAh0BWwdY2Yz/9JDJAWtq9QAKzEEGxVR1BZOdvPzqSbD/2Ju7AI73nwChpAcB+47lAwX+JQHGulkClQFxAR3d5wA==
|
||||
雾虚草囊,mId3PtTUkcCojE3ATumSPdwqiEB4VLY+T89iwKmmd8Di0JlACgtmwPWMEMHZB5I+nwGJwC26aMBU22lAu+0wwINXFkD29qpACe8QQVFSLcBw4WVAzlvMPnBpFr76wiG/okP7QH1ijcBmYtBAE9eaQPN2oj+rASm/CZPNwGiSyT6INdJAzPKnwCVhnUCupgtBDJ+NwO4NJ78fQwNAJzaEQF2mpcC77E7AQwEjwObIjb+llFjAGhkJwb9mJkDc8V5AJDX3wHChnT/SDVs+wQOHPyB1I77bk/RAYse2wLgFyD+nWwXAEfnGv4ILWD7Jz8JAlUSlQHZmkL7el7w+VdOdwA==
|
||||
宝石闪闪,Otp9v9kEO8BmOEy/3fYKPzoWtUB9LIVAibzFv1aIOMCUyoFAGVgzwCYA6MBfMg2/61SvwNqiF8BIfQ9ByNOMwP7VDb/BPHNAWX3/QKddAcCY8v4//7aUQJwcLT/MFAnATu4/QRxFLMAqIptAi0VxQC98h0Cw8dq73vOCwE9tKsAhnqVAIgVUwBCwHEASfMtA7kZPwEz5VsCk5xZAX/EnQLUQtsC7iv3AkEiBwOYc0j99vQPAavmAwOTuuD9bsKVAqUkmwVC6R0CN+XE/xlifQAVdkkBlnodA5GwCwM06gkAT9My/oA6AwG6KBMDS0opAcetOQNOLLMBnJkZADzMtwA==
|
||||
提瓦特煎蛋,VnC6wPYuTsCYfUhAfvFqP04M4T54PHVAaF4Tv2PpHMDr1ck/sKdxwFM3q8CbBHfADheGwGtz7z5U2QNABGASwV9fIMFsVwo/zs62QKNqez9npjc/pCCJQD/Sl0DU3vK9euO8QRp/DcAywANA1jreP2ayFUEH50NAp9tNwPfyDD8WoH9AGB7RvySxAr8oXY1APA0awIgp+cDAqcu/c0zZv6pr3L9j5RHBKhBhv5sf9T87CTJAu3oMQDt5FcAOEho/a2V6wRp7CkCES6ZAdrYmQRryN0HYRVFBZiuKwP9tmD8QPBPB38CQwesFwsD0VnlAeEYZQE6JG79cv3c+Y+YwPw==
|
||||
唯一的真相,9OEiv8zrLcAIIwE/7behP4zjrEBmUcU/ZH/DvppmL8C1AhZAkvVdwMxCtcBF3Y/Am0g2wPwwmL8N9hBB+3TBwIv9TcD+BmI/hwbIQBEeB8DYBOk/5QqeQLWfbEDSVDQ/SjCJQf16QsCA/YJAL2gEQOfFvUAWgF1AtwFowM39mMBtumRAl8MbwN7fpD+C5q5AeCUUwDXGCMGMxU+/6kM/P/BpoUB+UAbB2ucDwX3vnEBkSoU/LtyywKRLwL/8d4O/Dzk8wSZbVD/eDpRAXUreQB/uKUGLIkBBIh88v+J5BkDvBhPBn44SwTDQh8B554RADgcjQOus6D80HoZAzvrVvw==
|
||||
炝炒肉片,wvF1wAiRMsAH+aI/ErFPv0o1+b9f3gVAFI/XvkK3K8DZEC1AIU1NwCYa7sDsG4o/5BBhwB4vuL/4PdA/hdrKwOWN0MBuSgBAIfD0QO0RUD8ubM4+yYZMv/TrrD+cJhM/3oo6QRNRA8ABP1NAO2MwQKzlm0Bbsnq/TTWLwM6W10D4e6RAYR2BwP4t3z+SMMxA2pBFwFTHuz6uXYbAFSEfQMgiM8EEfKjAJhFtQIC3ob/n2Yi/1WuVPvn+zz5w+mk/12AUwda8ED+6a5Y/uRvTQMeopkBt6gtB+CGHwF6SPcDzoZjAPP4BwUZYccChrIhAsR01QMskFb9BF2m/pNtDwA==
|
||||
四喜圆满,xF5sPiItVsB4S1g9sitXwGBXZUDYyOE/rrAGwD0ENsDvindAQ3RJwHYmEcEh2G4/At6dwHjqW8B3cVtA6KXLwL/n4D94ooRAtAMaQbvHlr9llRpAPiyaPht8yr9uHlS/PrpqQRfiUMCYCrRAB0qCQHuXlUB36fC+aEHFwOjgKz+lFM9AcOeCwBrAUECyyQlBpuFowPkogcA+qp6/I8V0QKKq0cCjgKXAHsDev5I8vL9GXD7A6w7CwAeEMEAENApAC+9bweSFAEBOAIY/C9WkQHP/9z+grxxBSdSsv+DwUUB7e0bAFMOPv/CsubyAnbFAr6aJQOWKVT+HmQ9AnSyTwA==
|
||||
学士的书签,vBAJQHtsxcA9+mHAsntNPwtO+D8mcSzAdt2OwGW/ucAjWMZAhurCwJQKG8HrxDDAm1+qwFdkp8DcDJxA4EKWwO//hcAUO3dAdYwfQZ274MAqUrtAS8uOP8gVUD+D6Iw+qwNRQYtatcDgb/9AY3SkQGJql79un5E/oBXHwNJkpL9ondBAdlvQwI87lUD8dhBBswaewP6+JsBxroFAFUdrQK9pncCfoGzBpMgqwQK/ckBxTRjA91tFwL7YpD+KuhlAAMQKwdtv3b/2OiJAvZO1QJbkE0E6+J1AOdh9PzhACkBESp/AgJn+wOJ15r/OL+VAZYu6QFsIZb9tP9hA8AOfwA==
|
||||
天枢肉,5lFVwM5dVMCScINAALg6wDBqxsBkWoA/5nAcwPUlCsAAVyJAgWpYwLcjAMHsRVdA6zf3wHZt1L/sAjnBwpkEwU+nB8AxLT1Avk/qQCW1M0BArLM/BKyowDU73r8dgJm/vBaHQY4eX8AO6GtAI0NnQHmjxEAlxGo/soTLwNjxA0HWhcVA9JxNwASu6bziO+ZA+6ydwHZsAb9bc2bAvnnjP2nHgsENfmHAImhbQADOAsGmBVq/HVDewJ0QNEBQe1c/Ln6AwVRTH8Da1AZAIm2eQG2InD9Gf4BB/qZbwIOM1j8lUUHAoKvPvzyag71Er5lATeeLQON3bL8FIwzA1YBIwA==
|
||||
蒙德土豆饼,VOmpwAzhPsChlxZA5jMswFE4RcCYEx5A4TqLvwEvHsCkIEhA1YEzwN/cAsGAYpU/f5W1wNLhlL9xS3TACQT5wNgBML9Vm09AsX4JQcXG2j83mUA/9TrWv8heGD82Dia/6dKCQRzVNMDl7nxAN7stQCoK1UDSLog/LeGuwGjNc0AQlcFAkaqHwA7qBkDrD+1AIJdjwKireb4EjUq/GamhP987EMHTQZTAjwB1v3rmp79YrXS/5gMLvxoKuj/8WdK/h3N8wcY2ET/FmjtAszUDQaXjD0DIfxRBlO+dPz5TTz8p85rA6YgGwUDR5r9yu59AfBZtQBuI9z8b8CrAcYgqwA==
|
||||
特工祭刀,/yGFv9M1jMCPVre/DCKiP5mWML8UrGw/uZdTwPkMfsBeSZBAkrh3wDT/B8EBnTw/M2uowB96NsDyGbq+7eSUwOlL078I0ohAamwFQaYAAsD/clJAcfMIv0K6Ar16u1G/ApIeQYLEhMCD7qhAQFeBQCqIHkCCS3g+Fgy5wHQQyUDRlMlAmV+JwOqUMEDRnvxAa2OUwGrPmD/X1eU+NWZLQOZGLsGDVrHAZlpKwOtgX8C4eRTA+aHEwKjo9z96T6NA1xULwTh9B8BElZA/eCJAQBSlNUBppg9Bi4eLwEtjrD/7Zx/AEwtgwMDkpb/Vy7hAqaeSQOnPRsB4D4I9c8iLwA==
|
||||
千灵慕斯,zIXCP1DE8cCAlUm/vkyLvwQ8CsB+m33AOcS3wDm5z8DsHKFA3D3jwMO4DMGAuhzAEc/UwJ1eWMCAPuw6gQiYwFqlFMDOBVNA4ucCQXRBo8AMX9BATEtmPgFrjT+wAmu/U2GqQVvl6sDWk8hA7m+iQFM/Z0BVxQNASK3dwOGNUD/iPtpA9CaPwB+6DEBaCQpBWA23wA4fgMArmDBAUoOQP4ycCMHcQkrBlr9SPw+EW8DwO/K8+IbHv9BWtj+/q+69ecpfwR5bu791ZlVAU9jxQPwxdUEasWxBTraQwHevEUBgBgXBCP5GweBWC8BmBupAJvHLQBo1v7+FJ3tAmvFDwA==
|
||||
感别之冠,6s0rwMqKh8BSl2o/HUOJwL/XVMCIAlXAfXfQv6d2HsBCDC9AfFtwwFJTFsEydzVAfJSfwLpKhsAOuV7AMUUYwe/OuD+0G5BAmI0WQQKFoj9dyg1A+1+8wAW/i7/1QLY/2xZsQY8VkMA8q8tAaq+DQIeP20AWyX+9lBHMwGrtZMDQbNFAz0iRwJZxhUCbCQpBtSJqwDqq2T8IU7m/nRlcQKoee8AIBy3ASromv6ETD8BQIgbAHPzZwJL4gkBeZkHApOpPwUbwMUAJKvg+UvI6QN7HgUCUMwpBfPJuwFKfDMD6Oo7AsmzCv+8ksL46prZATg+OQKi9zkB/H2BAWcO0wA==
|
||||
盛世太平,efaAwAfmfcAkln8/7wxrPlMftT9cPJ4/FXilv82wTcAH1ElAZ1SWwEFx/MCkh1bAfTahwGgE+r8iwZJAmQYiwWmHz8D9/z9A0KAIQc19i79UChRAfAI1QAEuaEAxq4c/4mawQaqEa8DDpKtA6zhLQCqZ00BdukRAYJKjwLw7GsBEp7ZACRJ8wPAaIkCJvOVATYFbwPAAzMAhf22+lzmLP13fukCJctHA5PgGwWJNr0CqFTg/XEOywHQiP7/w6/a/8al+wZ73QT/nh59Aj+HlQF8c+0CcGU1BIKKMvUww8r44ThzBBQdVwemXvcCtXLVAOUKAQDflgkCyL6k/iYwnwA==
|
||||
摩拉肉,AmlTwAMw8b+OO4g/RBBdP8JVb0AnXaVAJC+zPkDGD8C4vfg/vYYewAbzssBQhQfAYfUewMRl1b20iQtBB8OtwMmlEMB7Dsc/bIPMQFgVxrvoI/i9hmGtQIIEWUBVbIO/4XpuQb0Xsr9AjCdAThvtP+pa5EDtABA/ITItwDzULsC7UX1AvVAZwHVqjz+wo5dA84nhv/IemMAJYr2+ONn/Pq5C8b/wMu/AKvxuP12CTUCYyRe9NCisPaRwbb+ssAJA98g7wXZvk0BVAiBA4Xf9QMuMvUCmu+tAI3L0v9bqB0CwFLnAQoclwfTPi8ClJ1BAwiLPP+NMYb/t34M/cIxevw==
|
||||
雾虚花粉,Z4ejv3KxbsCIdwbAO67OP78eR0AczN+/bdTxv6l5IcDhW0lA88pBwAZj+MDP94u+Npc1wEuPZcD7B2Q/wnZxwJUD/L+ThmpAtlP2QCPPP78koAtApYSyvw67mz8FI+I/YloNQV0qccB6J7dAEmtYQAwQP0BsBq28UTaiwJCYB78LDaVA6R6PwE1ZgkCeYOpAuPpIwHR8874kfDA/w3diQA0Th78b8fe/NUmiwNw0Cz6bgQ/A05L+wMzjAUAhXks+ppTYwBp4pj8xUOw+81GRPQadGECQY/JAWcXQwOZLPr+dN1vA/RuPwMgtOb/cy6BAEBJ5QEWCNkBjsIU/cqqRwA==
|
||||
串串三味,gLzLu3ixk8B8Rpg98rN6PxugAUDATqs/auQ9wJrIhsC6vmdA1hOXwMLf9cAKPzbAxrKgwEg6FMAk478/G3/DwF+IdsCW/jZAr13zQD4NJMCMm11AFGY2QEKswj9cggHAH6+YQUEZkMCD3aRA1KyFQMwBv0DXWSpAZJe2wADQTcAtbbRAoW9jwARE0T8U/+tAjueOwC6e4cBcQ20/rC2sPyUyhcB/nyHBg9i+wC4MkT8gmnE8F1Y5wMCBh7vqjHpAAFRTwXFBJz4KDlBAE2PdQI8/7kCAUj1BbIEowFaMhECbVb/AR6cVwaCXuL9XJL1AlU6RQIUsC8ARPX9A0LoGwA==
|
||||
老兵的容颜,1oYKwPZ8Z8AKxFa/1X6VPy7lTL+gPa89uakZwI2SSMAg/khAqTJ8wCtKysDl65nAHqpywGV34b+P7DLApzTGwF9TAcHrWJw/PjrPQFpdP8Cc+zJAt19cP25PL0AGUFW/yqFUQZBMZcDB2YpAPiUvQDjHJ0AW3Y9AkdeCwAhIGb9V+n9Ailh9wEuC0j/wArhA0CNzwMYxqMCaJBdASQlvPqIBHcGgj2nBMQgFwT2JAEDC4Io/02eDwGQU4r+QgaJATrIAwe4ZV8BLZqNAHUDvQFgqx0C4jClBhv0yvxaRUkCUQbfAyqYywcI8/b9JvpJA5mBhQI8Cc8Dwno1AkUnJvw==
|
||||
祭水礼冠,Pu8YQTYRPMH5LtzAVrbrPxxj1sBkrUzBSuQbwVU6MsG4+zVBbgJEwf4sfcGK6RRAqNsPwZ+vM8EIVrq/9gUgwIWYAsG6PO1ABlF4QWMOQsE64TNB/rPrwHzAEsCsOg5Ag2qCQRhMHsF41l5BDc4GQe6BOMG0xn7AJuYywRodnkDuPjlB4FpgwS5zEkHKyGtBQpANwehXl0DcbkhAKBkQQZRNTcGQq13BVV4qP0AoNcCKHN7ADnt4wJjx1EA02nnAmsY9wc1VcMA4t22/RNljQJ7PXkEy2jtB/JejwFjFDcGdE5DAnOP3wE6rTj+u2jZBeBYnQfEtib9YPadAOuM1wQ==
|
||||
灵光源起之蕊,ND44wKLqYsD0Une/0P66v62H8j8ATTu9GowDwJlOG8CDS1NAb5xMwBBh/MDHQQLA4Gp6wBpGFsCDXkM/RwnYwChq8b8KtlRArsH2QC+qOr+TOiZAtyyMPSiC1D9CnC8/E2I8QTyyV8D4jKRAUYxOQFL+k0D1bBhAn1SlwHiyj8CSkLVA3zpWwMa/REBicvZAQ6FcwC5qT8AyJgJAjhW3P1cUEMDYzvDAEnSZwKDIvj8OtDi/n+MJwTDBWz+isAJAlzwKwRGzEkDKUjhAZpl/QCNvkkCnOwlB3mGewFWd8T8GebDAlJydwDR4DcASGq5AVGCAQJnIzT8dg01AWTJUwA==
|
||||
地脉的枯叶,9aqUv0xaqMB10knAs31Dv/xHt0CwsS4/FXptwGvjecAZrqBAktOHwNikDsEwefu/4b2TwBWdW8DkGvRA3RKcwG5W3z8yJ7BAT5kRQXcKPMCg7Y5AwmU1QN0t3D+tELm+0idGQSNOn8DPzuBAz0WiQG3ilUA2pow/ch/IwAIOHMHs09tAK+6DwF9XoEBlGQ1BzxWGwGhMe8BCon1Az9wgQADen0APCL3AjP7awFlQr0DZkQHADQPxwFqxyj8o9ZY/Bu4MwQK/j0BuJ90/RLwvQLzUkkB0X5tAcBaGwAANNEDXY6PABxihwJrrB8B0BdxAXP2uQN+XZUAe42dAmRZuwA==
|
||||
遗忘的容器,OP3Avutwg8BPUU/ADCvBPv6BqUAkOx1Ai384wFUtXsCwPZlA3FBQwMh8BcGoLPW/C6eRwN8cS8DcYMxA1T2VwMTwK7/5eIxA/0sLQUveZ8AVMGBAyMBMQAykKj+PKuq/baQTQWfBcMB/TctA1y2WQGjiDUBBins/EDOqwFSDl8CSn75Aj8iRwOiDikCYIPVAJAGEwDxAUcCGKH9AvVY4QDL0aMDEQhrBr24ewVTZc0C3vgvAEC/SwNRNgz93nvpASxLxwKVP5z+Gbsc/MpZaQENQgD+tuVZAVwzov3nlnUAJFwXAuyxJwJAgDb+8O7VA5/uQQD4gFcDTP5hAm7ZgwA==
|
||||
丰年有余,rQPmv3W8XsD8Tac/zbrgPyzCXj/Gv6c/v9jQvxbaV8CvASdAW22HwOmq1sCk+f6/8XaVwEfQg78iNy1A/lPtwKJBib9XzBBA647UQNvITb+8jus/upceQGPlEEAiHX2/TTyGQQyzgMCDgoxABG1LQCCo1UCfbjZAcIWlwBd1db4MnKVA4QhDwEJZoj8IZdBAmo10wM7iksAZy62+X8inP2q7AEB7FJ3AC/oGwRAwzD/hPps+HafIwNfPv74A7+8+bSNRwaBk1r/BPmNANTi7QOoJ10B8VmBBQNcFPVMlxT+OZQDBZuf6wIb6bMCKvaFAydtuQMVvQT8HfjE/Q+skwA==
|
||||
爆炒肉片,QO25wHe2cMCl0IZAWIhywLfKDMGWhwRAd8XGv5ZXBcAn/jhAPXVlwAfsAMETSIdAL37cwH6P2r82kRLBSasHwT2WDMG0nABAPQT4QGT5hEAoCZ4/2wa0wIyfTr8G9jg+lxCOQU3j2L9uIgxAtOBJQIWn6ECB2pe/+DWlwEz/jEDQNb1AnG1WwBY8hL6Wzc5AddaAwNq0VT+zkaHA6NZqP7oYo8FBddvAnKNlQVHS4MCW9zC/R7MuQBNEJUBMF++/RDNywXWoSED2M5g/ozbxQCero0DvFDVBJGr9wP2B8L85SEHAysbBwLi20b/0+5JAWJhrQClhEsAjKHE/VdEQwA==
|
||||
学士的镜片,rnN7QI9lIsFcA5DAVLEJwEjwdcA0qQHBJCf4wCDaEMHo7RJBEI0jwQqyVsHYB3a8Y6LxwBsr68AgfnNAsBezwGIIo0ASMvJAZhlWQTSQ+sDomhhBmg5dwL6uJT1xmY4/2JyJQWRFHcHOwjdBbyz2QIcqjz/lLqE9OPsfwQjYT8GRvS5Buf8NwbYyBUF0qFZBlovpwAkypj8UtaJAeRmOQJ7sW0BjezXBhwqev6aDtEA3S2bAUHLtP1ffj0BdKzHBCQFuwbqVE0BEGPI/0fu/QN0HV0ERjoRARrmhQJLrHcCOkBjBd3XawFZE078tnjNBEewVQW98+EAL4NtAW37+wA==
|
||||
猎人的胸花,SH6WwFYtecCZLv2+gImjPrVSYUBQatA/VM4LwH9OLMCsOGhAi9NlwMnBAcGio4LAfVSqwGbqFcApUQxAstkmwbAiQ8AjJnJAXa0GQSU7yr8VWCdA3Hg6QOCWKUCjw8M+PmZoQaaXhMB63rxAiDKEQEIVv0B66YJAiMWqwFIROT8BwLVAJy1wwFsjY0DMkOtA0u1xwGpqqsCZbEpAfe6XPwxvBEB8owLBwOmcwdxvZ0CIDgg9I4y6wI60Bb4c73hALQc9waDamr7XG59A3k/cQOMJtkAw59NAGuzTP+R5LEAiW73ACBkiwa11lcDr1bRAKHyNQFniST/WyDlAoVo7wA==
|
||||
北地苹果焖肉,E+STwDatM8An8GhADlyFwGgESr9i0jVAM1qcv4TY4L9W1as/V/YswGEPu8BYU5q/I/23wMgQJ79UH9S/hn4AwRBfGMDnGOA/5tvQQOvP3j5mX7I/Fw4RP8xNuz/+7wO/LMG6QWVvJ8DyiSxAVnICQEjt6kDj2RlAZ7x0wI9FOr4s34ZAtxPrv0wQSD5rEptA1KUfwGE11MDBcBm+eCIYvx7e2sBuixTBYIK9QN67TsAIrW8/xxJjwCBgJT/gB5+/zMyawVVgC0BPRZNAszgLQRrC4kB3RWRBQRhCwLDQb0BwL9bAAbgjwYwKFcBlu11AcpskQKV8MT93l0U/qR6uvg==
|
||||
侦察骑士烤肉!,rFJBwOf7pL/vnpQ+USb8v0QNej8OxqBAdO/pPlu3AsBjfxRAvsixvyrazcCQWIM/zEUPwKSj/72k1ZpA8Mt8wK4RmL9jkRNA7dfnQPfZ8j5/pCu/v9V6P5DDJj9fl4y/l0X/QIBeNL9QvCFAdlERQFBLikD3G5q/VS9lwIBttkDiC45A0vhgwFhNCkCW161ADysewGByaL2AJXDAnHEJQDSuQcFbJpbAJluWQPr/974uCLq/iDYBP17GCL6mbFlA/oUMwcX5vj+FLno/YvnSQDMPGL/bktdAJFojv/gcbj7LajXAtSedwP6m9b/TnlBAGkQAQDm1+L/ur4a/AJcNwA==
|
||||
杏仁豆腐,WkUMwLq0K8CHBQJAckBaPziBlkA9oog/9wQ7vsHo/L+Rb5U/MGxhwE+XiMDkE7XAaWVOwMBMFb/eVNdA5r7vwCwur8A0z7i9ytSgQEYAdL/c+rs/7SKbQEA/jkBn1XA/DUmxQQbILsBlzz5AQPOjP/Rp+0AZsZZAoGUuwPTk6MAncSVAFNi0vxbihj0ETHtA1CXUv+4KLcH4YUo+F1eFv3T70EDjDxvB7p/BwJAunEDPwh1A9rqmwHqsCsBqS0nAnC9pwYAUqz+aKr9AbsX6QCIqTEHQBWpBVNKPv7gFL0DQOijBzr9VwfJknMCiHVpAG8IEQEQwLUBumYlAMpZPvg==
|
||||
星蕈,QIppwJ+277/KECu/ShiYv126pUC3MjVATE7ePtxNMsDMuSpALNwrwKxh2cDP35vA9fTSvxS9M79KgwNBJ33rwBv7sb8FTdo/RHf9QE6xw7/GlNg+5kucQCU9RkAaaKo/S9FHQSSQDsA2gIBA504RQOH+xUAQr0BAbPpkwFrHLcDdVYtAvy5ewAhYPkC508BAp/YNwLOh1sBsDxm/+MN7P0iga0BYIgrBVKAnwUgu/kDoARk9+p0BP9hl5L/KRd+/jpAcwZiDCj8YM5NAt/IQQcUrtEB7eqRAu7P7QA6nH0AEtgDBzztGweAYsMCUjo9Anz0gQDKxkUBCJ3NAaNL5vw==
|
||||
地脉的旧枝,7uiUwGvUPcCmiei/MqaLvWSEZ0Do3aU/ga/Rvw9Zdr8PmC1A2GOWvyfW68BsS4E/74VewHhOEcBtYQG/z1qXwIo2Tb/4BpdA2m/pQIqsD0BDiH4/OX6Hv2ABcT/D9uo+OxjTQHJENcBZU5RAsYRSQGHy1EAyt1o+W6eZwNxd2L8AuKpArMZJwPUihEAaqdtAoKs8wGqlHT8AMgA/25odQDX/k8DsOc4+lw8fwHTUYr5qmADAA2b/wJ+oBkBuGjRALdHNwBLGPEAUNKE8LNxvv0jPHsDtP4pAzY/1wIwqyj7j956/iMKcv9jdEb8trJFA8Y9hQHZjKkCWl7u+D75ZwA==
|
||||
雾凇秋分,lNcUwL9a8L8E6Bq/Oax7wGzYx0ATXbNAE5+Fvwo03b/JajtAb6y+v8r63sBX4+q+G3GKwKbWvL8nOwlBlGyzwNUHGkA2JIFARFMDQYdVer+WRJg/3s+KQHiEE7/3BBLAP3lAQQlYu78PqXVAzUZMQLSQs0C+/nO+bpR/wIYasDyK7KFAA00uwASETUCyf7xAVMEawL6vh8Co7Qc/jH8ZQK3A88Dx1MzAOhYMv0SfIT5U9fK/DqqHwMn+nj/IoOJAd8JKwb8kaEBKjKM/iW/AQI8IWr8N1bBAttMKv0BAn0CnlOG/u6IWwCikvr+2dmhAgDYtQI3F/7/pQdk/MSwhwA==
|
||||
鸡豆花,w1yGv5Y/hMBrlb4/xoi0PnxgkEBQ+PY/Yx8UwHZmVMAeWyNAxI+JwGpercAEVZLA5sKlwNzxlr+AoptAE0bPwFivH8DCPMA/dt+1QA7pHcC24ExAw8e1QJPoUkA+jra/hn62QfRdg8Bys4NAmM87QMyo30DQZHpAQviBwM4P9MAk/IBAJ6nKv+BKDz4EL6JAwHVJwKtIIsGrsENAA9RfvwKxNEDIIjDBhgCEwE70/D8DrOE/nmi1wGjYV7//Jo4/kmd8wdjYK0Cer55A9JHtQCiiOkEOoVFBBLdzwF9RuUArfPvA2w41wSR2Q8C4IpFAW95hQN7yJr+j4IJAPiRAvQ==
|
||||
黄金时代的先声,4pvTPtN+s8DI4HPAyAUuPww/vUAQ2oK/OlRwwLPspMCIXKpAno+zwLXAD8FRRaHAJwp3wB5WbMBmER1BniGzwMCJaL4qYYNA7y8YQdtpvMByOaJAnmaQQPhCNEBxQQU/q1tlQcryssAimPJABvyZQB/lCkD4sCRAfQPMwH04psDmr8tAyKGywML8qkD69AtBdhyIwP4vwcDrnIlA8yghQA/rt0D69SbBfO5EwSR9zkAX952/pkS+wMQV/j3CDqQ9O6IjwQRw8j5523JAEPS6QKz2CkHMIAVBxeAwPydJBkBe6P/AojAnwWYQWsDIO+RAOx2wQGQGJkAAVY9AcHF6wA==
|
||||
夏祭游鱼,IV6KwLymccCa7j++4oG9vRenQ78aCS5AMAUGwPl0NMCHtXRAJ20/wHHxCcHcpMA/mcW9wAIAzL/IqZq/z97kwLXOf8B6M5ZAi5ELQZEcyz9u7eo/DSS2vzRCGz9+4Qe/O1pKQbjMUMAD4ZNAlg9zQIBIx0AuLSe+f565wAIxjEDlAdxALeB+wETdNEBwgfxA3zGJwHAhNj4PxhHAa2ggQBt/3MDnG6u+1wqhwImCl79Xkr+/f7bGwFLtuD8AIYNAjt4cwSzccD+IZIY/QMV9QIwZx74IugRBuWi0wPjTUr7chmvALkSOwGKBJcCOx69ATEWMQKrzrD4lO6C/OLl1wA==
|
||||
祝圣精华,WKRLvjCiz8CYMlLAebQVQC+y1D+NzDHAABOYwMg8usCe4bpAIKXFwIgfIMEtBTfAkJaqwM7soMDFTKBAB9zWwBXQS8Dr2ZVADK8hQf9BtMCIw69ATrQFQD8tCUC+mpk/q0FkQfOjy8CWSwRBlz6hQMEQ3T/IItI/DBXQwMzlOb9sb+pAIYO8wEAzn0D9NxhB9jqcwKuv2b/3hYFABupbQKqxBz/85irBkHxtwWkKWkBIsQ7AHzmYwET35z9OMBlAtZIYwRxzSb8jhipAkVqwQNDTNUHfDKJASIDAvb9DIj/bRL/AKGEwwe5TjcB7H/FA3AHCQFStfT5nqpFAJW2jwA==
|
||||
岩港三鲜,okh4wBzaG8D0I4xAU0ucv3shuL94JYe+bD7Hvka5rb/gS1M/OhlFwI2nj8BM6wXAiHedwGB4Kr+720fAWdgDwcIRxMAls7y+V1GLQIUJDEBvuHg/xKAnvlqWMUCbn4U/bXmuQcmqCsDWXPQ/Ii8sP/3cDUGzvGxAAFMvwIEWksCJ9DZA6QtLv0iJ3L9rT3pAKl8AwMxB0MDUhpa/IILFv9FMh8AR6xXBmAAkQHMeor/h4g9A5B6MwDwGx73TVH7AOSp4wXm8AkCyEZRAv2vrQHCbQEEGbWJBXGqPwPwcHkBes/PAXwUhwfQBVMA5cDtANVn4Pz8xmz8uzmtAYk04vg==
|
||||
满足沙拉,/GejPrr2g8AH9oU+5XHoP2cHGUBW1am/DbOZv+CpecDUnjNASzmWwBeUwMCDnpLAY3ZfwAKH1r9fhPRAu4rBwIWwOcDeHm4/1J/QQGIjR8C4GSFA+CpKQLo6WkCMjJk9Cp6NQQDEk8BftJZACbdBQIpss0C7/mZANWOLwKRLmcAMHXpAWXhhwIs+2j8SkL9A8udSwMdayMDnMdY+fI8IP9ZwbEDXJibBgWgiwaVLyUD8gp8/dlJDvpaCvb/602XAJx86wUDym79DP5NAJxr+QOmfU0GfrilBkB+CQEZxcj/UwR3BS806wWIke8DWjKNAMKpiQBf2J0Aj+7VAIsL2vw==
|
||||
四方和平,EuFdwDjYhsCQxYE//SKmP/Qyor+CS7Y/PT/rv1tCYcA2LUlAWYmYwNHu8cBhKznAj3mfwGvixL/WzLQ/7nULwclvu8CDmCFAwnQBQQV+g7+rmhdAHwv2P5PoREDaX2G+mn6tQTZWZMAZY5FAubVBQDJO1EAfF0BAZyykwKFS6r+7I7ZA+Yp4wPVR9D9K391AAJlhwKLTqsAHl9Y+HmwAP5wKy76Lwg3BjO6kwCtxG0CqVS8/IKvQv6TkKL+f8RO+ixeAwcGoJD5r4pVAiV8EQVs7A0Hw6FdBhjs1v74JVz+CMw7BMzNZwf5ChsAWSrBAWgp5QHSZQj4DZfA/VAcBwA==
|
||||
咚咚,NvkxwA3Ujr9Ylxk9FmBZwJogFUF7uLRAkhi0PdOZRb9TKzNAuMZGv/Fv7cD3xLu+Rb6fwHYKKcCTNgNBm9LjwPObsj9eQYZAQWQLQbuscL6sa2U+s0acQETYFL/LM8W/DCE0QbZdXr8KFpRAkKlUQOeWqkB/c52+MeRbwCgFG78Aq5ZAahAjwEZGPkBc78RAmxXwv3EOkMDyAwJAxzhJQJsBhsANmLTA5vQGwbfNqj9NpDjAFpnlwM2EGkAU9epAf9Q9wQSRvUB9wBE/i6GaQN605b7hCKY/yJAyv+RsrkD/F+A/7rQPwNx1279xZ05ACQoRQEwtF74xVEtAbW9DwA==
|
||||
三彩团子,Z96twMzTQsCsjPY/G4PMv64vxj/RLXJAuTq1vxqK7L/cTtg/cZ0lwEJlusBqmynAfDWlwL6ALD5nahrAXAjzwBg7o8CNARBAhCbCQLnraj9gQ60/q+/jP2IHLUAXWpC/kCGfQYC/NMCdKzdAQNEiQIYR+UD1wUxAbaaHwELZpD8IDY9A3pHsvzyMOj/WHqdAb/lNwPpE08A9nZo9bEQ1v6yzRsDhYqnAuVYxwLo9l77e9NI/sQakwPDAjL/YAz9AvxJqwSwPqD/v7ZBAgT3SQAMfNkDQvU1BtUObwHzSUED28sPAM2g9wZRCJsC8mohAsl5SQGXINT+YoKu/SJJKvQ==
|
||||
混沌回路,BgRQwG95JcDCxi2+wQk6QN+AtEDGvWlAbMP8vm90DsDVyUZARA1EwHpi4cA6HnPAq55lwOwdrb/mLbxAyYUFwcWSpr9BGVBAGsv2QH8Vg7+UwIc/gLySQOfyXEAG7Vm/WolfQc1KNMB3Z6BANqtEQHqlmkC3CFJAKfiQwBz2scBLsJtAQxBRwJLoM0AEEthAXkA1wOk1u8BJm0JANK6rP2sXQUFy+orAX/OcwbBWCkG7CJ6+iIfzwMhOhL98Ybg/Fe8nwRzx+j/7vndA1fujQPuRAkAD8NVAzPADQJa/PEAJyM3AVH77wHi5acC5cp5AshhOQBuuZECpKNc/E1ItwA==
|
||||
连心面,duCjPnh4TMCorak/Ggkiv5timT9UU/w/zk5fv5tlZMCE5TxAjLaKwKPT/8C+Qpm/HzCBwGmwE8Ao16lA4e78wMrhCT9rlCRAujsFQQ2wx79w8QdAe9fFPyZmhT+W4XS/MO+UQRiRRcCRopxAWVxzQMJgvUAI2fI/ity2wMNe+sDhq7ZANzZdwO7iwz8UGfhAX2tawHU438DEwBG/5075PwcJMkDEOwnByzyawIL+VUCecFe/aKGdwOwMEj+gzPK//5FvwUbbLkDq9E1A55/bQL/fs0CKAVFB2bvQP/rAZEChqevAK46jwGiErr8eS7JAGfRuQF/b/z/NFrtAalFSwA==
|
||||
黄金之夜的喧嚣,V7MFQMhXs8CfTUzAU5OBvxmZmUCYedO/IBOKwG4EocCAJ7VAQNitwNICHMEIZEHAUDm6wOLYm8DibPBAWSWxwBpa473i4ZZAuaMlQfum1sAg96ZAZVw5QPijZD6eqmg+6EdhQf6EpsBFDgFBl56rQLhoCD5yOos/XyPRwBLnqz0CDNJA6Oi/wIwPrUCElBBBkK+fwMrHi8DcADtAVmGCQICoyL9oXDHBjn7dwM9WVkBzaiTAfdUvwOCC1j80G00//DpQwXm+bL84FjVAiSm6QEAS50A8iv9AUZBwQLFGAEBAH5bANU3awAYT7r+vYN5AOXa2QHCYlD+a6I5A+I2dwA==
|
||||
黑晶号角,Ivn5vo16cMDVrUbA7rzdPeoQ4kBjoSFAu9QrwOsNZsAXq5FALUxVwEK+A8HCWwvAtf5LwOKwGsAIV/hA6h5KwEQpi0CFNZNA1c4JQTYjPsDvvUNAWBFyQOZhxj+S0lq/3yX/QPZGaMD0srhA11aEQOIjDkAHBnU//FezwEWrlcCMG8BAm4iQwMr2lkAIDwBBohppwPDBVMBMunFA7Xw6QJRQGUCJZp3A8fuTwAaXZkDQ9yXA4BL6wC4tcz9YQaA/xooIwSniK0CDWug/5dAhQK+D+D1LOthAKMXTv0BVXkDsM3TAR2p7wMRofL/Uz7lARJePQKMdkT+YXlY/m6B4wA==
|
||||
督察长祭刀,1/6Ev2ItgsDJmCPAJH8FvX/7OT/EkRpAhWdVwNeKc8CJj55AMxlRwMWtC8EXVLU/b8mWwCejNsCqYuE/32o4wAyYCEC265hAuXYMQU24BMBuvU5AXDwWPmRr177FpM+/mOnHQIFSaMC+da1AxS2LQGE08D+zse6+ZVi/wMqXwT8EUtFApqGdwKLddUBKFAFBSj6MwIC4uT+ACfU/0HleQLbQP8HN7sLAhTZXPwqeQ8COwVXAQrbuwGr4IkANqLZAKvzuwNYrGz/9urU+khIVQN9uy75Df9lAVGOywMg+J0ARXmS/uFD+vsgZZj3G9bRA+U6XQHb5e8DKQJg+03qKwA==
|
||||
来来菜,YpgJwG5AQcD+bOM/yNvhPm6y1kBe9m5Anbmjv/b7+7+8nw5AsyAnwJANrMD0yGfAtUO5wJZKqr+ADMdAh/bwwGgNSMDf7NM/oPO+QC25k79CDRRA9rq4QPXbCUDSo2G/AU+lQcRqPMBu8mxAB9wwQIO76kDOiE9AsHBZwPJrq8Ci/WpAyGJ4v2XXuL2lLJtAckwlwEkMI8GOL+4/omz7PaEq4T9FTBTB8qD4wPm0DkDVdoM/tWkUwS77Xb5ebGhAnSVowaq4D0AX04dAVCrDQCZ3B0HLuSVBWhJUwGCb4kBXT8LA8ufDwIyJZsB5+HBA8PxAQFULvL8PeqVArKIRvw==
|
||||
「堆高高」,0Oq1wM9GTb8QQvw+N47RwNhdBEFzd5pABEdAPZ3OU7+SifM/ZkTbvhJRycDCAl/AEqFwwCzIij2AQp1A0kEAweuEz0AgXk9AwqnjQMXjDj70Vny+0kKbQFARrD82Lwu/vnAUQcB/8r8e129AUFcxQAaUBEE4M2JAuA9vwMyvgT6C03xA5rzDv3JbOECp9LJAnwEJwNmRycBN5ck/RcsTP0r7mr+6F6rA9lUowcCxNkCMUKy9YK+dwJ5eWL4Ce+c/Fiw6wdFhNED6xYVAf3TtQJoR2L6u+zVAHp/jQIje1EABQj7A/l2pwPQyT8DIDmVAcjYhQJT+nUD/Cd8/h6KYvw==
|
||||
大英雄的经验,/bWQPzAhcsDBppDAEkwrwDRCoUBwSOI+aOl7wHYXbsCdIqtAIfArwKIHGsGfofc/ly2XwPYakMCItRlAzDGCv9n7uEAWmcxAZjAjQRofW8BZolRACb2Rv/q+KMAfRZy/1X+ZQHmKcsCFCeRAmLidQGNejb9ivRnAibrJwBgYxT8Gt99AbUjFwOC6zEAB0QtBzb+QwOKI1D/pnClA0vS3QMSkFcEX8CHA21orP6mtHcA/hLHACcELwW/VhUCg2ZFATgv3wBnCuj/S+6K/ar4LP1YIr8Aw/69ABdQ5wDYLJ0DTab0/wx6eP/twK0DAM7RAghqhQGhN5j1U5ig+u0DDwA==
|
||||
祭冰礼冠,lEBtQNCF+sCqOoTAzATTP9pUNMCsTKHAuvrMwIAk5MCpLvFAeTH+wDwrQMHZO8s/hSHXwODd38CYWJm8eKdwwNhahsCi/MNAZqM9Qd0Uz8DARuBA2IddwC5ng79a6Cs/vHlHQS272sCR0BZB94fPQFL0Z8CLePe/wmUIwVuflUDEsQ5B1RsPwXh+yUC0YTNBYkzOwJzJ9j80/ew/EqDEQP8iMMFNshTBm/t+v1VDQcCzCZfAnneXwG6oiUAIti0/gfwjwXB8G8BKLhs97C5IQHF95kA91CFBJsOdwCScYMC8pWHA8N+twD72bL4rEAdBafHtQJWSxr8zUOQ/YBr4wA==
|
||||
黄金剧团的奖赏,yFstQBTBmMBp0f6/Gpf3P3xIBb/vsF2/gjJAwDSKrcDYyqJA31aswN5YCMFLNAy/AhZ7wJKQbsCIfp1AgNpEwLfsJD9DWi9AvjQNQZHeqcCEGINAx6wOP47sTj9UpTW/2tJLQahHjsCQO8tAT39rQBhIqz3vagc/hJ21wBywqcC7Y8RADiu+wOOMUUDLzgFB+V6EwPKRiL98hkFAO1NFQFg+ncCYk0rBpMpiwO02tz+hISTAxVOQwGJM3D8UcyQ/oWkrwaal/T51kfI/gg++QC3c9EA0JAlBJjgJP/7+/j8+tb3AmCCbwF47Ur+sI8NANLCTQJTqLcAWnKpAiAqYwA==
|
||||
「缥雨一滴」,ncPxv8DtcMDoAZ4/HzZGQEKqQ0C6lwJAHVK+v66VVMD4Uz5A/o6VwOhz58ChLpjABZKewCrfxL9axJhAKqkowYNAqMDnsRlAwS71QPUR9b8nTwZA1P6ZQLy2U0DJJYS/TISmQYVkg8CqQ6JAxYZUQGcx30BVJJRAtuyowJCblb84hqtAp0VMwAxq0T+sxeJAOEp0wFstA8GD5ta9JwWOPzC480CMc+/AAQOOwdPh1UDkk44/YPSMwC6877+s8NE/l9BtwY2rH8BAerRABHwGQWy1BEFusFlBypmBQKbDEUCe7RzB2e0ywU7Pq8CcHbNAdqZ8QMvbrz/4BSJAs4IiwA==
|
||||
昔时应许之梦,hLCYQP59LsH26+nAptQTP5sCqT9VtEXBmQEQwZQAGMFxBBVBYz8cwV+0acEPjBa/DdbiwK85GsEK+j5AmVNowM0UkL+D+QNB7JdXQYROE8GWTCZBiq0nwBC1Rz+lrJZAB0NdQZgML8FQQ1hBJwvsQGJNm7+cvP2//IIaweWRQcAn6C9BA2YQwaX0DUHZHmRBp3jzwK1ohkCe/KtAKcfwQMbLmD9uh8DAhTccwOo9wL9ZtqTAp5AIwczkw0AbGYLAedgFwV+eAUCZOnS/K8mvvrWKg0H3W8FA6OAhwXGlksDesrfAvEITwdwvSsDBwDRBfi8cQb+yu0BUSrVA4jogwQ==
|
||||
学士的墨杯,uG9xQAA3ysCjGp7AAaFeQBMQv0BYlVe/tqWxwJ8uvcDJmM1A1vG1wOYaIMHaSf+/hL+bwN09rcDRrwJB5LQ0wCOQyr+iU6hAUggjQS712sBK7MJAuKxxQAA/ljlmJCi/9gdIQZiOtMCbmAFBMT+xQFNJ976/nba+XefYwONarT9v7+1AbeHKwCgFq0DIvhlBrN6owGaDQcAN+m1AKCOiQDMwhsBsjg/BHX3lwPVnPj+EnXXAajHUwPAxFEBQvtpA6XIewaAAzL9xZUw/OnshQNpfqEDVCgZB1ytWwNJzOEAHoVfAqpq0wOTTmb9+QepA13bGQFQeoMCj4VxAp6a3wA==
|
||||
海祗之冠,+JStQJ1G9sCW4WnAeLMQQFCBy8A+kKHAcInFwCAR9MC/+vlA7YUAwXJWPsE/vHNA7eXNwMhk5MBlIc6/FLz/vxBMpj4MHa5AVHA3QUTfv8C61uNAWZ6xwLEp3b9OEf+8OnJDQR6p0MDYzQ9BrTbEQCBXNsB3FDTAw8UFwbmFbMDEwxFBpQQRwQYrq0CkvTJBkMjFwFG/i0AmNC1Aq8m+QEdcPMFAzTLBn/CwQOhWc8BV5LDAJap9wOzyskBMwcu/kkEtwSl/Rj9e+oO/FhM6QGvd1kDXeBNBx5iiwOXWvL97yVLACFbVv7RHxT8GXwZB/6noQBnMWMBDOpdAzqoDwQ==
|
||||
奇迹耳坠,QvMjPzvZncAlsiXAKoKPQOMsiEDQLV0/Cw9cwDOMecAK6ZlA0nuTwHHVAsEqz2LAaXCxwECQSsBSIphAiYfdwIf+6sDa/n1ApYAKQdC1j8DMZ3xAFD9gQMqcqj+H4YS/RtZuQVg8hMD9Ps9AamWMQJNP5j24XeE/B2uxwKrGwkCprbZA+3qkwKjtbkDOePRADhyKwAH+lMDQvNc/P9VHQCwzn7+SLejAc5uDwdZuJkD8zm6/IXrWwC/1lb6gsRZBH1QmwciQYcDQx0ZAm3aOQLUQhEBCFzVBYNEYwGP4kz8AX5rA0aj0wMr4LcANoLRAxceYQMZGoMCaSvQ/tLuBwA==
|
||||
教官的怀表,3gpYQGZ90MBFljLAaAlnQPT1Oz998z3A4o+QwNDZzcBqEr1AlCzmwLB9GcHeNX/AakegwDAPncByH8ZAPcC/wFA7hsB9TFJA7x8dQejZAMGmh7tAMMAzQHQl7j8y0s48KC+QQddnu8DuUgFBB52XQGkagr+t2A1A5cLUwJbwrb92HdpAvxXWwK5yhkDu9xFBrhyjwCrvd8DMmG1AUHhNQH+Rmj+lc3bBiNc1wdkqk0At6tq//T4SwOB3BD/Mh48/EyFRwQjnMcDroXpAQTDqQKzhSUGIXzlBTo4rQESKhT/atgPBP/w3wQQgPcA0/PFAKT23QLnpz78ilqBAOeacwA==
|
||||
守护之血,sgpMvwhE4cA2eoTA+HvFvuTvm0CTDV3AoCK8wGSlgsCLXcVABbiowBghJ8G0d7O/WDD/wGPDw8BojSJAgdfswG2W2MCCw+lAKyglQfI+ccCQochAZPJYvqCqbb5KWHQ/PhFPQYR1zMAV7AxBVx/bQFRcGEBSpXc+JvbcwKcxxL+szfpAEUCnwCd4y0AMiBtBTh2/wKASLMCuCWdAlL+KQC86WMApZ7nAkoUHwfBcFEDroR/Aq+mXwOlEOkCsEGJAQ5EFwcDUgD419mk/Sg5OPjm/nEDHlmtAdxiswC6mtr4EagnA5qihwIT1A8DHhfNAY7reQKPL2j9DOZpAiVmzwA==
|
||||
逐光之石,4E2sQAQtBcH8j4DAd8pYP6ckF8A4vNTALJnPwCG6+8Cyo+9ARPsEwU4kMMEnrpO/OOjNwIodwsA0/EVAihdzwDJ4KMDa7JVAcsEuQcYSB8Hcv/hAZ16VvzAxtT3QyCU/OJxzQcQp9sC2QhNBkAq8QCK4B8A6Ywg+WEX7wJT0ND/lxABB9nD/wLIuqkBkhylBgiXSwGlN0z5p8OA/T0WQQJLWCsHKqVLBLkKXv9hmDj8kNTXAJDF+vvu5H0CX+/G/+R82wYolWMCooABAKsm4QNUUUkGUfiVByCKNPzsJ67/Gq9PAHuXywGv9zL+oWgpBbFTtQNmWsb/qU8JA4YzPwA==
|
||||
果果软糖,sSaawCTDgMA08aQ/7WuLv9JFAUDg9IRANwI+wInrLMB9lGNAKWNGwBTj8sBmurK/i5HkwLawjL+1ujvAqhMFweXY1L8kPXtAWPjsQKvruD6CXSJABgAzQNBJyD9Z/vC/7sSRQaXfgsBiPJJA34J3QFRS/0BzlE9AJhK5wDDtIkAS9r9AwKgXwBmJzD+mNOFAxPyOwFYjosCyGZM/QjIuP6BuO8B/rWrATYIIweghY7/lQrM+q9XZwJhSPD/N5JJABftxwVFhND89gXhAJIDAQBu8OkChBBpB0Ad4wPsxkkBMjInAKrLkwMHiNsBwmLFAeiyZQFPpDL+Avvq/8KXGvw==
|
||||
雷鸟的怜悯,QSPRQCi1FMHpSq/AIo9DQKXS9j4rkAbBxM7iwBq0AMFtqvhAoDMVwUyXSsHjF3O/q1e8wJ2mC8HruSxAZoRPwKnZ+sBQCq1A8AxBQTOUB8FH+RBBCV6+vxgXij6STD1Ar9aBQaziAMELOi5By2fOQKp/asCVnQTAuDcFwY6oPz+7/gpBRb8QwY4syUApxz1BlWLAwLJlnL+byjpAmmvWQJ3kwsA8zjHBoBNDvnuTZMDX+4/Azc0Jwa5blkBIDWc/CwsZwak/SD7mOla+O8mLP5Z5WkGQiklBXnxKwcDOTcC3DazA6ngDwaIHgb+p9hBBOsb8QOeuEsBf+61A2GIIwQ==
|
||||
唤雷的头冠,GdtJv2/7XcC8oLy/s/JPQJo8ej+00zZAMHsEwKkOW8DpiXlApoJWwL5V98Bsd+O8IiJkwKoCF8Ar7oFA+XOPwESZsD7E91xAiNYCQdV47L8yIRFAUBMEQGpaxT/TaZq/pZswQZFGRsCGS6ZAildCQLIqUkAdNjw/VzmlwLTcz7+8C7dAvG2TwAD/QkAcuOdABthZwPrMWL/0c2w/ffo8QEnXisDKjMfA7kdgwDpxvb7RbRLAsm8gwRxkrz+eV6pAsV0mwVTtNz9YEcE/dsZtQIFIBEADiiRBksOPwNDa1z/e74DAzrIhwKwx2L/u3Z9AaxxnQCuceMCegoI/KEV6wA==
|
||||
测绘员蛋堡,4Khmvi/CXsDwApq/Jj1zv6I1jkD3SytAm2LvvyDdVsB5koNABExowGQ//sCJWAbAq/mJwAsjGsCGsw5B3IvMwJterz985INArGMIQc3GDsC5NCpAmBh/QBpHpD8EcxXAIqFbQctDWcAGQLRAXGeDQDiWvEAyzOk/O5uqwMwlFMGQt8FAWvpmwF3EVEBH4ftAOLdrwIgPosBgyug/9zAGQE6mAED0+wTBOkv2wEe/4UA64qK/qOVSwLL9GD6KjhdABaswwcfeQkA5kCJAtrLEQBsMfkDNt8BAXFYXQL5vfkDh2KHAwKiPwPJSCsAGFbhA6LeBQC0thD+WP6VAmck8wA==
|
||||
摇·滚·鸡!,oKdJwB2mA8AAahw/f+F0PmDZeUCm9FNAV2xJvmhl97+NphtAsvoZwCKNw8DIoifAm+RQwJTJTr/UScNAH8PbwL7sWL/x1wJAfR3ZQFNZmD5vEhE/Ru5EQPmmC0DGQku/uMRcQeKIJcB8hm1Ad/EdQLzKBkGf3khAkt+EwMGFr8BCD4xAlWQSwEGE5j+MiL1AFeIywMZtysDZAOS+zio8PxQh8T51j9HA2cUVwebYrUAxUqM+/9URwDTxW79QaMO+doc3wSacHz/D92VA1W/ZQG36kkAlMuxA4iRvQBrxYkD0ddPASwLWwPSBP8C3S4xArrQwQBcbDkAov11A/FzYvw==
|
||||
冒险家的经验,cqdmwLZECcAQ21W/wPAbwBL83L7gIZNAMfbrv5BX2b8B5FVAahGBvyIF9sDUbUpAJ6WbwLz+mL8aPSrAukt+wL73nkD+qqJAfE/+QMvvnz8X9iw/KG8CwCb4wL9GEDXA5QOJQB87D8C9aX1AB3FwQM7kf0A/rIi+TZStwDpr9z9E0MJA3IVdwG3nVUCTiuNAGgaFwBaRKEDvN4w+F8ssQPb2RcF2IuS/6eOYPyjKO8AGvD3AnabkwAZRHEAuorRAV8UAwU9YTD+Kxzo9zcvtP200/cCHwrFAla3+vwb1VUBwsXI/6NZUQFfNvz9e1ZBAKa1mQLORZ7+J2Nm/uS9cwA==
|
||||
「蒙德往事」,WD+GwB8av79bS+8+3dDLwLc6nECBAHBADiEyv34ewb9d9ChAvKKsv1mO4sDevsC/E72HwBlF3L6A5x1AP4HTwK5G2UA1hGBAGoH0QDVuGz8Y9/09oDsKQOiqCz93y4+/LoUAQaUZFMCiuXlAhhhTQLFq+UCJQR1Aj7CVwEr7gb9r/plA64YawK4hOEAOg9FADStHwKIncsDDueC9zQiJP9rNpMAVDKXA1GJewJadLUD6w1y/1d8xwFE8sT5kv7C/91UwwXznCkD8a0BAKWzVQEXI078jaI9AZrTwQF7npUCKsxjA2YVRwNSA079OWotAjjRHQGiAtUBPDrM/WkL+vw==
|
||||
烤肉卷,9GdDv0/gE8CbdbQ/c8olwFLegkBAwOk9kGVpPNrz7L8ojug/8MFPwBwPwcDOA2XAzHcWwO5c6r+ZF5JARD7KwMQC1r4xG4M/t0PVQDVzgr+2zbA/M8nxPzmBGEAJk4A/OCSKQehmGMBlBH9AyTj7P88uukCiyU9ALp9rwF+9HcGs6VtA9+4swJJD4j+IQ7VA7DcDwCC3BMEg+h4/kHfLPhTuAEDDWifBObiGv92kjEBUvwg/0gTCvy8d6b76/8jA67tmwSy/V0ARXJNAUx7mQEuyAUHpBitBEp80QMpiQkCeEPrAXb8IwQi9z79Af4BAeG4jQKG+m0BVUbFAV8zhvw==
|
||||
气泡酸莓汁,nrg6wACvKcBC5Ya/cOi3wI3nOUCSehpANqEAv5RKI8BbkFFA5cz4vwirAMEB1Yw+5Q5XwA9/2r/9aINALkeDwCSi6r6dcldARY0KQSe4gr9MzoI/ad8OvwjyMT8ytIA8PRLiQN6AIMDDiJJA49trQI27a0CWqI2/fmaPwFF+jEDMKaFAJZyKwB1lc0AWMtpA6DxAwPRjmL7c7Je/afohQPEKL8FVO7jATYfbPWy4HT5MzOe/SmVDwJwKhz+g4Jc/EdnqwPOyhUCIdog/tgW+QMXdPj8h3ghAbwkOwGQrsz7efPG/AkufwOyCrb8dWItAJO9TQE26DUCHlu4/IZA4wA==
|
||||
华饰之兜,o9Thv3ZAncDjHQLAT1XZvx26oL7gb7M+VPt3wDj2gcBPR6VAgjCDwGOaHcERzvE+svLHwJ09dMCI3ZM/wRPpwG8uBj4W27JAOEseQVHULMCxDoFAAlDPvkBfgr6NsoO/Sd4vQWHlmsBhbNpAPZKgQGxDUECNa1k/du/PwJDpOT/sd/FAJQCowHcHlkCDKhFBvs+gwFgVNz8pJz1ApdpYQB7dCcHBqATBX2/SwA2TAL9uRTHAauu8wIkwJEAWHaZAG2giwR9zTb7XptE/mzGKQM/FEUB9C5tAVFudv/LJAkDSOy/A1CxPwNzDzL++39RA6musQKtlT7/9oz9A3maZwA==
|
||||
祭雷礼冠,mga+QIWgFcF/fJnAgKRDQAMd18DSVvzAKxvuwMvpE8E2pxJB4P0fwV1wWsEQhwdAg4npwAucCcGO1Ny/G/kxwL3Bv8D9mLdAUzRTQS/YC8FD8AdBcDapwGSPhb+wkYw/w+hfQbtwAcGYxjBBfVDeQPVC28Bx3CXATykZwTCqoECVVB1BQ4M1wWRw1UC+RkxBNLLtwOgbhUDkLBRAGhPpQB3DT8FduEvBrFk7vyB+PsCC0bbA2T1owMCipkAOvcW/vbErwZalbsD3yIO+ucCBQLvzLkE2OC1BsoeMwOnEqsDZbo3ADNLrwJwBmL1tpBtBZxEJQb1BI8ArV0lAsbESwQ==
|
||||
奇迹之沙,vIFiQLfp4sBZkoXACYn+PyDbPT8G8EbAokquwCyy28DXbOhAp57WwN7HMcEQshHAcYXlwBmszcCpzj5APNaewNd7q8DkqZ9Alzg0QfI3CcFactZAPgWbPpBtAL/GaW6/xfdbQeaU18B0whVBvy3NQAtMTsBJ8sI+P63nwGBPRUBbkvRA1xr2wBUBp0CNeSRBzwvKwG4KXL/3ypBAfH2qQK4X48CUtl/B5BdvwehJQkCCgmDA1JmRwPN8C0BQy9FAlxwRwYJRZ8A2Md4/K4qrQLFE5EB92rlAlEKgP67/EUCuCkPAdoruwGaCt7+QCP9A83nYQFOSV8AfhsFAa9TGwA==
|
||||
永恒的信仰,Ly6rwENQLsAbFh1AVCJcP9olGb7gHLBANBv1PnlHdcB7fxpA0jF0wO9Z3sDDp6bAVHA8wEBbVj/V5LFAyMkdwZF6uMAGohc/UYD0QKXs0r5MU1U+WiydQBV+okAlOJK/aHSYQUkXMcB5KkFAoacaQMh7D0HqG4VAy5WLwFRggj5lvqFAOyZGwJq7fD445stA1ZRLwCEN4MD5hgLAtlQfv5DSQb4G/CrB+vkcwQxD4kBSTAJAEm3vvxsdbsDA69w+NSpUwdChp75f/slATh1NQbsg/kBwA1hBMDuAQJh4HUAcNzTBk3aDwVUK1sBgqqVAcwA6QLVTKj9GTApAZtMUvw==
|
||||
黑铜号角,eikSwN7uQsDYcvC/pLGUP8RVKEC6GolABagQwA8uIsDk74NAwP4JwI/LA8HQ8oM/qcyIwJpSAsBAptU/lP59wHTOG0BBkp1AU3sGQYWYl77kv+U/F555P/z7mjwAERDA+SDeQGOcL8BiMZ1AVPl7QFDPcUDlLsk9RyKywLAsob9OPcpAT+F9wM5VakB+4vpAs8Z6wKDjh72oB/M/ULNAQEfiqsCwMDzAXI63wMOUCT/QMj3Aij7mwCyA5D8uf9BA7qj9wHWv4j+JMN4+ilkEQA3AjsCXI5hA1IJKwFzJbUCcjFa/fElbvUSSqj0mDahALzWDQGGu/b+GSJy+RfNzwA==
|
||||
鲜鱼炖萝卜,6IOvv0gWzcChsK4/iXQHvrOLLMFeiJjA3nlPwOTGvMB7lMpAbgTcwONwLMEu1xZAlU76wGk2kMCekA3BY82+wN+hPMBo3EFAougaQYslSb4gn5NADMgCwQDVXD8zFwdAnJ1qQS6G0cCinN9AX9qbQDK2Y0A9GEI//37ywNlJvMCwe/5AR07XwFs+CkAU1CVBs6jDwA13kEBxeqi+/cpBQHeGvsCuLvfAoC5VO2T0yD8SRvK/zKrBvx1QVkCCMzrBnZApwWRphj5HFQNASTa3QOlPPkGFJu9A4BpQvxKlFcAdH/vATC3kwPg4E8A/YQJB52TSQPJCk0DpL21AqtexwA==
|
||||
勇士的壮行,eEFnP+RJcsAtihLAR1BKPsA2v0DEU4I/n0AdwB2FWsCfKI9Agfp/wDkqDcEGeRrADdSIwMkngcBs6ttA/Yq4wD1Iw78Cs49AWecZQQMWjMBaYzhASs1EQPwMBz+S9Xq/iPQ9QVN1bcBeZdxA26qSQE7jAj/95Go/1qS3wKter79ky8BAt/GrwBrul0B7cAZBZXaEwLB8lsCtsC9AOsl6QApOF79IUBDBBS0xwUJ5mkBD+iPAN4SbwJGUhz8Eg2lAZs8rwQgTiTygkgRAQI6RQLnf/z8SGMdAXrg8QFRaGUDzpU3A4h+ZwJxil78MVLlA3NqNQLXLPj/X41VAJd6LwA==
|
||||
地脉的新芽,SIHWv5xrtMD3Ow/ADcaMQGWw0kBEEiRAPtR8wFO4ccAJRZtAdbePwE8VDsGhhhLANly+wCaSbsCtaAFBh3DowHHbUcA6vKpA0L8OQVXlNMBb/Y5AuvSrQBuDF0AmeGC/PmyAQc8oo8AvJN1AX0ynQDxdjEDJioQ/n7DGwGLajb8+RNpAqop/wEb8ekCn2AhBxi2IwHxQlMCj9n1AzYw8QFR5XkCROpDAJOtrwa+LFkB1Pe6/NT4hwfsByT9hTwJBfuQowfB8BECmL+0/K9g6QAY3ykA1CPtASUgMwQYBIkAiLJrAMoviwPB+fMBxOdBAnV+tQI5RNcCUygc+dWB2wA==
|
||||
纷争的前宴,ytAHQKmy28C0NF3AXaBFwA1Y6r2d0MHAyo2qwPWLpcDEX8RASfzOwE/nO8H+mi2+AUC8wPPM18C6Juu+U2nSwKuRZsBPX7ZA25A+QTWwm8CS285AAkeCwMPQi78TJco/hkp8QVD9s8BL6A9BRcinQEwpYj/bMmW/Y+fswLjpdcBLGQJBoo3cwD2DxUCQ8C9BXMehwKxlmb7ScS5ASIacQAH+/MCeghTB67WVPxAl/b8UqmbAoRXtwH3chUCANrg/7SVDwR74XECtNCo/as9tQFGS0UB5iShBsGriwIcCo78nWEbAEuGlwO5Beb7Or/RAf+jLQNX1GD9Xn89AYMnqwA==
|
||||
流浪者的经验,xhGewLLUMMB8KpU9sq1hPyKRu0AEr5g/Plssvzyrrr+ojhVA/LI3wG1D18BvL6zA6ASGwAYfDsC4HJRAVWYvwZ1vqcDurjZA/h/rQBsQmL80hK4/DJRKQMkDYkCSuAdA9GNwQZIjSMCynKhAuEhLQOmfzEAUKohAfSVzwJGROD4rSYFAjmMwwK9KQ0BEvrxAZogWwMzE2cCsH9U/AHpUP/WQ9kBGwOPA4hyiwR8S1UAiPk0/FeW8wBiJfr/4WHY/vVowwfIMsj1Sw61AAc+4QJpV3EBwD9tANicVQFW75T/glNDAKbs4wfc/ucB+pI1AkoJGQKamkEBa6UNAx6EXwA==
|
||||
泡泡桔,U+fjvwGOw78BmIC/dXlswHenBECJLTpAkMRQPNtpEsDPmR5ACufdv4YUwsAfjvq+b7qkv23dvb4Cks5AHPo0wGjGhEDtRyNAFVffQLlKtr6Sppk+ZyOkP9xDLD/GfQa/GvLXQHGAvr9pP0BA8UcRQHxAmkBiwSw+nTJswIOmkcBKBo5A+chOwBgqTUDVRrNAUtEPwCPo0L+2awXAaavDP4Frn8AXS67A9TPaQAAETUBWrq6/qMUGP3jwIL3s/+G/+Wr1wNHNLEC2T9E/0xy0QBQIPD6hN7RAvNVDQPuGtD/tA5PAT0l6wFQIrb8ZyHJA9RgLQH/0MUBVsak/ytgGwA==
|
||||
混沌炉心,FgUxwILUCsApvIq+yQ6lPwUbXUBS7YhA7vw+v1Az2L81ujRAImAPwHXt4MC4dMi/QiNfwITw6L8jcbhAYTDywMwwcr845VlA8Nv3QGsOVL8W8IE/kB1HQGQPyz/Xa/y/IP81QWi6CcBhZZJAS7NOQIuum0AtUQhAROSLwIS6t8BUzZ5AaT9XwCrQL0CMZ89A00E0wGQcfMAq3xxA9EXjPyxPzL7CjvHAzrwzwat8fEDKE6O/GyXrwHI+Oz5ApMZAM1UhwUz/C0Bg7i1AmXaiQGfpTT/Oos9AxJHQvsQ/S0Aj8mnAuR5mwKQZ879G34ZAKbEwQBWE0b9VEENAr1Y4wA==
|
||||
凉拌薄荷,7KZRwOPe2L+iDlpAdtQ9v+kEpb50fuo/Dvb/vRNuJL+MaDI/0yEMwFG2jMCgeo6/a1qEwKHDy75aLiHAWQrswC+flMDmV0k+anWWQDE+CkC+7LM+firTPq5V6D+U4K29I9SgQYvvpL/W//I/IisMP/B3/0DiuSxAifoqwJ6TN7/Obi1Avfqhv/MIYr+bemJAIwHEv/AJtcCHSOi/fGkmv+Dk4sA+JuPALVy3P8L6XsAc4LM/bNO+wKTUhz2gqoE/ktJ7wYYK4j+WbHVARYzSQCWR2UDCPmlBiJ6pwOh6KEAciLLALvT1wPwL/r/Rvg1ADVK9PxWM6b8hDdI/zCEkvw==
|
||||
千层酥酥,zu9zwNAwKsDdHDRA8zbJv+aiQT8S4G5AJ+Oov8IU+r9JcQ1AwyMnwGsRxMDf02a/+gq1wMk+0L7lsE6/ncbuwDdHGD7Njg5Ad1/FQK1znT8Wrrk/8/W1P1Zxkz8V6ri/bPyNQerzNMDMZzVAgIwqQPMZCUGX/kNAn9iTwC/ZMsAbqZlAxnqVv9Ei2726w71A0IVbwHrBuMDBQSG+/CIAvcjBlsCHNcnAT0Nwv4G4Mb+QXDM/ptjAwDn9iz4YYCk/oxZpwUhPjT+mq29AdizNQDPuh0DZC0NB9Y69v83krECjKrrAuMaZwAS/E8Cuq41AJZtZQFFEAT11CaQ/Q6+Kvw==
|
||||
烛伞蘑菇,6Ufbvwp4T8Dryug+VKFUPzFQMT8jXDtAZ7wAvukyZcBvbVFAfp51wLjx98D4jTXANy0swJDMBcB/K+JAl1LkwJdTjMCljK0/FZoGQS8d1r9KUtA/LPlRQPfpJkC6aQe+Yg13QeNFK8DNF4RAgFpCQI7IuUDciOw/pX2IwAxO+cCIw6dAda1rwM3N1z8mCudA4PRHwJBYoMD5oNC+Ste8P4ImLL9gjzXByiXTwFPT5ECGGxa/HebeP+yzL7+ga+w+oronwUQ+KkAvA0tA0UYPQTCl8UDdVp9APCo3QEjXM0DuAebA2tgjwabSfsCHdqxAguBLQAbRjr4R4MVAGYccwA==
|
||||
至高的智慧(生活),qOMLwMcHkb/oqy9A3Wy4v1h65D8w9CM/B6H0P7v0fL+wehQ/fukIwICOvMAwYRnAa/G4v8zC1r9MTIhAMJUVwe6W07+mmEg+MaTfQNH1fD8FXUK/HujUPuqQ+z9lP44/tCeMQZGn0b8paFVAA+KmP+8TCEHObkdADZxbwNhv6sAfx0lAIUcywJQQnz9lm6xAlwySvybV08AIaBDAZBN+P6bzJ0CPnwfBnwHgwA0KmkBy8Qo+5TIswCdks76ibajAY6lnwUwZCkDlHYdAyEQHQQBq+EBJiytBpDqdQNOKgz+8hwLBLODqwHxbGsD6LlhANa+3P+bepkBfM8VA1fwlwA==
|
||||
没有未来菜,9FwLwHTzJMAwNGE/FHgqPxSuqEAwVn5AHz+Ev/y/2b9NHDpACBwZwI2z2MCYL42/miabwNkZC8Dl7J1AC6jgwEz34r8MTTVAl2XjQHvd4T4IRu0/lIZTQPxTvD82YUK/oQp4QSpeDcBoSoZAnfs/QMDC6kA0ueY/GxSHwBsAEMFi0p1Azu7ov2v6hj8gTc1A/kMxwMPU1cBEt1U/op7UP8jg1j//p8DAVcbFwPNQMUAcYWS/j60wwTKNiT9E4GRAMtU3wbq8lUDPwhFAor10QLB3c0D9Qe9AkNemwCXMqEAchojAT+KBvyB5GMAZJI5AJrRMQEMbHL8LgapAq/0WwA==
|
||||
一捧绿野,7ExgwAT8PsDMqChA0AjtPnrn5j9N6WBAK2fbv0wFBcCaMfU/e2Q4wLvgmcBAX0LA4CzBwMFfo762svs+Fyn3wJPRSsDTcpk/haugQFBtQDsw1us/htSVQH9nJkCRvvK/9DCuQcQoTMAo9D5AbN/4P7VaB0HarXZAPylYwDjH9z4g8WVA1nCNv/fkTb809oNA96kqwEuO48BoJHI/8qZAvzWOhsCbrwbBoNumwGXeDcCIR/c/az/1wFhoDr+/1bpAybB+weS/Gj+noJZAkd/1QHbyEUEC3F1Bt3O7wNOdqEBUj8jAiQ0Xwd8Pb8BrGVVAYcMvQKq9hMC9AIQ/6CoEvQ==
|
||||
琦色灵彩之羽,qYSZv6O6WsDmIh/AJC75PuBZEECmsARAzdYYwMslLsBxR4VAv7IzwCF4BsG3Gkm/cS6AwEAvL8ALyAVApyqtwEX8EMCcyIFA4hoLQcFQFsArhyNAP+lZP1xzSz9FAa6/oOcDQVFgPsBJMblAKdl5QLmj9j+sdYU/wU+swHb5jr8bWbtANTGRwDVoa0AAKvtARbh2wJFLo790XSJAA4sxQECe2MAu6/TAFsDkwHVQWz9IZw3A26chwYVLmT8UyfVAZHbowM9qnD9WocI/C9cuQMCfSr9tddZAhYuowLx5G0DpKAfA7pa8v1D8Lr8iKKpAsUyAQM7MTsAUQhZAIhqAwA==
|
||||
饰金胸花,1CcdwH3KMcCFSLi+sa5XvkEvLz+eUitA9zSYv5HGR8BKAVtAX1NMwGRD7cBlEizAvdCCwK3hw7+C6k9AZ27nwISWrr6PCyZAXzYAQedKB8D4Es4/DlQXQMBu1T83ENC/QXJBQd5xSsCNdpRAj7VQQNx2g0AYd1JAk7iZwEy9rb8TbKdAvQqCwObvJUCsm9tA/ohqwM4ZN8DqpMU/CTPFPzZ0HsBz/RbBzroewUFVhkD+FBW/5zB4wJhoKr/GKRZAYfUswWmhxb/3doNAFanpQH2wZUBTYw1BBDeYQAbXREAl47TAmm3ywPqkLMD6xqJA/cVeQItKCb/YwVBAgVAxwA==
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask;
|
||||
@@ -20,3 +20,13 @@ public interface ISoloTask
|
||||
/// <returns></returns>
|
||||
Task Start(CancellationToken ct);
|
||||
}
|
||||
|
||||
public interface ISoloTask<T> : ISoloTask
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动独立任务
|
||||
/// </summary>
|
||||
/// <param name="ct">取消Token</param>
|
||||
/// <returns></returns>
|
||||
new Task<T> Start(CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 独立任务参数基类
|
||||
/// </summary>
|
||||
public class BaseTaskParam
|
||||
public abstract class BaseTaskParam<T> where T : class
|
||||
{
|
||||
public CultureInfo? GameCultureInfo { get; set; }
|
||||
public BaseTaskParam()
|
||||
/// <summary>
|
||||
/// 游戏语言CultureInfo
|
||||
/// </summary>
|
||||
public CultureInfo GameCultureInfo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 多语言StringLocalizer
|
||||
/// 用于读取与 <typeparamref name="T"/> 同名的.resx文件中的多语言信息
|
||||
/// </summary>
|
||||
public IStringLocalizer<T> StringLocalizer { get; private set; }
|
||||
public BaseTaskParam(CultureInfo? gameCultureInfo, IStringLocalizer<T>? stringLocalizer)
|
||||
{
|
||||
}
|
||||
public BaseTaskParam(CultureInfo? gameCultureInfo)
|
||||
{
|
||||
GameCultureInfo = gameCultureInfo;
|
||||
GameCultureInfo = gameCultureInfo ?? new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
|
||||
StringLocalizer = stringLocalizer ?? App.GetService<IStringLocalizer<T>>() ?? throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using Fischless.WindowsInput;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
public class ArtifactSetFilterScreen : IAsyncEnumerable<ImageRegion>
|
||||
{
|
||||
private readonly GridParams @params;
|
||||
private readonly CancellationToken ct;
|
||||
private readonly ILogger logger;
|
||||
private readonly InputSimulator input = Simulation.SendInput;
|
||||
|
||||
/// <summary>
|
||||
/// 对圣遗物套装筛选界面的操作封装类
|
||||
/// 直接对此类对象进行遍历即可获取所有项
|
||||
/// 每次的截图是上次滚动后的,如果实时性要求高,应每次迭代自行截图
|
||||
/// 在末页可能重复返回GridItem,须自行处理
|
||||
/// </summary>
|
||||
/// <param name="@params"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="ct"></param>
|
||||
public ArtifactSetFilterScreen(GridParams @params, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
this.ct = ct;
|
||||
this.logger = logger;
|
||||
this.@params = @params;
|
||||
}
|
||||
public IAsyncEnumerator<ImageRegion> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GridEnumerator(@params.Roi, @params.Columns, new GridScroller(@params, logger, input, ct), ct);
|
||||
}
|
||||
|
||||
public class GridEnumerator : IAsyncEnumerator<ImageRegion>
|
||||
{
|
||||
private readonly Rect roi;
|
||||
private readonly CancellationToken ct;
|
||||
private readonly int columns;
|
||||
private readonly GridScroller gridScroller;
|
||||
|
||||
private Queue<ImageRegion> imageRegions;
|
||||
private ImageRegion? current;
|
||||
ImageRegion IAsyncEnumerator<ImageRegion>.Current => current ?? throw new NullReferenceException();
|
||||
|
||||
internal GridEnumerator(Rect roi, int columns, GridScroller gridScroller, CancellationToken ct)
|
||||
{
|
||||
this.roi = roi;
|
||||
this.ct = ct;
|
||||
this.columns = columns;
|
||||
this.gridScroller = gridScroller;
|
||||
|
||||
this.imageRegions = new Queue<ImageRegion>();
|
||||
}
|
||||
|
||||
public async ValueTask<bool> MoveNextAsync()
|
||||
{
|
||||
if (current == null || this.imageRegions.Count < 1)
|
||||
{
|
||||
if (current != null)
|
||||
{
|
||||
using var ra4 = TaskControl.CaptureToRectArea();
|
||||
ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2);
|
||||
await TaskControl.Delay(300, ct);
|
||||
|
||||
if (!await this.gridScroller.TryVerticalScollDown(GetGridItems))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
using ImageRegion ra = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion imageRegion = ra.DeriveCrop(this.roi);
|
||||
IEnumerable<ImageRegion> gridItems = GetGridItems(imageRegion.SrcMat, this.columns).Select(imageRegion.DeriveCrop);
|
||||
|
||||
this.imageRegions = new Queue<ImageRegion>(gridItems);
|
||||
}
|
||||
this.current = this.imageRegions.Dequeue();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 顺便把顺序也从左到右从上到下排好了
|
||||
/// </summary>
|
||||
/// <param name="src"></param>
|
||||
/// <param name="columns"></param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<Rect> GetGridItems(Mat src, int columns)
|
||||
{
|
||||
using Mat grey = src.CvtColor(ColorConversionCodes.BGR2GRAY);
|
||||
using Mat canny = grey.Canny(20, 40);
|
||||
|
||||
using Mat closeKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
|
||||
using Mat close = canny.MorphologyEx(MorphTypes.Close, closeKernel);
|
||||
|
||||
//Cv2.ImShow("grey", grey);
|
||||
//Cv2.ImShow("close", close);
|
||||
//Cv2.WaitKey();
|
||||
|
||||
Cv2.FindContours(close, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null);
|
||||
|
||||
contours = contours
|
||||
.Where(c =>
|
||||
{
|
||||
Rect r = Cv2.BoundingRect(c);
|
||||
if (r.Width < src.Width / columns * 0.66) // 剔除太小的
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (r.Height == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Math.Abs((float)r.Width / r.Height - 8.63) < 0.4; // 按形状筛选
|
||||
}).ToArray();
|
||||
|
||||
IEnumerable<Rect> boxes = contours.Select(Cv2.BoundingRect);
|
||||
|
||||
double avgWidth = boxes.Average(r => r.Width);
|
||||
double avgHeight = boxes.Average(r => r.Height);
|
||||
|
||||
List<Cell> cells = ClusterToCells(boxes, 10).ToList();
|
||||
|
||||
double avgColSpacing;
|
||||
double avgRowSpace;
|
||||
{
|
||||
int count = 0;
|
||||
int sum = 0;
|
||||
foreach (var row in cells.GroupBy(t => t.RowNum))
|
||||
{
|
||||
for (int i = 0; i < row.Max(r => r.ColNum); i++)
|
||||
{
|
||||
var x1 = row.SingleOrDefault(r => r.ColNum == i);
|
||||
var x2 = row.SingleOrDefault(r => r.ColNum == i + 1);
|
||||
if (x1 == null || x2 == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sum += x2.Rect.X - x1.Rect.X - x1.Rect.Width;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
avgColSpacing = Math.Round(((double)sum) / count, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
{
|
||||
int count = 0;
|
||||
int sum = 0;
|
||||
foreach (var col in cells.GroupBy(t => t.ColNum))
|
||||
{
|
||||
for (int i = 0; i < col.Max(r => r.RowNum); i++)
|
||||
{
|
||||
var y1 = col.SingleOrDefault(r => r.RowNum == i);
|
||||
var y2 = col.SingleOrDefault(r => r.RowNum == i + 1);
|
||||
if (y1 == null || y2 == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sum += y2.Rect.Y - y1.Rect.Y - y1.Rect.Height;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
avgRowSpace = Math.Round(((double)sum) / count, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
int avgLeft = (int)Math.Round(cells.Average(c => c.Rect.X - (avgWidth + avgColSpacing) * c.ColNum), MidpointRounding.AwayFromZero);
|
||||
int avgTop = (int)Math.Round(cells.Average(c => c.Rect.Y - (avgHeight + avgRowSpace) * c.RowNum), MidpointRounding.AwayFromZero);
|
||||
|
||||
// 遍历方阵,补上缺的Cell
|
||||
for (int i = 0; i < cells.Max(r => r.ColNum) + 1; i++)
|
||||
{
|
||||
for (int j = 0; j < cells.Max(r => r.RowNum) + 1; j++)
|
||||
{
|
||||
if (cells.Any(c => c.ColNum == i && c.RowNum == j))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int x = (int)Math.Round(avgLeft + (avgWidth + avgColSpacing) * i, MidpointRounding.AwayFromZero);
|
||||
int y = (int)Math.Round(avgTop + (avgHeight + avgRowSpace) * j, MidpointRounding.AwayFromZero);
|
||||
int width = (int)Math.Round(avgWidth, MidpointRounding.AwayFromZero);
|
||||
int height = (int)Math.Round(avgHeight, MidpointRounding.AwayFromZero);
|
||||
Cell cell = new Cell(new Rect(x, y, width, height));
|
||||
cell.ColNum = i;
|
||||
cell.RowNum = j;
|
||||
cells.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
return cells.OrderBy(c => c.RowNum).ThenBy(c => c.ColNum).Select(c => c.Rect).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 具有行号列号的单元格
|
||||
/// ColNum和RowNum也是0-based的
|
||||
/// 不仅方便编程,ClusterColsAndRows方法也需要一个引用类型
|
||||
/// </summary>
|
||||
/// <param name="rect"></param>
|
||||
private class Cell(Rect rect)
|
||||
{
|
||||
internal Rect Rect = rect;
|
||||
internal int ColNum;
|
||||
internal int RowNum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把检出的矩形聚簇成类似Excel的单元格集合
|
||||
/// </summary>
|
||||
/// <param name="rects"></param>
|
||||
/// <param name="threshold"></param>
|
||||
/// <returns></returns>
|
||||
private static IEnumerable<Cell> ClusterToCells(IEnumerable<Rect> rects, int threshold)
|
||||
{
|
||||
var result = rects.Select(r => new Cell(r));
|
||||
result = result.ToArray(); // 必需,不然引用会丢掉。。
|
||||
|
||||
var orderByX = result.OrderBy(t => t.Rect.Left).ToArray();
|
||||
int col = 0;
|
||||
int? lastX = null;
|
||||
for (int i = 0; i < orderByX.Length; i++)
|
||||
{
|
||||
if (lastX != null && orderByX[i].Rect.X - lastX > threshold)
|
||||
{
|
||||
col++;
|
||||
}
|
||||
orderByX[i].ColNum = col;
|
||||
lastX = orderByX[i].Rect.X;
|
||||
}
|
||||
|
||||
var orderByY = result.OrderBy(t => t.Rect.Top).ToArray();
|
||||
int row = 0;
|
||||
int? lastY = null;
|
||||
for (int i = 0; i < orderByY.Length; i++)
|
||||
{
|
||||
if (lastY != null && orderByY[i].Rect.Y - lastY > threshold)
|
||||
{
|
||||
row++;
|
||||
}
|
||||
orderByY[i].RowNum = row;
|
||||
lastY = orderByY[i].Rect.Y;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
BetterGenshinImpact/GameTask/Model/GameUI/GridParams.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
public class GridParams
|
||||
{
|
||||
internal Rect Roi { get; private set; }
|
||||
internal int Columns { get; private set; }
|
||||
internal int S1Round { get; private set; }
|
||||
internal int RoundMilliseconds { get; private set; }
|
||||
internal int S2Round { get; private set; }
|
||||
internal double S3Scale { get; private set; }
|
||||
|
||||
public GridParams(Rect roi1080p, int columns, int s1Round, int roundMilliseconds, int s2Round, double s3Scale)
|
||||
{
|
||||
Roi = roi1080p.Multiply(TaskContext.Instance().SystemInfo.AssetScale);
|
||||
Columns = columns;
|
||||
S1Round = s1Round;
|
||||
RoundMilliseconds = roundMilliseconds;
|
||||
S2Round = s2Round;
|
||||
S3Scale = s3Scale;
|
||||
}
|
||||
|
||||
private static readonly GridParams weapons = new GridParams(new Rect(106, 110, 1171, 845), 8, 3, 40, 32, 0.024);
|
||||
|
||||
internal static FrozenDictionary<GridScreenName, GridParams> Templates { get; } = new Dictionary<GridScreenName, GridParams>() {
|
||||
{ GridScreenName.Weapons, weapons },
|
||||
{ GridScreenName.Artifacts, new GridParams(new Rect(106, 162, 1171, 783), 8, 3, 40, 32, 0.024)},
|
||||
{ GridScreenName.CharacterDevelopmentItems, weapons },
|
||||
{ GridScreenName.Food, weapons },
|
||||
{ GridScreenName.Materials, weapons },
|
||||
{ GridScreenName.Gadget, weapons },
|
||||
{ GridScreenName.Quest, weapons },
|
||||
{ GridScreenName.PreciousItems, weapons },
|
||||
{ GridScreenName.Furnishings, weapons },
|
||||
{ GridScreenName.ArtifactSalvage, new GridParams(new Rect(48, 106, 1267, 768), 9, 3, 40, 28, 0.018)}
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using Fischless.WindowsInput;
|
||||
@@ -17,15 +16,10 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
public class GridScreen : IAsyncEnumerable<ImageRegion>
|
||||
{
|
||||
private readonly Rect gridRoi;
|
||||
private readonly GridParams @params;
|
||||
private readonly CancellationToken ct;
|
||||
private readonly ILogger logger;
|
||||
private readonly InputSimulator input = Simulation.SendInput;
|
||||
private readonly int columns;
|
||||
private readonly int s1Round;
|
||||
private readonly int roundMilliseconds;
|
||||
private readonly int s2Round;
|
||||
private readonly double s3Scale;
|
||||
|
||||
/// <summary>
|
||||
/// 对Gird类型界面的操作封装类
|
||||
@@ -33,41 +27,32 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
/// 每次的截图是上次滚动后的,如果实时性要求高,应每次迭代自行截图
|
||||
/// 在末页可能重复返回GridItem,须自行处理
|
||||
/// </summary>
|
||||
/// <param name="gridRoi">Grid所在位置</param>
|
||||
/// <param name="@params"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="ct"></param>
|
||||
public GridScreen(Rect gridRoi, GridScreenParams @params, ILogger logger, CancellationToken ct)
|
||||
public GridScreen(GridParams @params, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
this.gridRoi = gridRoi;
|
||||
this.ct = ct;
|
||||
this.logger = logger;
|
||||
if (@params.Columns < 4)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(@params.Columns));
|
||||
}
|
||||
this.columns = @params.Columns;
|
||||
this.s1Round = @params.S1Round;
|
||||
this.roundMilliseconds = @params.RoundMilliseconds;
|
||||
this.s2Round = @params.S2Round;
|
||||
this.s3Scale = @params.S3Scale;
|
||||
this.@params = @params;
|
||||
}
|
||||
|
||||
public IAsyncEnumerator<ImageRegion> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GridEnumerator(gridRoi, columns, s1Round, roundMilliseconds, s2Round, s3Scale, logger, input, ct);
|
||||
return new GridEnumerator(@params.Roi, @params.Columns, input, new GridScroller(@params, logger, input, ct), ct);
|
||||
}
|
||||
|
||||
public class GridEnumerator : IAsyncEnumerator<ImageRegion>
|
||||
{
|
||||
private readonly Rect roi;
|
||||
private readonly CancellationToken ct;
|
||||
private readonly ILogger logger;
|
||||
private readonly InputSimulator input = Simulation.SendInput;
|
||||
private readonly int columns;
|
||||
private readonly int s1Round;
|
||||
private readonly int roundMilliseconds;
|
||||
private readonly int s2Round;
|
||||
private readonly double s3Scale;
|
||||
private readonly GridScroller gridScroller;
|
||||
|
||||
/// <summary>
|
||||
/// 单次滚动得到的页面
|
||||
@@ -91,59 +76,13 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="ct"></param>
|
||||
public GridEnumerator(Rect roi, int columns, int s1Round, int roundMilliseconds, int s2Round, double s3Scale, ILogger logger, InputSimulator input, CancellationToken ct)
|
||||
internal GridEnumerator(Rect roi, int columns, InputSimulator input, GridScroller gridScroller, CancellationToken ct)
|
||||
{
|
||||
this.roi = roi;
|
||||
this.ct = ct;
|
||||
this.logger = logger;
|
||||
this.input = input;
|
||||
this.columns = columns;
|
||||
this.s1Round = s1Round;
|
||||
this.roundMilliseconds = roundMilliseconds;
|
||||
this.s2Round = s2Round;
|
||||
this.s3Scale = s3Scale;
|
||||
}
|
||||
|
||||
public async Task<bool> TryVerticalScollDown()
|
||||
{
|
||||
using var ra = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion prevGrid = ra.DeriveCrop(roi);
|
||||
|
||||
for (int i = 0; i < this.s1Round; i++)
|
||||
{
|
||||
this.input.Mouse.VerticalScroll(-2);
|
||||
await TaskControl.Delay(this.roundMilliseconds, this.ct);
|
||||
}
|
||||
await TaskControl.Delay(300, this.ct);
|
||||
using var ra2 = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion scrolledGrid = ra2.DeriveCrop(this.roi);
|
||||
|
||||
bool isScrolling = IsScrolling(prevGrid.CacheGreyMat, scrolledGrid.CacheGreyMat, out Point2d shift, logger: this.logger);
|
||||
|
||||
return isScrolling;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否还能继续滚动,如果到底了则只能滚动一丝并很快地回弹
|
||||
/// </summary>
|
||||
/// <param name="prevGray">先前的灰度图</param>
|
||||
/// <param name="nextGray">尝试滚动并等待可能的回弹后的灰度图</param>
|
||||
/// <param name="shift">估计的位移</param>
|
||||
/// <param name="lowerThreshold">低于下限则可能不存在平移</param>
|
||||
/// <param name="upperThreshold">上限用于抵消微小的其他差异,比如高亮图标的呼吸闪烁</param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsScrolling(Mat prevGray, Mat nextGray, out Point2d shift, double lowerThreshold = 0.5, double upperThreshold = 0.95, ILogger? logger = null)
|
||||
{
|
||||
using Mat prev = new Mat();
|
||||
prevGray.ConvertTo(prev, MatType.CV_32FC1);
|
||||
using Mat next = new Mat();
|
||||
nextGray.ConvertTo(next, MatType.CV_32FC1);
|
||||
|
||||
using Mat window = new Mat();
|
||||
shift = Cv2.PhaseCorrelate(prev, next, window, out double response); // 相位相关性
|
||||
//logger?.LogInformation($"response={response:F3}, shift=({shift.X:F2}, {shift.Y:F2})");
|
||||
return response > lowerThreshold && response < upperThreshold;
|
||||
this.gridScroller = gridScroller;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -153,20 +92,51 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
/// <param name="numbers">传入的Y列表</param>
|
||||
/// <param name="threshold"></param>
|
||||
/// <returns>外层是各行从上到下,内层是一行从左到右</returns>
|
||||
static List<List<ImageRegion>> ClusterRows(IEnumerable<ImageRegion> regions, int threshold)
|
||||
public static List<List<T>> ClusterRows<T>(IEnumerable<T> regions, int threshold)
|
||||
{
|
||||
// 先对Y排序,便于聚簇
|
||||
var sortedRegions = regions.OrderBy(r => r.Y).ToList();
|
||||
static int getX(T t)
|
||||
{
|
||||
if (t is ImageRegion imageRegion)
|
||||
{
|
||||
return imageRegion.X;
|
||||
}
|
||||
else if (t is Rect rect)
|
||||
{
|
||||
return rect.X;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
static int getY(T t)
|
||||
{
|
||||
if (t is ImageRegion imageRegion)
|
||||
{
|
||||
return imageRegion.Y;
|
||||
}
|
||||
else if (t is Rect rect)
|
||||
{
|
||||
return rect.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
List<List<ImageRegion>> clusters = new List<List<ImageRegion>>();
|
||||
// 先对Y排序,便于聚簇
|
||||
var sortedRegions = regions.OrderBy(getY).ToList();
|
||||
|
||||
List<List<T>> clusters = new List<List<T>>();
|
||||
|
||||
if (sortedRegions.Count == 0)
|
||||
return clusters;
|
||||
|
||||
// 初始化第一个聚簇
|
||||
List<ImageRegion> currentCluster = new List<ImageRegion> { };
|
||||
List<T> currentCluster = new List<T> { };
|
||||
|
||||
foreach (ImageRegion r in sortedRegions)
|
||||
foreach (T r in sortedRegions)
|
||||
{
|
||||
if (currentCluster.Count <= 0)
|
||||
{
|
||||
@@ -174,23 +144,23 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
continue;
|
||||
}
|
||||
|
||||
ImageRegion lastInCluster = currentCluster.Last();
|
||||
T lastInCluster = currentCluster.Last();
|
||||
|
||||
// 如果当前数字与聚簇中最后一个数字的差值小于阈值,则加入当前聚簇
|
||||
if (r.Y - lastInCluster.Y <= threshold)
|
||||
if (getY(r) - getY(lastInCluster) <= threshold)
|
||||
{
|
||||
currentCluster.Add(r);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 否则,创建一个新的聚簇
|
||||
clusters.Add(currentCluster.OrderBy(r => r.X).ToList());
|
||||
currentCluster = new List<ImageRegion> { r };
|
||||
clusters.Add(currentCluster.OrderBy(getX).ToList());
|
||||
currentCluster = new List<T> { r };
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最后一个聚簇
|
||||
clusters.Add(currentCluster.OrderBy(r => r.X).ToList());
|
||||
clusters.Add(currentCluster.OrderBy(getX).ToList());
|
||||
|
||||
return clusters;
|
||||
}
|
||||
@@ -245,7 +215,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Math.Abs((float)r.Width / r.Height - 0.8) < 0.05; // 按形状筛选
|
||||
return Math.Abs((float)r.Width / r.Height - 0.81) < 0.05; // 按形状筛选
|
||||
}).ToArray();
|
||||
|
||||
IEnumerable<Rect> boxes = contours.Select(Cv2.BoundingRect);
|
||||
@@ -294,9 +264,9 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
/// <summary>
|
||||
/// 背包界面的背景是把打开界面之前的画面进行了模糊+黑白渐变滤镜+左上角水印叠加处理
|
||||
/// 放任五彩斑斓的输入,并且允许点击高亮的话处理起来就复杂了
|
||||
/// 所以这个Alpha版方法留在这里只是想说明:
|
||||
/// <para>所以这个Alpha版方法留在这里只是想说明:
|
||||
/// 越是琢磨算法,就越会发现传统算法的能力是有极限的
|
||||
/// 既然是游戏画面,不如在输入的时候就尽量获得没有噪声的画面
|
||||
/// 既然是游戏画面,不如在输入的时候就尽量获得没有噪声的画面</para>
|
||||
/// </summary>
|
||||
/// <param name="src"></param>
|
||||
/// <returns></returns>
|
||||
@@ -407,7 +377,8 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
if (this.currentPage == null || this.currentPage.ImageRegions.Count < 1)
|
||||
{
|
||||
if (this.currentPage != null)
|
||||
IEnumerable<ImageRegion> gridItems;
|
||||
if (this.currentPage != null) // 当前页遍历完了就向下滚动
|
||||
{
|
||||
if (this.currentPage.AntiRecycling.HasValue)
|
||||
{
|
||||
@@ -426,47 +397,16 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2);
|
||||
await TaskControl.Delay(300, ct);
|
||||
|
||||
bool canScoll = await TryVerticalScollDown();
|
||||
|
||||
if (canScoll)
|
||||
if (!await this.gridScroller.TryVerticalScollDown((src, columns) => GetGridItems(src, columns)))
|
||||
{
|
||||
for (int i = 0; i < this.s2Round; i++) // 再滚动差不多(最多行数-1)行
|
||||
{
|
||||
input.Mouse.VerticalScroll(-2);
|
||||
await TaskControl.Delay(this.roundMilliseconds, ct);
|
||||
}
|
||||
|
||||
DateTimeOffset rollingEndTime = DateTime.Now.AddSeconds(2);
|
||||
while (DateTime.Now < rollingEndTime)
|
||||
{
|
||||
await TaskControl.Delay(60, ct);
|
||||
using var ra2 = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion grid2 = ra2.DeriveCrop(this.roi);
|
||||
IEnumerable<Rect> gridItems2 = GetGridItems(grid2.SrcMat, this.columns);
|
||||
if (gridItems2.Min(i => i.Y) > (ra2.Width * this.s3Scale)) // 最后精细滚动,保证完整地显示最多行
|
||||
{
|
||||
input.Mouse.VerticalScroll(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
using var ra3 = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion grid3 = ra3.DeriveCrop(this.roi);
|
||||
grid3.MoveTo(grid3.Width, grid3.Height);
|
||||
await TaskControl.Delay(300, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await TaskControl.Delay(300, ct);
|
||||
this.logger.LogInformation("滚动到底部了");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<ImageRegion> gridItems;
|
||||
if (this.currentPage == null)
|
||||
using ImageRegion ra = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion imageRegion = ra.DeriveCrop(this.roi);
|
||||
gridItems = GetGridItems(imageRegion.SrcMat, this.columns).Select(imageRegion.DeriveCrop);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 第一页采集时,主动操作来避免图标高亮
|
||||
// 双击第四列,采集第一、二列
|
||||
@@ -495,12 +435,6 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
|
||||
gridItems = columns12Items.Select(imageRegion12.DeriveCrop).Union(columnsRestItems.Select(imageRegionRest.DeriveCrop)).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
using ImageRegion ra = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion imageRegion = ra.DeriveCrop(this.roi);
|
||||
gridItems = GetGridItems(imageRegion.SrcMat, this.columns).Select(imageRegion.DeriveCrop);
|
||||
}
|
||||
|
||||
List<List<ImageRegion>> clusterRows = ClusterRows(gridItems, (int)(0.025 * this.roi.Height));
|
||||
this.currentPage = new Page(new Queue<ImageRegion>(clusterRows.SelectMany(r => r)), clusterRows.Reverse<List<ImageRegion>>().Skip(1)?.FirstOrDefault()?.FirstOrDefault()?.ToRect());
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
public static class GridScreenExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取GridItem图标底部的文字
|
||||
/// </summary>
|
||||
/// <param name="mat"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetGridItemIconText(this Mat mat, IOcrService ocrService)
|
||||
{
|
||||
Mat subMat = mat.SubMat(mat.Height * 128 / 153, mat.Height * 150 / 153, mat.Width * 5 / 125, mat.Width * 120 / 125);
|
||||
using Mat resize = subMat.Resize(new Size(subMat.Width * 2, subMat.Height * 2));
|
||||
return ocrService.Ocr(resize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 截取Grid图标中图案的部分
|
||||
/// </summary>
|
||||
/// <param name="mat"></param>
|
||||
/// <returns></returns>
|
||||
public static Mat GetGridIcon(this Mat mat)
|
||||
{
|
||||
using Mat resized = mat.Resize(new Size(125, 153));
|
||||
return resized.SubMat(0, 125, 0, 125).Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
@@ -26,6 +26,8 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
[Description("摆设")]
|
||||
Furnishings,
|
||||
[Description("圣遗物分解")]
|
||||
ArtifactSalvage
|
||||
ArtifactSalvage,
|
||||
[Description("圣遗物套装筛选")]
|
||||
ArtifactSetFilter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
public class GridScreenParams
|
||||
{
|
||||
internal int X1080p { get; private set; }
|
||||
internal int Y1080p { get; private set; }
|
||||
internal int Width1080p { get; private set; }
|
||||
internal int Height1080p { get; private set; }
|
||||
internal int Columns { get; private set; }
|
||||
internal int S1Round { get; private set; }
|
||||
internal int RoundMilliseconds { get; private set; }
|
||||
internal int S2Round { get; private set; }
|
||||
internal double S3Scale { get; private set; }
|
||||
|
||||
internal Rect GetRect(ImageRegion gameScreen)
|
||||
{
|
||||
float scale = gameScreen.Height / 1080f;
|
||||
return new Rect((int)(scale * X1080p), (int)(scale * Y1080p), (int)(scale * Width1080p), (int)(scale * Height1080p));
|
||||
}
|
||||
|
||||
private static readonly GridScreenParams weapons = new GridScreenParams()
|
||||
{
|
||||
X1080p = 106,
|
||||
Y1080p = 110,
|
||||
Width1080p = 1171,
|
||||
Height1080p = 845,
|
||||
Columns = 8,
|
||||
S1Round = 3,
|
||||
RoundMilliseconds = 40,
|
||||
S2Round = 32,
|
||||
S3Scale = 0.024
|
||||
};
|
||||
|
||||
internal static FrozenDictionary<GridScreenName, GridScreenParams> Templates { get; } = new Dictionary<GridScreenName, GridScreenParams>() {
|
||||
{ GridScreenName.Weapons, weapons },
|
||||
{ GridScreenName.Artifacts, new GridScreenParams(){
|
||||
X1080p = 106,
|
||||
Y1080p = 162,
|
||||
Width1080p = 1171,
|
||||
Height1080p = 783,
|
||||
Columns = 8,
|
||||
S1Round = 3,
|
||||
RoundMilliseconds = 40,
|
||||
S2Round = 32,
|
||||
S3Scale = 0.024
|
||||
}},
|
||||
{ GridScreenName.CharacterDevelopmentItems, weapons },
|
||||
{ GridScreenName.Food, weapons },
|
||||
{ GridScreenName.Materials, weapons },
|
||||
{ GridScreenName.Gadget, weapons },
|
||||
{ GridScreenName.Quest, weapons },
|
||||
{ GridScreenName.PreciousItems, weapons },
|
||||
{ GridScreenName.Furnishings, weapons },
|
||||
{ GridScreenName.ArtifactSalvage, new GridScreenParams(){
|
||||
X1080p = 48,
|
||||
Y1080p = 106,
|
||||
Width1080p = 1267,
|
||||
Height1080p = 768,
|
||||
Columns = 9,
|
||||
S1Round = 3,
|
||||
RoundMilliseconds = 40,
|
||||
S2Round = 28,
|
||||
S3Scale = 0.018
|
||||
}}
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
122
BetterGenshinImpact/GameTask/Model/GameUI/GridScroller.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using Fischless.WindowsInput;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Model.GameUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Grid界面垂直滚动服务类
|
||||
/// </summary>
|
||||
public class GridScroller
|
||||
{
|
||||
private readonly Rect roi;
|
||||
private readonly CancellationToken ct;
|
||||
private readonly ILogger logger;
|
||||
private readonly InputSimulator input = Simulation.SendInput;
|
||||
private readonly int columns;
|
||||
private readonly int s1Round;
|
||||
private readonly int roundMilliseconds;
|
||||
private readonly int s2Round;
|
||||
private readonly double s3Scale;
|
||||
|
||||
internal GridScroller(GridParams @params, ILogger logger, InputSimulator input, CancellationToken ct)
|
||||
{
|
||||
this.roi = @params.Roi;
|
||||
this.ct = ct;
|
||||
this.logger = logger;
|
||||
this.input = input;
|
||||
this.columns = @params.Columns;
|
||||
this.s1Round = @params.S1Round;
|
||||
this.roundMilliseconds = @params.RoundMilliseconds;
|
||||
this.s2Round = @params.S2Round;
|
||||
this.s3Scale = @params.S3Scale;
|
||||
}
|
||||
|
||||
internal async Task<bool> TryVerticalScollDown(Func<Mat, int, IEnumerable<Rect>> GetGridItems)
|
||||
{
|
||||
using var ra = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion prevGrid = ra.DeriveCrop(roi);
|
||||
|
||||
for (int i = 0; i < this.s1Round; i++)
|
||||
{
|
||||
this.input.Mouse.VerticalScroll(-2);
|
||||
await TaskControl.Delay(this.roundMilliseconds, this.ct);
|
||||
}
|
||||
await TaskControl.Delay(300, this.ct);
|
||||
using var ra2 = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion scrolledGrid = ra2.DeriveCrop(this.roi);
|
||||
|
||||
bool isScrolling = IsScrolling(prevGrid.CacheGreyMat, scrolledGrid.CacheGreyMat, out Point2d shift, logger: this.logger);
|
||||
|
||||
if (isScrolling)
|
||||
{
|
||||
for (int i = 0; i < this.s2Round; i++) // 再滚动差不多(最多行数-1)行
|
||||
{
|
||||
input.Mouse.VerticalScroll(-2);
|
||||
await TaskControl.Delay(this.roundMilliseconds, ct);
|
||||
}
|
||||
|
||||
DateTimeOffset rollingEndTime = DateTime.Now.AddSeconds(2);
|
||||
while (DateTime.Now < rollingEndTime)
|
||||
{
|
||||
await TaskControl.Delay(60, ct);
|
||||
using var ra4 = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion grid2 = ra4.DeriveCrop(this.roi);
|
||||
IEnumerable<Rect> gridItems2 = GetGridItems(grid2.SrcMat, this.columns);
|
||||
if (gridItems2.Min(i => i.Y) > (ra4.Width * this.s3Scale)) // 最后精细滚动,保证完整地显示最多行
|
||||
{
|
||||
input.Mouse.VerticalScroll(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
using var ra3 = TaskControl.CaptureToRectArea();
|
||||
using ImageRegion grid3 = ra3.DeriveCrop(this.roi);
|
||||
grid3.MoveTo(grid3.Width, grid3.Height);
|
||||
await TaskControl.Delay(300, ct);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await TaskControl.Delay(300, ct);
|
||||
this.logger.LogInformation("滚动到底部了");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否还能继续滚动,如果到底了则只能滚动一丝并很快地回弹
|
||||
/// </summary>
|
||||
/// <param name="prevGray">先前的灰度图</param>
|
||||
/// <param name="nextGray">尝试滚动并等待可能的回弹后的灰度图</param>
|
||||
/// <param name="shift">估计的位移</param>
|
||||
/// <param name="lowerThreshold">低于下限则可能不存在平移</param>
|
||||
/// <param name="upperThreshold">上限用于抵消微小的其他差异,比如高亮图标的呼吸闪烁</param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsScrolling(Mat prevGray, Mat nextGray, out Point2d shift, double lowerThreshold = 0.5, double upperThreshold = 0.95, ILogger? logger = null)
|
||||
{
|
||||
using Mat prev = new Mat();
|
||||
prevGray.ConvertTo(prev, MatType.CV_32FC1);
|
||||
using Mat next = new Mat();
|
||||
nextGray.ConvertTo(next, MatType.CV_32FC1);
|
||||
|
||||
using Mat window = new Mat();
|
||||
shift = Cv2.PhaseCorrelate(prev, next, window, out double response);
|
||||
// 相位相关性
|
||||
//logger?.LogInformation($"response={response:F3}, shift=({shift.X:F2}, {shift.Y:F2})");
|
||||
return response > lowerThreshold && response < upperThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ using BetterGenshinImpact.GameTask.Model;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Shell;
|
||||
|
||||
public class ShellTaskParam : BaseTaskParam
|
||||
public class ShellTaskParam : BaseTaskParam<ShellTask>
|
||||
{
|
||||
private ShellTaskParam(string command, int configTimeoutSeconds, bool configNoWindow, bool configOutput, bool configDisable)
|
||||
private ShellTaskParam(string command, int configTimeoutSeconds, bool configNoWindow, bool configOutput, bool configDisable) : base(null, null)
|
||||
{
|
||||
Command = command;
|
||||
TimeoutSeconds = configTimeoutSeconds;
|
||||
|
||||
@@ -4,6 +4,19 @@ namespace BetterGenshinImpact.Helpers;
|
||||
|
||||
public class DirectoryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 删除指定目录(如果存在)
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">要删除的目录路径</param>
|
||||
/// <param name="recursive">是否递归删除子目录和文件,默认为true</param>
|
||||
public static void DeleteDirectory(string directoryPath, bool recursive = true)
|
||||
{
|
||||
if (Directory.Exists(directoryPath))
|
||||
{
|
||||
Directory.Delete(directoryPath, recursive);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteReadOnlyDirectory(string directoryPath)
|
||||
{
|
||||
if (Directory.Exists(directoryPath))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using BetterGenshinImpact.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using BetterGenshinImpact.Model;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BetterGenshinImpact.Helpers.Extensions;
|
||||
|
||||
@@ -18,6 +20,33 @@ public static class EnumExtensions
|
||||
?.Description ?? value.ToString();
|
||||
}
|
||||
|
||||
public static bool TryGetEnumValueFromDescription<T>(this string description, [NotNullWhen(true)] out T? result) where T : struct, Enum
|
||||
{
|
||||
result = null;
|
||||
if (string.IsNullOrEmpty(description))
|
||||
return false;
|
||||
|
||||
var type = typeof(T);
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute))
|
||||
is DescriptionAttribute attribute)
|
||||
{
|
||||
if (attribute.Description.Equals(description, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = field.GetValue(null);
|
||||
if (value is T enumValue)
|
||||
{
|
||||
result = enumValue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int GetOrder(this Enum value)
|
||||
{
|
||||
return value.GetType()
|
||||
|
||||
@@ -39,12 +39,44 @@ public class ScriptObjectConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static T GetValue<T>(ScriptObject source, string propertyName, T defaultValue)
|
||||
{
|
||||
if (source[propertyName] is not Undefined && source[propertyName] != null)
|
||||
{
|
||||
object value = source.GetProperty(propertyName);
|
||||
|
||||
Type type = typeof(T);
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
// 处理数字
|
||||
if (value is int intValue)
|
||||
{
|
||||
if (Enum.IsDefined(type, intValue))
|
||||
{
|
||||
return (T)Enum.ToObject(type, intValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
// 处理字符串
|
||||
else if (value is string strValue)
|
||||
{
|
||||
if (Enum.TryParse(type, strValue, ignoreCase: true, out object? parsedEnum))
|
||||
{
|
||||
return (T)parsedEnum;
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (T)value;
|
||||
}
|
||||
return defaultValue;
|
||||
|
||||
@@ -24,12 +24,12 @@ public class WindowHelper
|
||||
switch (themeType)
|
||||
{
|
||||
case ThemeType.DarkNone:
|
||||
window.Background = new SolidColorBrush(Colors.Transparent);
|
||||
window.Background = new SolidColorBrush(Color.FromArgb(255, 32, 32, 32));
|
||||
WindowBackdrop.ApplyBackdrop(window, WindowBackdropType.None);
|
||||
break;
|
||||
|
||||
case ThemeType.LightNone:
|
||||
window.Background = new SolidColorBrush(Colors.Transparent);
|
||||
window.Background = new SolidColorBrush(Color.FromArgb(255, 243, 243, 243));
|
||||
WindowBackdrop.ApplyBackdrop(window, WindowBackdropType.None);
|
||||
break;
|
||||
|
||||
|
||||
@@ -213,8 +213,7 @@
|
||||
Command="{Binding SwitchBackdropCommand}"
|
||||
CornerRadius="0"
|
||||
Icon="{ui:SymbolIcon Blur24}"
|
||||
ToolTip="切换深浅主题背景和毛玻璃效果"
|
||||
Visibility="{Binding IsWin11Later, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}" />
|
||||
ToolTip="切换深浅主题或背景样式" />
|
||||
<ui:Button Width="46"
|
||||
Height="32"
|
||||
Background="Transparent"
|
||||
|
||||
@@ -877,7 +877,7 @@
|
||||
</ui:CardExpander>
|
||||
|
||||
<!-- 新增手动导入本地脚本仓库功能 -->
|
||||
<ui:CardControl Margin="0,0,0,12" Icon="{ui:SymbolIcon Folder24}">
|
||||
<!--<ui:CardControl Margin="0,0,0,12" Icon="{ui:SymbolIcon Folder24}">
|
||||
<ui:CardControl.Header>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -907,7 +907,7 @@
|
||||
</Grid>
|
||||
|
||||
</ui:CardControl.Header>
|
||||
</ui:CardControl>
|
||||
</ui:CardControl>-->
|
||||
|
||||
|
||||
<!-- 其他设置 -->
|
||||
|
||||
@@ -2339,10 +2339,11 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="3*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
@@ -2359,18 +2360,55 @@
|
||||
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
|
||||
TextWrapping="Wrap">
|
||||
只要满足的圣遗物都会被选中
|
||||
<LineBreak/>
|
||||
JS接受ArtifactStat作为入参;应对Output赋值一个布尔值作为返回
|
||||
</ui:TextBlock>
|
||||
<ui:TextBox Grid.Row="0"
|
||||
<ui:Button Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,36,0"
|
||||
Content="从脚本仓库复制"
|
||||
Command="{Binding CopyArtifactSalvageJavaScriptFromRepositoryCommand}" />
|
||||
<ui:TextBox Grid.Row="2"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
MinWidth="180"
|
||||
MaxWidth="800"
|
||||
Margin="0,0,36,0"
|
||||
Text="{Binding Config.AutoArtifactSalvageConfig.JavaScript, Mode=TwoWay}"
|
||||
TextWrapping="Wrap" Cursor="IBeam" />
|
||||
</Grid>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
FontTypography="Body"
|
||||
TextWrapping="Wrap">
|
||||
按套装筛选
|
||||
</ui:TextBlock>
|
||||
<ui:TextBlock Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
|
||||
TextWrapping="Wrap">
|
||||
利用游戏自带的筛选功能先行筛选
|
||||
<LineBreak/>
|
||||
一般填写套装内生之花名,可填入多个名称;留空则不用
|
||||
</ui:TextBlock>
|
||||
<ui:TextBox Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
MinWidth="180"
|
||||
MaxWidth="400"
|
||||
Margin="0,0,36,0"
|
||||
Text="{Binding Config.AutoArtifactSalvageConfig.ArtifactSetFilter, Mode=TwoWay}"
|
||||
TextWrapping="Wrap" Cursor="IBeam" />
|
||||
</Grid>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -2426,6 +2464,35 @@
|
||||
Value="{Binding Config.AutoArtifactSalvageConfig.MaxNumToCheck, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Text="{Binding Config.AutoArtifactSalvageConfig.MaxNumToCheck, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
FontTypography="Body"
|
||||
Text="识别失败策略"
|
||||
TextWrapping="Wrap" />
|
||||
<ui:TextBlock Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
|
||||
Text="识别单个圣遗物面板信息失败时,是跳过还是终止"
|
||||
TextWrapping="Wrap" />
|
||||
<ComboBox Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
Width="100"
|
||||
Margin="0,0,36,0"
|
||||
ItemsSource="{Binding RecognitionFailurePolicyDict}"
|
||||
SelectedValuePath="Key"
|
||||
DisplayMemberPath="Value"
|
||||
SelectedItem="{Binding Config.AutoArtifactSalvageConfig.RecognitionFailurePolicy, Converter={StaticResource EnumToKVPConverter}, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ui:CardExpander>
|
||||
|
||||
|
||||
@@ -1445,8 +1445,14 @@
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
FontTypography="Body"
|
||||
Text="自动吃食物配置"
|
||||
TextWrapping="Wrap" />
|
||||
Text=""
|
||||
TextWrapping="Wrap">
|
||||
自动吃食物配置 -
|
||||
<Hyperlink Command="{Binding GoToAutoEatUrlCommand}"
|
||||
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
|
||||
点击查看调用方法
|
||||
</Hyperlink>
|
||||
</ui:TextBlock>
|
||||
<ui:TextBlock Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ui:FluentWindow x:Class="BetterGenshinImpact.View.Windows.OcrDialog"
|
||||
<ui:FluentWindow x:Class="BetterGenshinImpact.View.Windows.OcrDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
@@ -35,8 +35,15 @@
|
||||
<Image Name="Screenshot" Source="pack://application:,,,/Resources/Images/logo.png" Stretch="None" Margin="3"/>
|
||||
</Grid>
|
||||
<StackPanel Margin="12">
|
||||
<ui:TextBox Name="TxtRecognized" Margin="5" />
|
||||
<ui:TextBox Name="RegexResult" Margin="5" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:TextBox Grid.Column="0" Name="TxtRecognized" Margin="2" />
|
||||
<ui:TextBox Grid.Column="1" Name="ModelStructure" Margin="2" />
|
||||
</Grid>
|
||||
<ui:TextBox Name="RegexResult" Margin="2" />
|
||||
<StackPanel Margin="5"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
|
||||
@@ -3,8 +3,12 @@ using BetterGenshinImpact.GameTask;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace BetterGenshinImpact.View.Windows;
|
||||
|
||||
@@ -15,6 +19,8 @@ public partial class OcrDialog
|
||||
private readonly double widthRatio;
|
||||
private readonly double heightRatio;
|
||||
private readonly string? javaScript;
|
||||
private readonly AutoArtifactSalvageTask autoArtifactSalvageTask;
|
||||
|
||||
public OcrDialog(double xRatio, double yRatio, double widthRatio, double heightRatio, string title, string? javaScript = null)
|
||||
{
|
||||
this.xRatio = xRatio;
|
||||
@@ -22,6 +28,7 @@ public partial class OcrDialog
|
||||
this.widthRatio = widthRatio;
|
||||
this.heightRatio = heightRatio;
|
||||
this.javaScript = javaScript;
|
||||
this.autoArtifactSalvageTask = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName)));
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
@@ -33,16 +40,46 @@ public partial class OcrDialog
|
||||
{
|
||||
using var ra = TaskControl.CaptureToRectArea();
|
||||
using var card = ra.DeriveCrop(new OpenCvSharp.Rect((int)(ra.Width * xRatio), (int)(ra.Height * yRatio), (int)(ra.Width * widthRatio), (int)(ra.Height * heightRatio)));
|
||||
//Cv2.ImWrite($"{DateTime.Now.ToString("yyyyMMddHHmm")}_GetArtifactStat.png", card.SrcMat);
|
||||
var bitmapImage = card.SrcMat.ToWriteableBitmap();
|
||||
|
||||
this.Screenshot.Source = bitmapImage;
|
||||
|
||||
ArtifactStat artifact = AutoArtifactSalvageTask.GetArtifactStat(card.SrcMat, OcrFactory.Paddle, new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName), out string allText);
|
||||
this.TxtRecognized.Text = allText;
|
||||
if (this.javaScript != null)
|
||||
try
|
||||
{
|
||||
bool isMatch = AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript);
|
||||
this.RegexResult.Text = isMatch ? "匹配" : "不匹配";
|
||||
ArtifactStat artifact = this.autoArtifactSalvageTask.GetArtifactStat(card.SrcMat, OcrFactory.Paddle, out string allText);
|
||||
|
||||
this.TxtRecognized.Text = allText;
|
||||
this.ModelStructure.Text = artifact.ToStructuredString();
|
||||
if (this.javaScript != null)
|
||||
{
|
||||
bool isMatch = AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript);
|
||||
this.RegexResult.Text = isMatch ? "匹配" : "不匹配";
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var multilineTextBox = new TextBox
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Text = e.ToString(),
|
||||
IsReadOnly = true
|
||||
};
|
||||
var p = new PromptDialog($"出错了:{e.Message}\r\n\r\n是否保存该圣遗物截图?(至log/autoArtifactSalvageException/)", $"异常处理", multilineTextBox, null);
|
||||
p.Height = 600;
|
||||
p.MaxWidth = 800;
|
||||
p.ShowDialog();
|
||||
|
||||
if (p.DialogResult == true)
|
||||
{
|
||||
string directory = Path.Combine(AppContext.BaseDirectory, "log/autoArtifactSalvageException");
|
||||
Directory.CreateDirectory(directory);
|
||||
string filePath = Path.Combine(directory, $"{DateTime.Now.ToString("yyyyMMddHHmmss")}_GetArtifactStat.png");
|
||||
Cv2.ImWrite(filePath, card.SrcMat);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
this.UpdateLayout();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:vio="http://schemas.lepo.co/wpfui/2022/xaml/violeta"
|
||||
Title="脚本仓库"
|
||||
Width="400"
|
||||
Width="410"
|
||||
MinWidth="360"
|
||||
MinHeight="50"
|
||||
ResizeMode="NoResize"
|
||||
@@ -36,107 +36,220 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<Grid Grid.Row="1" Margin="12,0,12,12">
|
||||
<Border Background="{ui:ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标签页区域 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{ui:ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ui:ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1,1,1,1"
|
||||
CornerRadius="8">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TabControl Margin="12">
|
||||
<!-- Git仓库标签页 -->
|
||||
<TabItem Width="110">
|
||||
<TabItem.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Margin="0,0,6,0" Symbol="BranchFork24" />
|
||||
<TextBlock Text="Git一键更新" />
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<Grid Margin="0,12,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 第一行:更新渠道 -->
|
||||
<Grid Grid.Row="0" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 第一行:更新渠道 -->
|
||||
<Grid Grid.Row="0" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="更新渠道:" />
|
||||
<ComboBox Grid.Column="1"
|
||||
MinWidth="160"
|
||||
VerticalAlignment="Center"
|
||||
DisplayMemberPath="Name"
|
||||
ItemsSource="{Binding RepoChannels}"
|
||||
SelectedItem="{Binding SelectedRepoChannel}" />
|
||||
</Grid>
|
||||
<ui:TextBlock Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="更新渠道:" />
|
||||
<ComboBox Grid.Column="1"
|
||||
MinWidth="160"
|
||||
VerticalAlignment="Center"
|
||||
DisplayMemberPath="Name"
|
||||
ItemsSource="{Binding RepoChannels}"
|
||||
SelectedItem="{Binding SelectedRepoChannel}" />
|
||||
</Grid>
|
||||
|
||||
<!-- 第二行:脚本仓库URL -->
|
||||
<Grid Grid.Row="1" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 第二行:脚本仓库URL -->
|
||||
<Grid Grid.Row="1" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="仓库地址:" />
|
||||
<TextBox Grid.Column="1"
|
||||
MinWidth="160"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding IsRepoUrlReadOnly, Converter={StaticResource InverseBooleanConverter}}"
|
||||
IsReadOnly="{Binding IsRepoUrlReadOnly}"
|
||||
Text="{Binding Config.SelectedRepoUrl}" />
|
||||
</Grid>
|
||||
<ui:TextBlock Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="仓库地址:" />
|
||||
<TextBox Grid.Column="1"
|
||||
MinWidth="160"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding IsRepoUrlReadOnly, Converter={StaticResource InverseBooleanConverter}}"
|
||||
IsReadOnly="{Binding IsRepoUrlReadOnly}"
|
||||
Text="{Binding Config.SelectedRepoUrl}" />
|
||||
</Grid>
|
||||
|
||||
<!-- 第三行:按钮组 -->
|
||||
<Grid Grid.Row="2" Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 第三行:按钮组 -->
|
||||
<Grid Grid.Row="2" Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:Button Grid.Column="0"
|
||||
Margin="0,0,4,0"
|
||||
Command="{Binding UpdateRepoCommand}"
|
||||
Content="更新仓库"
|
||||
Icon="{ui:SymbolIcon CloudSync24}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<ui:Button Grid.Column="1"
|
||||
Margin="4,0,4,0"
|
||||
Command="{Binding ResetRepoCommand}"
|
||||
Content="重置仓库"
|
||||
Icon="{ui:SymbolIcon ArrowReset24}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<ui:Button Grid.Column="2"
|
||||
Margin="4,0,0,0"
|
||||
Appearance="Primary"
|
||||
Command="{Binding OpenLocalScriptRepoCommand}"
|
||||
Content="打开仓库"
|
||||
Icon="{ui:SymbolIcon BookStar24}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
<ui:Button Grid.Column="0"
|
||||
Margin="0,0,4,0"
|
||||
Command="{Binding UpdateRepoCommand}"
|
||||
Content="更新仓库"
|
||||
Icon="{ui:SymbolIcon CloudSync24}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<ui:Button Grid.Column="1"
|
||||
Margin="4,0,0,0"
|
||||
Command="{Binding ResetRepoCommand}"
|
||||
Content="重置仓库"
|
||||
Icon="{ui:SymbolIcon ArrowReset24}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
|
||||
<!-- 进度条区域 -->
|
||||
<Grid Grid.Row="3"
|
||||
Margin="0,8,0,4"
|
||||
Visibility="{Binding IsUpdating, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Margin="0,0,0,4"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding UpdateProgressText}" />
|
||||
<ProgressBar Grid.Row="1"
|
||||
Maximum="100"
|
||||
Value="{Binding UpdateProgressValue}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 在线更新标签页 -->
|
||||
<!--<TabItem Width="100">
|
||||
<TabItem.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Margin="0,0,6,0" Symbol="DocumentArrowDown20" />
|
||||
<TextBlock Text="URL更新" />
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<Grid Margin="0,12,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Margin="0,0,0,12"
|
||||
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="从在线地址直接下载脚本仓库压缩包,适用于网络环境较好的情况。" />
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="下载地址:" />
|
||||
<TextBox Grid.Column="1"
|
||||
MinWidth="160"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding OnlineDownloadUrl}" />
|
||||
</Grid>
|
||||
|
||||
<ui:Button Grid.Row="2"
|
||||
Command="{Binding DownloadOnlineRepoCommand}"
|
||||
Content="下载并导入"
|
||||
Icon="{ui:SymbolIcon Cloud24}"
|
||||
HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
</TabItem>-->
|
||||
|
||||
<!-- 手动导入标签页 -->
|
||||
<TabItem Width="100">
|
||||
<TabItem.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Margin="0,0,6,0" Symbol="FolderZip24" />
|
||||
<TextBlock Text="手动导入" />
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<Grid Margin="0,12,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Margin="0,0,0,12"
|
||||
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="从本地zip文件导入脚本仓库,适用于离线环境或网络不稳定的情况。" />
|
||||
|
||||
<ui:TextBlock Grid.Row="1"
|
||||
Margin="0,0,0,12"
|
||||
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="请选择包含脚本仓库内容的zip压缩包文件进行导入。" />
|
||||
|
||||
<ui:TextBlock Grid.Row="2"
|
||||
Margin="0,0,0,12"
|
||||
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="可以从这里获取zip:" />
|
||||
<Hyperlink NavigateUri="https://bettergi.com/feats/autos/srepo.html" RequestNavigate="Hyperlink_RequestNavigate">
|
||||
<Run Text="脚本仓库文档" />
|
||||
</Hyperlink>
|
||||
</ui:TextBlock>
|
||||
|
||||
<ui:Button Grid.Row="3"
|
||||
Command="{Binding ImportLocalScriptsRepoZipCommand}"
|
||||
Content="选择zip文件导入"
|
||||
Icon="{ui:SymbolIcon FolderZip24}"
|
||||
HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
|
||||
<!-- 进度条区域 -->
|
||||
<Grid Grid.Row="1"
|
||||
Margin="0,12,0,8"
|
||||
Visibility="{Binding IsUpdating, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Row="0"
|
||||
Margin="0,0,0,4"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding UpdateProgressText}" />
|
||||
<ProgressBar Grid.Row="1"
|
||||
Maximum="100"
|
||||
Value="{Binding UpdateProgressValue}" />
|
||||
</Grid>
|
||||
|
||||
<!-- 公共按钮区域 -->
|
||||
<Grid Grid.Row="2" Margin="0,4,0,0">
|
||||
<ui:Button Appearance="Primary"
|
||||
Command="{Binding OpenLocalScriptRepoCommand}"
|
||||
Content="打开仓库"
|
||||
Icon="{ui:SymbolIcon BookStar24}"
|
||||
HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
@@ -5,13 +5,18 @@ using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.Helpers.Ui;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Navigation;
|
||||
using Wpf.Ui.Violeta.Controls;
|
||||
|
||||
namespace BetterGenshinImpact.View.Windows;
|
||||
@@ -48,6 +53,9 @@ public partial class ScriptRepoWindow
|
||||
[ObservableProperty] private string _updateProgressText = "准备更新,请耐心等待...";
|
||||
[ObservableProperty] private ScriptConfig _config = TaskContext.Instance().Config.ScriptConfig;
|
||||
|
||||
// 在线更新相关属性
|
||||
[ObservableProperty] private string _onlineDownloadUrl = "";
|
||||
|
||||
public ScriptRepoWindow()
|
||||
{
|
||||
InitializeRepoChannels();
|
||||
@@ -250,4 +258,212 @@ public partial class ScriptRepoWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[RelayCommand]
|
||||
private async Task DownloadOnlineRepo()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OnlineDownloadUrl))
|
||||
{
|
||||
Toast.Warning("请输入有效的下载地址。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsUpdating)
|
||||
{
|
||||
Toast.Warning("请等待当前操作完成后再进行下载。");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsUpdating = true;
|
||||
UpdateProgressValue = 0;
|
||||
UpdateProgressText = "正在下载脚本仓库...";
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.Timeout = TimeSpan.FromMinutes(10);
|
||||
|
||||
// 下载文件
|
||||
var response = await httpClient.GetAsync(OnlineDownloadUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var tempZipPath = Path.Combine(Path.GetTempPath(), $"script_repo_{DateTime.Now:yyyyMMddHHmmss}.zip");
|
||||
await using (var fileStream = File.Create(tempZipPath))
|
||||
{
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
}
|
||||
|
||||
UpdateProgressText = "正在解压并导入...";
|
||||
UpdateProgressValue = 50;
|
||||
|
||||
// 导入下载的zip文件
|
||||
await ImportZipFile(tempZipPath);
|
||||
|
||||
// 清理临时文件
|
||||
if (File.Exists(tempZipPath))
|
||||
{
|
||||
File.Delete(tempZipPath);
|
||||
}
|
||||
|
||||
Toast.Success("在线脚本仓库下载并导入成功!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Toast.Error($"下载失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsUpdating = false;
|
||||
}
|
||||
}*/
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ImportLocalScriptsRepoZip()
|
||||
{
|
||||
if (IsUpdating)
|
||||
{
|
||||
Toast.Warning("请等待当前操作完成后再进行导入。");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var openFileDialog = new OpenFileDialog
|
||||
{
|
||||
Title = "选择脚本仓库压缩包",
|
||||
Filter = "压缩包文件 (*.zip)|*.zip",
|
||||
Multiselect = false
|
||||
};
|
||||
|
||||
if (openFileDialog.ShowDialog() == true)
|
||||
{
|
||||
IsUpdating = true;
|
||||
UpdateProgressValue = 0;
|
||||
UpdateProgressText = "正在导入脚本仓库,请耐心等待...";
|
||||
|
||||
await ImportZipFile(openFileDialog.FileName);
|
||||
Toast.Success("脚本仓库导入成功!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Toast.Error($"导入失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ImportZipFile(string zipFilePath)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var tempPath = ScriptRepoUpdater.ReposTempPath;
|
||||
try
|
||||
{
|
||||
// 阶段1: 准备工作 (0-10%)
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 0;
|
||||
UpdateProgressText = "正在准备导入环境...";
|
||||
});
|
||||
|
||||
var tempUnzipDir = Path.Combine(tempPath, "importZipFile");
|
||||
|
||||
// 先删除临时目录
|
||||
DirectoryHelper.DeleteReadOnlyDirectory(tempPath);
|
||||
|
||||
// 创建目标目录
|
||||
Directory.CreateDirectory(tempUnzipDir);
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 10;
|
||||
UpdateProgressText = "准备完成,开始解压文件...";
|
||||
});
|
||||
|
||||
// 阶段2: 解压zip文件 (10-50%)
|
||||
ZipFile.ExtractToDirectory(zipFilePath, tempUnzipDir, true);
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 50;
|
||||
UpdateProgressText = "文件解压完成,正在验证仓库结构...";
|
||||
});
|
||||
|
||||
// 阶段3: 查找并验证 repo.json (50-60%)
|
||||
var repoJsonPath = Directory.GetFiles(tempUnzipDir, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
|
||||
if (repoJsonPath == null)
|
||||
{
|
||||
throw new FileNotFoundException("未找到 repo.json 文件,导入失败。");
|
||||
}
|
||||
|
||||
var repoDir = Path.GetDirectoryName(repoJsonPath)!;
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 60;
|
||||
UpdateProgressText = "仓库结构验证通过,正在清理旧数据...";
|
||||
});
|
||||
|
||||
// 阶段4: 删除旧的目标目录 (60-70%)
|
||||
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath))
|
||||
{
|
||||
DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPath);
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 70;
|
||||
UpdateProgressText = "旧数据清理完成,正在复制新仓库...";
|
||||
});
|
||||
|
||||
// 阶段5: 复制新目录 (70-95%)
|
||||
DirectoryHelper.CopyDirectory(repoDir, ScriptRepoUpdater.CenterRepoPath);
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 95;
|
||||
UpdateProgressText = "仓库复制完成,正在清理临时文件...";
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 阶段6: 清理临时文件 (95-100%)
|
||||
DirectoryHelper.DeleteReadOnlyDirectory(tempPath);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// 最终完成
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateProgressValue = 100;
|
||||
UpdateProgressText = "导入完成";
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理超链接点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = e.Uri.AbsoluteUri,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"无法打开链接: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,30 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel
|
||||
|
||||
private void ApplyTheme(ThemeType themeType)
|
||||
{
|
||||
var originalThemeType = themeType;
|
||||
|
||||
// 根据主题类型设置应用程序主题(深色/浅色)和背景效果类型(Mica/Acrylic/None)
|
||||
if (!OsVersionHelper.IsWindows11_22523_OrGreater)
|
||||
{
|
||||
// 22523以下版本只支持深浅色切换,修正背景材质为纯色
|
||||
if (themeType == ThemeType.DarkMica || themeType == ThemeType.DarkAcrylic)
|
||||
{
|
||||
themeType = ThemeType.DarkNone;
|
||||
}
|
||||
else if (themeType == ThemeType.LightMica || themeType == ThemeType.LightAcrylic)
|
||||
{
|
||||
themeType = ThemeType.LightNone;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果主题类型被修正,更新配置并保存
|
||||
if (themeType != originalThemeType)
|
||||
{
|
||||
Config.CommonConfig.CurrentThemeType = themeType;
|
||||
_configService.Save();
|
||||
_logger.LogInformation($"主题类型已从 {originalThemeType} 修正为 {themeType},因为当前系统不支持该主题效果");
|
||||
}
|
||||
|
||||
switch (themeType)
|
||||
{
|
||||
case ThemeType.DarkNone:
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Core.Recorder;
|
||||
using BetterGenshinImpact.Core.Script;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.AutoFight;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoPathing;
|
||||
using BetterGenshinImpact.GameTask.AutoPathing.Handler;
|
||||
using BetterGenshinImpact.GameTask.AutoTrackPath;
|
||||
using BetterGenshinImpact.GameTask.Common;
|
||||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||||
using BetterGenshinImpact.GameTask.Common.Job;
|
||||
using BetterGenshinImpact.GameTask.Common.Map.Maps.Base;
|
||||
using BetterGenshinImpact.GameTask.Macro;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.GameTask.QuickBuy;
|
||||
using BetterGenshinImpact.GameTask.QuickSereniteaPot;
|
||||
using BetterGenshinImpact.GameTask.QuickTeleport.Assets;
|
||||
using BetterGenshinImpact.GameTask.UseRedeemCode;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using BetterGenshinImpact.Model;
|
||||
using BetterGenshinImpact.Service.Interface;
|
||||
using BetterGenshinImpact.View;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCvSharp;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||||
using BetterGenshinImpact.GameTask.Common.Map.Maps.Base;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using BetterGenshinImpact.GameTask.QuickBuy;
|
||||
using BetterGenshinImpact.GameTask.QuickTeleport.Assets;
|
||||
using BetterGenshinImpact.GameTask.UseRedeemCode;
|
||||
using BetterGenshinImpact.View;
|
||||
using OpenCvSharp;
|
||||
using Vanara.PInvoke;
|
||||
using HotKeySettingModel = BetterGenshinImpact.Model.HotKeySettingModel;
|
||||
|
||||
@@ -533,7 +534,7 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel
|
||||
if (pathRecording)
|
||||
{
|
||||
Task.Run(() => { pathRecorder.AddWaypoint(); });
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -583,7 +584,7 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel
|
||||
Config.HotKeyConfig.Test1HotkeyType,
|
||||
(_, _) =>
|
||||
{
|
||||
Task.Run(async () => { await new AutoArtifactSalvageTask(4).Start(new CancellationToken()); });
|
||||
Task.Run(async () => { await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star: 4, null, null, null, null)).Start(new CancellationToken()); });
|
||||
|
||||
}
|
||||
));
|
||||
|
||||
@@ -51,7 +51,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
private readonly ILogger<ScriptControlViewModel> _logger = App.GetLogger<ScriptControlViewModel>();
|
||||
|
||||
private readonly IScriptService _scriptService;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 配置组配置
|
||||
/// </summary>
|
||||
@@ -143,13 +143,13 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
|
||||
GameInfo? gameInfo = null;
|
||||
var config = LogParse.LoadConfig();
|
||||
|
||||
|
||||
OtherConfig.Miyoushe mcfg = TaskContext.Instance().Config.OtherConfig.MiyousheConfig;
|
||||
if (mcfg.LogSyncCookie && !string.IsNullOrEmpty(mcfg.Cookie))
|
||||
{
|
||||
config.Cookie = mcfg.Cookie;
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(config.Cookie))
|
||||
{
|
||||
config.CookieDictionary.TryGetValue(config.Cookie, out gameInfo);
|
||||
@@ -220,7 +220,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
stackPanel.Children.Add(mergerStatsSwitch);
|
||||
|
||||
|
||||
// 开关控件:ToggleButton 或 CheckBox
|
||||
CheckBox faultStatsSwitch = new CheckBox
|
||||
{
|
||||
@@ -228,21 +228,21 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
stackPanel.Children.Add(faultStatsSwitch);
|
||||
|
||||
|
||||
// 开关控件:ToggleButton 或 CheckBox
|
||||
CheckBox hoeingStatsSwitch = new CheckBox
|
||||
{
|
||||
Content = "统计锄地摩拉怪物数",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
|
||||
CheckBox GenerateFarmingPlanData = new CheckBox
|
||||
{
|
||||
Content = "生成锄地规划数据",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
stackPanel.Children.Add(GenerateFarmingPlanData);
|
||||
|
||||
|
||||
//firstRow.Children.Add(toggleSwitch);
|
||||
|
||||
// 将第一行添加到 StackPanel
|
||||
@@ -338,7 +338,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
GenerateFarmingPlanData.IsChecked = sgpc.GenerateFarmingPlanData;
|
||||
faultStatsSwitch.IsChecked = sgpc.FaultStatsSwitch;
|
||||
mergerStatsSwitch.IsChecked = sgpc.MergerStatsSwitch;
|
||||
|
||||
|
||||
hoeingDelayTextBox.Text = sgpc.HoeingDelay;
|
||||
|
||||
MessageBoxResult result = await uiMessageBox.ShowDialogAsync();
|
||||
@@ -364,9 +364,9 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
|
||||
if (mcfg.LogSyncCookie && !string.IsNullOrEmpty(cookieValue))
|
||||
{
|
||||
mcfg.Cookie = cookieValue;
|
||||
mcfg.Cookie = cookieValue;
|
||||
}
|
||||
|
||||
|
||||
LogParse.WriteConfigFile(config);
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Toast.Information(status, time:5000);
|
||||
Toast.Information(status, time: 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
// 生成HTML并加载
|
||||
win.NavigateToHtml(LogParse.GenerHtmlByConfigGroupEntity(configGroupEntities,
|
||||
hoeingStats ? realGameInfo : null, sgpc));
|
||||
win.ShowDialog();
|
||||
win.ShowDialog();
|
||||
// 取消订阅事件
|
||||
LogParse.HtmlGenerationStatusChanged -= OnHtmlGenerationStatusChanged;
|
||||
|
||||
@@ -559,14 +559,14 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
private void ExportMergerJsons()
|
||||
{
|
||||
int count = 0;
|
||||
var pathDir = Path.Combine(LogPath,"exportMergerJson",DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),"AutoPathing");
|
||||
var pathDir = Path.Combine(LogPath, "exportMergerJson", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), "AutoPathing");
|
||||
foreach (var scriptGroupProject in SelectedScriptGroup?.Projects ?? [])
|
||||
{
|
||||
if (scriptGroupProject.Type == "Pathing")
|
||||
{
|
||||
var mergerJson= JsonMerger.getMergePathingJson(Path.Combine(MapPathingViewModel.PathJsonPath,
|
||||
var mergerJson = JsonMerger.getMergePathingJson(Path.Combine(MapPathingViewModel.PathJsonPath,
|
||||
scriptGroupProject.FolderName, scriptGroupProject.Name));
|
||||
string fullPath = Path.Combine(pathDir,scriptGroupProject.FolderName,scriptGroupProject.Name);
|
||||
string fullPath = Path.Combine(pathDir, scriptGroupProject.FolderName, scriptGroupProject.Name);
|
||||
string dir = Path.GetDirectoryName(fullPath);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
@@ -576,13 +576,13 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count>0)
|
||||
if (count > 0)
|
||||
{
|
||||
Process.Start("explorer.exe", pathDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public void AddScriptGroupNextFlag(ScriptGroup? item)
|
||||
{
|
||||
@@ -591,7 +591,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
scriptGroup.NextFlag = false;
|
||||
}
|
||||
|
||||
if (item!=null)
|
||||
if (item != null)
|
||||
{
|
||||
item.NextFlag = true;
|
||||
TaskContext.Instance().Config.NextScriptGroupName = item.Name;
|
||||
@@ -731,7 +731,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
private void OnAddJsScript()
|
||||
{
|
||||
var list = LoadAllJsScriptProjects();
|
||||
var stackPanel = CreateJsScriptSelectionPanel(list);
|
||||
var stackPanel = CreateJsScriptSelectionPanel(list, typeof(CheckBox));
|
||||
|
||||
var result = PromptDialog.Prompt("请选择需要添加的JS脚本", "请选择需要添加的JS脚本", stackPanel, new Size(500, 600));
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
@@ -740,19 +740,19 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer CreateJsScriptSelectionPanel(List<ScriptProject> list)
|
||||
internal static ScrollViewer CreateJsScriptSelectionPanel(List<ScriptProject> list, Type selectType)
|
||||
{
|
||||
var stackPanel = new StackPanel();
|
||||
|
||||
|
||||
var filterTextBox = new TextBox
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
PlaceholderText = "输入搜索条件...",
|
||||
};
|
||||
filterTextBox.TextChanged += delegate { ApplyJsScriptFilter(stackPanel, list, filterTextBox.Text); };
|
||||
filterTextBox.TextChanged += delegate { ApplyJsScriptFilter(stackPanel, list, filterTextBox.Text, selectType); };
|
||||
stackPanel.Children.Add(filterTextBox);
|
||||
|
||||
AddJsScriptsToPanel(stackPanel, list, filterTextBox.Text);
|
||||
|
||||
AddJsScriptsToPanel(stackPanel, list, filterTextBox.Text, selectType);
|
||||
|
||||
var scrollViewer = new ScrollViewer
|
||||
{
|
||||
@@ -764,7 +764,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
return scrollViewer;
|
||||
}
|
||||
|
||||
private void ApplyJsScriptFilter(StackPanel parentPanel, List<ScriptProject> scripts, string filter)
|
||||
private static void ApplyJsScriptFilter(StackPanel parentPanel, List<ScriptProject> scripts, string filter, Type selectType)
|
||||
{
|
||||
if (parentPanel.Children.Count > 0)
|
||||
{
|
||||
@@ -780,16 +780,16 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
removeElements.ForEach(parentPanel.Children.Remove);
|
||||
}
|
||||
|
||||
AddJsScriptsToPanel(parentPanel, scripts, filter);
|
||||
AddJsScriptsToPanel(parentPanel, scripts, filter, selectType);
|
||||
}
|
||||
|
||||
private void AddJsScriptsToPanel(StackPanel parentPanel, List<ScriptProject> scripts, string filter)
|
||||
private static void AddJsScriptsToPanel(StackPanel parentPanel, List<ScriptProject> scripts, string filter, Type selectType)
|
||||
{
|
||||
foreach (var script in scripts)
|
||||
{
|
||||
var displayText = script.FolderName + " - " + script.Manifest.Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(filter) &&
|
||||
|
||||
if (!string.IsNullOrEmpty(filter) &&
|
||||
!displayText.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
|
||||
!script.FolderName.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
|
||||
!script.Manifest.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -797,15 +797,33 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
continue;
|
||||
}
|
||||
|
||||
var checkBox = new CheckBox
|
||||
if (selectType == typeof(CheckBox))
|
||||
{
|
||||
Content = displayText,
|
||||
Tag = script.FolderName,
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
|
||||
};
|
||||
|
||||
parentPanel.Children.Add(checkBox);
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
Content = displayText,
|
||||
Tag = script.FolderName,
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
|
||||
};
|
||||
parentPanel.Children.Add(checkBox);
|
||||
}
|
||||
else if (selectType == typeof(RadioButton))
|
||||
{
|
||||
var radioButton = new RadioButton
|
||||
{
|
||||
Content = displayText,
|
||||
Tag = script.FolderName,
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_"),
|
||||
GroupName = "JsScriptsRadioButtonGroup"
|
||||
};
|
||||
parentPanel.Children.Add(radioButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,7 +862,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
[RelayCommand]
|
||||
private void OnAddShell()
|
||||
{
|
||||
var str = PromptDialog.Prompt("执行 shell 操作存在极大风险!请勿输入你看不懂的指令!以免引发安全隐患并损坏系统!\n执行 shell 的时候,游戏可能会失去焦点","请输入需要执行的shell");
|
||||
var str = PromptDialog.Prompt("执行 shell 操作存在极大风险!请勿输入你看不懂的指令!以免引发安全隐患并损坏系统!\n执行 shell 的时候,游戏可能会失去焦点", "请输入需要执行的shell");
|
||||
if (!string.IsNullOrEmpty(str))
|
||||
{
|
||||
SelectedScriptGroup?.AddProject(ScriptGroupProject.BuildShellProject(str));
|
||||
@@ -1067,7 +1085,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
// 递归时,传递当前节点的匹配状态
|
||||
// 每个子节点深度相同,所以如果递归过程中任意子节点应该显示,则当前节点也应该显示
|
||||
if (ShouldShowNode(child, filter, isDeepSearch, currentDepth + 1, currentNodeMatches))
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1248,7 +1266,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
// return result;
|
||||
// }
|
||||
|
||||
private List<ScriptProject> LoadAllJsScriptProjects()
|
||||
internal static List<ScriptProject> LoadAllJsScriptProjects()
|
||||
{
|
||||
var path = Global.ScriptPath();
|
||||
Directory.CreateDirectory(path);
|
||||
@@ -1394,13 +1412,13 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async void OnDeleteScriptByFolder(ScriptGroupProject? item)
|
||||
public async void OnDeleteScriptByFolder(ScriptGroupProject? item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (SelectedScriptGroup != null)
|
||||
{
|
||||
var toBeDeletedProjects = SelectedScriptGroup.Projects
|
||||
@@ -1411,7 +1429,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
{
|
||||
SelectedScriptGroup.Projects.Remove(project);
|
||||
}
|
||||
|
||||
|
||||
_snackbarService.Show(
|
||||
"脚本配置移除成功",
|
||||
$"已移除 {item.FolderName} 下的所有关联配置",
|
||||
@@ -1657,13 +1675,13 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
RunnerContext.Instance.Reset();
|
||||
|
||||
TaskProgress taskProgress = new()
|
||||
{
|
||||
ScriptGroupNames = [SelectedScriptGroup.Name]
|
||||
};
|
||||
{
|
||||
ScriptGroupNames = [SelectedScriptGroup.Name]
|
||||
};
|
||||
RunnerContext.Instance.taskProgress = taskProgress;
|
||||
taskProgress.CurrentScriptGroupName = SelectedScriptGroup.Name;
|
||||
TaskProgressManager.SaveTaskProgress(taskProgress);
|
||||
await _scriptService.RunMulti(GetNextProjects(SelectedScriptGroup), SelectedScriptGroup.Name,taskProgress);
|
||||
await _scriptService.RunMulti(GetNextProjects(SelectedScriptGroup), SelectedScriptGroup.Name, taskProgress);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -1737,7 +1755,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
{
|
||||
SetTaskContextNextFlag(group);
|
||||
List<ScriptGroupProject> ls = new List<ScriptGroupProject>();
|
||||
if (group.Projects.Where(g=>g.NextFlag ?? false).Count() > 0)
|
||||
if (group.Projects.Where(g => g.NextFlag ?? false).Count() > 0)
|
||||
{
|
||||
bool start = false;
|
||||
foreach (var item in group.Projects)
|
||||
@@ -1780,17 +1798,17 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
|
||||
return ls;
|
||||
}
|
||||
|
||||
return group.Projects.Select(g=>g).ToList();
|
||||
|
||||
return group.Projects.Select(g => g).ToList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OnContinueMultiScriptGroupAsync()
|
||||
{
|
||||
|
||||
// 创建一个 StackPanel 来包含全选按钮和所有配置组的 CheckBox
|
||||
// 创建一个 StackPanel 来包含全选按钮和所有配置组的 CheckBox
|
||||
var stackPanel = new StackPanel();
|
||||
|
||||
|
||||
|
||||
// 添加分割线
|
||||
var separator = new Separator
|
||||
@@ -1800,21 +1818,21 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
stackPanel.Children.Add(separator);
|
||||
|
||||
List<TaskProgress> taskProgresses = TaskProgressManager.LoadAllTaskProgress();
|
||||
var checkBox = new ComboBox();;
|
||||
var checkBox = new ComboBox(); ;
|
||||
stackPanel.Children.Add(checkBox);
|
||||
ObservableCollection<KeyValuePair<string, string>> kvs=new ObservableCollection<KeyValuePair<string, string>>();
|
||||
ObservableCollection<KeyValuePair<string, string>> kvs = new ObservableCollection<KeyValuePair<string, string>>();
|
||||
foreach (var taskProgress in taskProgresses)
|
||||
{
|
||||
var name = taskProgress.Name+"_"+taskProgress.CurrentScriptGroupName+"_";
|
||||
var name = taskProgress.Name + "_" + taskProgress.CurrentScriptGroupName + "_";
|
||||
if (taskProgress.Loop)
|
||||
{
|
||||
name += "循环("+taskProgress.LoopCount+")_";
|
||||
name += "循环(" + taskProgress.LoopCount + ")_";
|
||||
}
|
||||
if (taskProgress.CurrentScriptGroupProjectInfo!=null)
|
||||
if (taskProgress.CurrentScriptGroupProjectInfo != null)
|
||||
{
|
||||
name = name +taskProgress.CurrentScriptGroupProjectInfo.Index+ "_" + taskProgress.CurrentScriptGroupProjectInfo.Name;
|
||||
name = name + taskProgress.CurrentScriptGroupProjectInfo.Index + "_" + taskProgress.CurrentScriptGroupProjectInfo.Name;
|
||||
}
|
||||
kvs.Add(new KeyValuePair<string, string>(taskProgress.Name,name));
|
||||
kvs.Add(new KeyValuePair<string, string>(taskProgress.Name, name));
|
||||
}
|
||||
|
||||
checkBox.SelectedValuePath = "Key";
|
||||
@@ -1822,7 +1840,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
checkBox.ItemsSource = kvs;
|
||||
checkBox.SelectedIndex = 0;
|
||||
//SelectedValuePath="Key"
|
||||
// DisplayMemberPath="Value"
|
||||
// DisplayMemberPath="Value"
|
||||
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
|
||||
{
|
||||
Title = "选择需要继续执行的进度记录",
|
||||
@@ -1831,7 +1849,8 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
Content = stackPanel,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Height = 300 // 设置固定高度
|
||||
,Width = 600
|
||||
,
|
||||
Width = 600
|
||||
},
|
||||
CloseButtonText = "关闭",
|
||||
PrimaryButtonText = "确认执行",
|
||||
@@ -1842,7 +1861,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
var result = await uiMessageBox.ShowDialogAsync();
|
||||
if (result == MessageBoxResult.Primary)
|
||||
{
|
||||
|
||||
|
||||
/*var selectedGroups = checkBoxes
|
||||
.Where(kv => kv.Value.IsChecked == true)
|
||||
.Select(kv => kv.Key)
|
||||
@@ -1857,7 +1876,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnContinueTaskProgressAsync(string name,List<TaskProgress>? taskProgresses = null)
|
||||
public async Task OnContinueTaskProgressAsync(string name, List<TaskProgress>? taskProgresses = null)
|
||||
{
|
||||
if (taskProgresses == null)
|
||||
{
|
||||
@@ -1873,24 +1892,24 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
taskProgress=taskProgresses.FirstOrDefault(t=>t.Name == name);
|
||||
taskProgress = taskProgresses.FirstOrDefault(t => t.Name == name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (taskProgress!=null)
|
||||
|
||||
|
||||
if (taskProgress != null)
|
||||
{
|
||||
//await StartGroups(selectedGroups);
|
||||
//taskProgress.Next
|
||||
var sg = ScriptGroups.ToList().Where(sg => taskProgress.ScriptGroupNames.Contains(sg.Name)).ToList();
|
||||
TaskProgressManager.GenerNextProjectInfo(taskProgress,sg);
|
||||
if (taskProgress.Next==null)
|
||||
TaskProgressManager.GenerNextProjectInfo(taskProgress, sg);
|
||||
if (taskProgress.Next == null)
|
||||
{
|
||||
_logger.LogWarning("无法定位到下一个要执行的项目:next为空("+taskProgress.Name+")");
|
||||
_logger.LogWarning("无法定位到下一个要执行的项目:next为空(" + taskProgress.Name + ")");
|
||||
}
|
||||
else
|
||||
{
|
||||
await StartGroups(sg,taskProgress);
|
||||
await StartGroups(sg, taskProgress);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1927,13 +1946,13 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
var stackPanel = new StackPanel();
|
||||
var checkBoxes = new Dictionary<ScriptGroup, CheckBox>();
|
||||
|
||||
|
||||
|
||||
var loopCheckBox = new CheckBox
|
||||
{
|
||||
Content = "循环",
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// 创建全选按钮
|
||||
var selectAllCheckBox = new CheckBox
|
||||
{
|
||||
@@ -2046,7 +2065,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
);
|
||||
return;
|
||||
}
|
||||
await StartGroups(selectedGroups,null,loopCheckBox.IsChecked ?? false);;
|
||||
await StartGroups(selectedGroups, null, loopCheckBox.IsChecked ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2057,7 +2076,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
|
||||
public async Task OnStartMultiScriptGroupWithNamesAsync(params string[] names)
|
||||
{
|
||||
if( ScriptGroups.Count == 0)
|
||||
if (ScriptGroups.Count == 0)
|
||||
{
|
||||
ReadScriptGroup();
|
||||
}
|
||||
@@ -2085,7 +2104,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartGroups(List<ScriptGroup> scriptGroups,TaskProgress? taskProgress = null,bool loop = false)
|
||||
public async Task StartGroups(List<ScriptGroup> scriptGroups, TaskProgress? taskProgress = null, bool loop = false)
|
||||
{
|
||||
_logger.LogInformation("开始连续执行选中配置组:{Names}", string.Join(",", scriptGroups.Select(x => x.Name)));
|
||||
try
|
||||
@@ -2096,7 +2115,8 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
taskProgress = new()
|
||||
{
|
||||
ScriptGroupNames = scriptGroups.Select(x => x.Name).ToList()
|
||||
,Loop = loop
|
||||
,
|
||||
Loop = loop
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2104,16 +2124,16 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
var sg = GetNextScriptGroups(scriptGroups);
|
||||
foreach (var scriptGroup in sg)
|
||||
{
|
||||
if (taskProgress.Next!=null)
|
||||
if (taskProgress.Next != null)
|
||||
{
|
||||
if (scriptGroup.Name!=taskProgress.Next.GroupName)
|
||||
if (scriptGroup.Name != taskProgress.Next.GroupName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
taskProgress.CurrentScriptGroupName = scriptGroup.Name;
|
||||
TaskProgressManager.SaveTaskProgress(taskProgress);
|
||||
await _scriptService.RunMulti(GetNextProjects(scriptGroup), scriptGroup.Name,taskProgress);
|
||||
await _scriptService.RunMulti(GetNextProjects(scriptGroup), scriptGroup.Name, taskProgress);
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
|
||||
@@ -2133,7 +2153,7 @@ public partial class ScriptControlViewModel : ViewModel
|
||||
taskProgress.EndTime = DateTime.Now;
|
||||
TaskProgressManager.SaveTaskProgress(taskProgress);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -1,44 +1,41 @@
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Script;
|
||||
using BetterGenshinImpact.Core.Script.Project;
|
||||
using BetterGenshinImpact.GameTask;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.AutoDomain;
|
||||
using BetterGenshinImpact.GameTask.AutoFight;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation;
|
||||
using BetterGenshinImpact.GameTask.AutoMusicGame;
|
||||
using BetterGenshinImpact.GameTask.AutoSkip.Model;
|
||||
using BetterGenshinImpact.GameTask.AutoTrackPath;
|
||||
using BetterGenshinImpact.GameTask.AutoWood;
|
||||
using BetterGenshinImpact.GameTask.Model;
|
||||
using BetterGenshinImpact.Service.Interface;
|
||||
using BetterGenshinImpact.View.Pages;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.System;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||||
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using Wpf.Ui;
|
||||
using Wpf.Ui.Violeta.Controls;
|
||||
using BetterGenshinImpact.ViewModel.Pages.View;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Collections.Frozen;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.AutoStygianOnslaught;
|
||||
using BetterGenshinImpact.View.Windows;
|
||||
using BetterGenshinImpact.GameTask.AutoWood;
|
||||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||||
using BetterGenshinImpact.GameTask.GetGridIcons;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using BetterGenshinImpact.GameTask.UseRedeemCode;
|
||||
using BetterGenshinImpact.Helpers;
|
||||
using BetterGenshinImpact.Service.Interface;
|
||||
using BetterGenshinImpact.View.Pages;
|
||||
using BetterGenshinImpact.View.Windows;
|
||||
using BetterGenshinImpact.ViewModel.Pages.View;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Windows.System;
|
||||
using Wpf.Ui;
|
||||
using Wpf.Ui.Violeta.Controls;
|
||||
using TextBox = Wpf.Ui.Controls.TextBox;
|
||||
|
||||
namespace BetterGenshinImpact.ViewModel.Pages;
|
||||
@@ -162,6 +159,16 @@ public partial class TaskSettingsPageViewModel : ViewModel
|
||||
[ObservableProperty]
|
||||
private bool _switchArtifactSalvageEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private FrozenDictionary<Enum, string> _recognitionFailurePolicyDict = Enum.GetValues(typeof(RecognitionFailurePolicy))
|
||||
.Cast<RecognitionFailurePolicy>()
|
||||
.ToFrozenDictionary(
|
||||
e => (Enum)e,
|
||||
e => e.GetType()
|
||||
.GetField(e.ToString())?
|
||||
.GetCustomAttribute<DescriptionAttribute>()?
|
||||
.Description ?? e.ToString());
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _switchGetGridIconsEnabled;
|
||||
[ObservableProperty]
|
||||
@@ -527,7 +534,13 @@ public partial class TaskSettingsPageViewModel : ViewModel
|
||||
{
|
||||
SwitchArtifactSalvageEnabled = true;
|
||||
await new TaskRunner()
|
||||
.RunSoloTaskAsync(new AutoArtifactSalvageTask(int.Parse(Config.AutoArtifactSalvageConfig.MaxArtifactStar), Config.AutoArtifactSalvageConfig.JavaScript, Config.AutoArtifactSalvageConfig.MaxNumToCheck));
|
||||
.RunSoloTaskAsync(new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(
|
||||
int.Parse(Config.AutoArtifactSalvageConfig.MaxArtifactStar),
|
||||
Config.AutoArtifactSalvageConfig.JavaScript,
|
||||
Config.AutoArtifactSalvageConfig.ArtifactSetFilter,
|
||||
Config.AutoArtifactSalvageConfig.MaxNumToCheck,
|
||||
Config.AutoArtifactSalvageConfig.RecognitionFailurePolicy
|
||||
)));
|
||||
SwitchArtifactSalvageEnabled = false;
|
||||
}
|
||||
|
||||
@@ -540,10 +553,56 @@ public partial class TaskSettingsPageViewModel : ViewModel
|
||||
[RelayCommand]
|
||||
private void OnOpenArtifactSalvageTestOCRWindow()
|
||||
{
|
||||
OcrDialog ocrDialog = new OcrDialog(0.70, 0.098, 0.24, 0.52, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript);
|
||||
OcrDialog ocrDialog = new OcrDialog(0.70, 0.112, 0.275, 0.50, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript);
|
||||
ocrDialog.ShowDialog();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OnCopyArtifactSalvageJavaScriptFromRepository()
|
||||
{
|
||||
var list = ScriptControlViewModel.LoadAllJsScriptProjects();
|
||||
var stackPanel = ScriptControlViewModel.CreateJsScriptSelectionPanel(list, typeof(RadioButton));
|
||||
|
||||
var result = PromptDialog.Prompt("请选择需要复制的JS脚本", "请选择需要复制的JS脚本", stackPanel, new Size(500, 600));
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
string? selectedFolderName = null;
|
||||
foreach (var child in ((Wpf.Ui.Controls.StackPanel)stackPanel.Content).Children)
|
||||
{
|
||||
if (child is RadioButton { IsChecked: true } radioButton && radioButton.Tag is string folderName)
|
||||
{
|
||||
selectedFolderName = folderName;
|
||||
}
|
||||
}
|
||||
if (selectedFolderName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptProject scriptProject = new ScriptProject(selectedFolderName);
|
||||
string jsCode = await scriptProject.LoadCode();
|
||||
|
||||
var multilineTextBox = new TextBox
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Text = jsCode,
|
||||
IsReadOnly = true
|
||||
};
|
||||
var p = new PromptDialog($"{scriptProject.Manifest.Name}\r\n{scriptProject.Manifest.ShortDescription}\r\n\r\n将覆盖现有的JavaScript,是否继续?", $"预览 - {scriptProject.FolderName}", multilineTextBox, null);
|
||||
p.Height = 600;
|
||||
p.MaxWidth = 800;
|
||||
p.ShowDialog();
|
||||
|
||||
if (p.DialogResult != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Config.AutoArtifactSalvageConfig.JavaScript = jsCode;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OnSwitchGetGridIcons()
|
||||
{
|
||||
|
||||
@@ -74,7 +74,11 @@ public partial class TriggerSettingsPageViewModel : ViewModel
|
||||
"牢固的箭簇"
|
||||
};
|
||||
var p = new PromptDialog(
|
||||
"黑名单配置,每行一条记录",
|
||||
"黑名单配置,每行一条记录\n"+
|
||||
"示例:\n" +
|
||||
"精致的宝箱\n" +
|
||||
"史莱姆凝液\n" +
|
||||
"牢固的箭簇",
|
||||
"黑名单配置",
|
||||
multilineTextBox,
|
||||
text);
|
||||
@@ -111,7 +115,11 @@ public partial class TriggerSettingsPageViewModel : ViewModel
|
||||
"启动"
|
||||
};
|
||||
var p = new PromptDialog(
|
||||
"白名单配置,每行一条记录",
|
||||
"白名单配置,每行一条记录\n" +
|
||||
"示例:\n" +
|
||||
"调查\n" +
|
||||
"合成\n" +
|
||||
"启动",
|
||||
"白名单配置",
|
||||
multilineTextBox,
|
||||
text);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Script.Group;
|
||||
using BetterGenshinImpact.View.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.System;
|
||||
using Wpf.Ui.Violeta.Controls;
|
||||
|
||||
namespace BetterGenshinImpact.ViewModel.Pages.View;
|
||||
@@ -96,4 +97,10 @@ public partial class ScriptGroupConfigViewModel : ObservableObject, IViewModel
|
||||
{
|
||||
PathingConfig.Enabled = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OnGoToAutoEatUrlAsync()
|
||||
{
|
||||
await Launcher.LaunchUriAsync(new Uri("https://bettergi.com/dev/js/dispatcher.html#autoeat-自动吃食物"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterGenshinImpact.Core.Recognition.ONNX;
|
||||
using BetterGenshinImpact.Core.Recognition.ONNX;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR.Paddle;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
|
||||
using BetterGenshinImpact.GameTask.Model.GameUI;
|
||||
using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -13,12 +14,15 @@ using static BetterGenshinImpact.GameTask.AutoArtifactSalvage.AutoArtifactSalvag
|
||||
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
|
||||
{
|
||||
[Collection("Init Collection")]
|
||||
public partial class AutoArtifactSalvageTaskTests
|
||||
public class AutoArtifactSalvageTaskTests
|
||||
{
|
||||
private readonly PaddleFixture paddle;
|
||||
public AutoArtifactSalvageTaskTests(PaddleFixture paddle)
|
||||
private readonly IStringLocalizer<AutoArtifactSalvageTask> stringLocalizer;
|
||||
|
||||
public AutoArtifactSalvageTaskTests(PaddleFixture paddle, LocalizationFixture localization)
|
||||
{
|
||||
this.paddle = paddle;
|
||||
this.stringLocalizer = localization.CreateStringLocalizer<AutoArtifactSalvageTask>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -94,10 +98,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
|
||||
CultureInfo cultureInfo = new CultureInfo("zh-Hans");
|
||||
|
||||
//
|
||||
AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger());
|
||||
string result = PaddleResultDic.GetOrAdd(screenshot, screenshot_ =>
|
||||
{
|
||||
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot_}");
|
||||
GetArtifactStat(mat, paddle.Get(), cultureInfo, out string allText);
|
||||
sut.GetArtifactStat(mat, paddle.Get(), out string allText);
|
||||
return allText;
|
||||
});
|
||||
|
||||
@@ -112,30 +117,105 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetArtifactStatTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] { "ArtifactAffixes.png", new ArtifactStat("异种的期许", new ArtifactAffix(ArtifactAffixType.HP, 717f), [
|
||||
new ArtifactAffix(ArtifactAffixType.ElementalMastery, 16f),
|
||||
new ArtifactAffix(ArtifactAffixType.EnergyRecharge, 6.5f),
|
||||
new ArtifactAffix(ArtifactAffixType.ATKPercent, 5.8f),
|
||||
new ArtifactAffix(ArtifactAffixType.DEF, 23f)
|
||||
], 0), new CultureInfo("zh-Hans") };
|
||||
yield return new object[] { "202508242224_GetArtifactStat.png", new ArtifactStat("裁判的时刻", new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [
|
||||
new ArtifactAffix(ArtifactAffixType.ATK, 16f),
|
||||
new ArtifactAffix(ArtifactAffixType.CRITDMG, 7.8f),
|
||||
new ArtifactAffix(ArtifactAffixType.DEF, 19),
|
||||
new ArtifactAffix(ArtifactAffixType.CRITRate, 2.7f)
|
||||
], 0), new CultureInfo("zh-Hans") };
|
||||
yield return new object[] { "202508252004_GetArtifactStat.png", new ArtifactStat("Deep Gallery's Bestowed Banquet", new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [
|
||||
new ArtifactAffix(ArtifactAffixType.HP, 239),
|
||||
new ArtifactAffix(ArtifactAffixType.ATK, 18),
|
||||
new ArtifactAffix(ArtifactAffixType.HPPercent, 4.1f)
|
||||
], 0), new CultureInfo("en") };
|
||||
yield return new object[] { "20250827000123_GetArtifactStat.png", new ArtifactStat("Pacte distant des galeries profondes", new ArtifactAffix(ArtifactAffixType.ATK, 47), [
|
||||
new ArtifactAffix(ArtifactAffixType.DEFPercent, 7.3f),
|
||||
new ArtifactAffix(ArtifactAffixType.DEF, 16),
|
||||
new ArtifactAffix(ArtifactAffixType.HPPercent, 5.3f)
|
||||
], 0), new CultureInfo("fr") };
|
||||
yield return new object[] { "20250827000246_GetArtifactStat.png", new ArtifactStat("Reckoning of the Xenogenic", new ArtifactAffix(ArtifactAffixType.HP, 717f), [
|
||||
new ArtifactAffix(ArtifactAffixType.ElementalMastery, 16),
|
||||
new ArtifactAffix(ArtifactAffixType.EnergyRecharge, 6.5f),
|
||||
new ArtifactAffix(ArtifactAffixType.ATKPercent, 5.8f),
|
||||
new ArtifactAffix(ArtifactAffixType.DEF, 23)
|
||||
], 0), new CultureInfo("en") };
|
||||
yield return new object[] { "20250827151043_GetArtifactStat.png", new ArtifactStat("Couronne de laurier", new ArtifactAffix(ArtifactAffixType.ATKPercent, 7.0f), [
|
||||
new ArtifactAffix(ArtifactAffixType.CRITDMG, 5.4f),
|
||||
new ArtifactAffix(ArtifactAffixType.DEFPercent, 7.3f),
|
||||
new ArtifactAffix(ArtifactAffixType.DEF, 16),
|
||||
new ArtifactAffix(ArtifactAffixType.HPPercent, 5.8f)
|
||||
], 0), new CultureInfo("fr") }; // 一个百里挑一的识别失败的例子
|
||||
yield return new object[] { "20250827163340_GetArtifactStat.png", new ArtifactStat("Couronne de Watatsumi", new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [
|
||||
new ArtifactAffix(ArtifactAffixType.HP, 299),
|
||||
new ArtifactAffix(ArtifactAffixType.EnergyRecharge, 4.5f),
|
||||
new ArtifactAffix(ArtifactAffixType.CRITDMG, 6.2f),
|
||||
new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23)
|
||||
], 0), new CultureInfo("fr") };
|
||||
yield return new object[] { "20250827204545_GetArtifactStat.png", new ArtifactStat("Agitation de la nuit dorée", new ArtifactAffix(ArtifactAffixType.GeoDMGBonus, 7.0f), [
|
||||
new ArtifactAffix(ArtifactAffixType.HP, 269),
|
||||
new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23),
|
||||
new ArtifactAffix(ArtifactAffixType.CRITRate, 3.1f)
|
||||
], 0), new CultureInfo("fr") };
|
||||
yield return new object[] { "20250828084843_GetArtifactStat.png", new ArtifactStat("異種的期許", new ArtifactAffix(ArtifactAffixType.HP, 717f), [
|
||||
new ArtifactAffix(ArtifactAffixType.DEFPercent, 7.3f),
|
||||
new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23),
|
||||
new ArtifactAffix(ArtifactAffixType.ATKPercent, 4.1f)
|
||||
], 0), new CultureInfo("zh-Hant") };
|
||||
yield return new object[] { "20250828093344_GetArtifactStat.png", new ArtifactStat("黃金時代的先聲",new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [
|
||||
new ArtifactAffix(ArtifactAffixType.DEF, 19),
|
||||
new ArtifactAffix(ArtifactAffixType.CRITDMG, 7.8f),
|
||||
new ArtifactAffix(ArtifactAffixType.ATK, 18),
|
||||
new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23)
|
||||
], 0), new CultureInfo("zh-Hant") };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试获取分解圣遗物界面右侧圣遗物的各种结构化信息,结果应正确
|
||||
/// </summary>
|
||||
/// <param name="screenshot"></param>
|
||||
[Theory]
|
||||
[InlineData(@"ArtifactAffixes.png")]
|
||||
public void GetArtifactStat_AffixesShouldBeRight(string screenshot)
|
||||
[MemberData(nameof(GetArtifactStatTestData))]
|
||||
public void GetArtifactStat_AffixesShouldBeRight(string screenshot, ArtifactStat expectedArtifactStat, CultureInfo cultureInfo)
|
||||
{
|
||||
//
|
||||
CultureInfo cultureInfo = new CultureInfo("zh-Hans");
|
||||
|
||||
//
|
||||
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}");
|
||||
ArtifactStat artifact = GetArtifactStat(mat, paddle.Get(), cultureInfo, out string _);
|
||||
|
||||
/*
|
||||
using var gray = mat.CvtColor(ColorConversionCodes.BGR2GRAY);
|
||||
using Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15));
|
||||
using var bottomHat = gray.MorphologyEx(MorphTypes.TopHat, kernel);
|
||||
Cv2.ImShow("bottomHat", bottomHat);
|
||||
Mat threshold = new Mat();
|
||||
var otsu = Cv2.Threshold(bottomHat, threshold, 30, 255, ThresholdTypes.Binary);
|
||||
Cv2.ImShow($"thres = {otsu}", threshold);
|
||||
Cv2.WaitKey();
|
||||
*/
|
||||
|
||||
//
|
||||
Assert.Equal("异种的期许", artifact.Name);
|
||||
Assert.True(artifact.MainAffix.Type == ArtifactAffixType.HP);
|
||||
Assert.True(artifact.MainAffix.Value == 717f);
|
||||
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.ElementalMastery && a.Value == 16f);
|
||||
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.EnergyRecharge && a.Value == 6.5f);
|
||||
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.ATKPercent && a.Value == 5.8f);
|
||||
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.DEF && a.Value == 23f);
|
||||
Assert.True(artifact.Level == 0);
|
||||
AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger());
|
||||
ArtifactStat result = sut.GetArtifactStat(mat, paddle.Get(cultureInfo.Name), out string _);
|
||||
|
||||
//
|
||||
Assert.Equal(expectedArtifactStat.Name, result.Name);
|
||||
Assert.Equal(expectedArtifactStat.MainAffix.Type, result.MainAffix.Type);
|
||||
Assert.Equal(expectedArtifactStat.MainAffix.Value, result.MainAffix.Value);
|
||||
foreach (ArtifactAffix expectedArtifactAffix in expectedArtifactStat.MinorAffixes)
|
||||
{
|
||||
Assert.Contains(result.MinorAffixes, a =>
|
||||
a.Type == expectedArtifactAffix.Type && a.Value == expectedArtifactAffix.Value);
|
||||
}
|
||||
Assert.True(result.Level == expectedArtifactStat.Level);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -153,11 +233,12 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
|
||||
public void IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected)
|
||||
{
|
||||
//
|
||||
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}");
|
||||
CultureInfo cultureInfo = new CultureInfo("zh-Hans");
|
||||
|
||||
//
|
||||
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}");
|
||||
ArtifactStat artifact = GetArtifactStat(mat, paddle.Get(), cultureInfo, out string _);
|
||||
AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger());
|
||||
ArtifactStat artifact = sut.GetArtifactStat(mat, paddle.Get(), out string _);
|
||||
bool result = IsMatchJavaScript(artifact, js);
|
||||
|
||||
//
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BehaviourTree;
|
||||
using BehaviourTree;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
@@ -17,7 +17,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(@"20250225101300361_ChooseBait_Succeeded.png", new string[] { "medaka", "butterflyfish", "butterflyfish", "pufferfish" })]
|
||||
[InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka", "medaka" })]
|
||||
[InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka", "medaka" })] // todo 更新用例
|
||||
[InlineData(@"202503160917566615@900p.png", new string[] { "pufferfish" })]
|
||||
/// <summary>
|
||||
/// 测试各种选取鱼饵,结果为成功
|
||||
@@ -35,7 +35,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
};
|
||||
|
||||
//
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator());
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes);
|
||||
BehaviourStatus actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
@@ -69,11 +69,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
FakeTimeProvider fakeTimeProvider = new FakeTimeProvider(dateTime);
|
||||
|
||||
//
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), fakeTimeProvider);
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes, fakeTimeProvider);
|
||||
BehaviourStatus actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.True(blackboard.chooseBaitUIOpening);
|
||||
Assert.Equal(BehaviourStatus.Running, actual);
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.False(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.NotNull(blackboard.selectedBait);
|
||||
Assert.True(blackboard.chooseBaitUIOpening);
|
||||
Assert.Equal(BehaviourStatus.Running, actual);
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.False(blackboard.chooseBaitUIOpening);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
}
|
||||
@@ -124,15 +124,15 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
|
||||
#region 第1次失败
|
||||
//
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), fakeTimeProvider);
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes, fakeTimeProvider);
|
||||
BehaviourStatus actual = sut.Tick(imageRegion);
|
||||
fakeTimeProvider.SetUtcNow(dateTime.AddSeconds(3));
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "fake fly bait"));
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait));
|
||||
#endregion
|
||||
|
||||
#region 第2次失败
|
||||
@@ -146,9 +146,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "fake fly bait").Count());
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait).Count());
|
||||
Assert.False(blackboard.abort);
|
||||
#endregion
|
||||
|
||||
@@ -165,9 +165,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait"));
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait));
|
||||
#endregion
|
||||
|
||||
#region sunfish受到遮挡,medaka再次出现,第4次成功,并钓起medaka
|
||||
@@ -183,9 +183,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.False(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.NotNull(blackboard.selectedBait); // todo 更新用例
|
||||
Assert.Equal(BehaviourStatus.Succeeded, actual);
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait"));
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait));
|
||||
#endregion
|
||||
|
||||
#region sunfish再次出现,第5次失败
|
||||
@@ -201,9 +201,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait").Count());
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait).Count());
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -230,15 +230,15 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
|
||||
#region 第1次失败
|
||||
//
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), fakeTimeProvider);
|
||||
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes, fakeTimeProvider);
|
||||
BehaviourStatus actual = sut.Tick(imageRegion);
|
||||
fakeTimeProvider.SetUtcNow(dateTime.AddSeconds(3));
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "fake fly bait"));
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait));
|
||||
#endregion
|
||||
|
||||
#region koi受到遮挡,第2次失败
|
||||
@@ -254,9 +254,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait"));
|
||||
Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait));
|
||||
Assert.False(blackboard.abort);
|
||||
#endregion
|
||||
|
||||
@@ -273,9 +273,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "fake fly bait").Count());
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait).Count());
|
||||
#endregion
|
||||
|
||||
#region 第4次失败
|
||||
@@ -291,9 +291,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName));
|
||||
Assert.Null(blackboard.selectedBait);
|
||||
Assert.Equal(BehaviourStatus.Failed, actual);
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait").Count());
|
||||
Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait).Count());
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BehaviourTree;
|
||||
using BehaviourTree;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.Model.Area.Converter;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
@@ -6,8 +6,6 @@ using BehaviourTree.Composites;
|
||||
using BehaviourTree.FluentBuilder;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using OpenCvSharp;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
{
|
||||
@@ -106,17 +104,12 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}");
|
||||
var imageRegion = new GameCaptureRegion(mat, 0, 0, new DesktopRegion(new FakeMouseSimulator()), converter: new ScaleConverter(1d), drawContent: new FakeDrawContent());
|
||||
|
||||
ServiceCollection services = new ServiceCollection();
|
||||
services.AddLogging().AddLocalization();
|
||||
using ServiceProvider sp = services.BuildServiceProvider();
|
||||
IStringLocalizer<AutoFishingImageRecognition> autoFishingImageRecognitionStringLocalizer = sp.GetRequiredService<IStringLocalizer<AutoFishingImageRecognition>>();
|
||||
|
||||
FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1);
|
||||
AutoFishingAssets autoFishingAssets = new AutoFishingAssets(systemInfo);
|
||||
Blackboard blackboard = new Blackboard(autoFishingAssets: autoFishingAssets);
|
||||
|
||||
//
|
||||
FishBite sut = new FishBite("-", blackboard, new FakeLogger(), false, new FakeInputSimulator(), OcrService, drawContent: new FakeDrawContent(), new System.Globalization.CultureInfo(cultureName), autoFishingImageRecognitionStringLocalizer);
|
||||
FishBite sut = new FishBite("-", blackboard, new FakeLogger(), false, new FakeInputSimulator(), OcrService, drawContent: new FakeDrawContent(), new System.Globalization.CultureInfo(cultureName), stringLocalizer);
|
||||
BehaviourStatus actual = sut.Tick(imageRegion);
|
||||
|
||||
//
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BehaviourTree;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using OpenCvSharp;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
|
||||
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
{
|
||||
@@ -38,10 +39,10 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("20250225101257889_GetFishpond_Succeeded.png", new string[] { "fruit paste bait", "fruit paste bait", "redrot bait", "redrot bait" }, new string[] { "false worm bait", "false worm bait", "fake fly bait", "fake fly bait" })]
|
||||
[InlineData("20250225101257889_GetFishpond_Succeeded.png", new BaitType[] { BaitType.FruitPasteBait, BaitType.FruitPasteBait, BaitType.RedrotBait, BaitType.RedrotBait }, new BaitType[] { BaitType.FalseWormBait, BaitType.FalseWormBait, BaitType.FakeFlyBait, BaitType.FakeFlyBait })]
|
||||
/// 测试鱼的鱼饵均在失败列表中且被忽略,结果为运行中
|
||||
/// </summary>
|
||||
public void GetFishpondTest_AllIgnored_ShouldBeRunning(string screenshot1080p, IEnumerable<string> chooseBaitfailures, IEnumerable<string> throwRodNoTargetFishfailures)
|
||||
public void GetFishpondTest_AllIgnored_ShouldBeRunning(string screenshot1080p, IEnumerable<BaitType> chooseBaitfailures, IEnumerable<BaitType> throwRodNoTargetFishfailures)
|
||||
{
|
||||
//
|
||||
Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BehaviourTree;
|
||||
using BehaviourTree;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using BetterGenshinImpact.GameTask.Model.Area.Converter;
|
||||
using BetterGenshinImpact.GameTask.Model.Area;
|
||||
@@ -6,18 +6,19 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using OpenCvSharp;
|
||||
using BehaviourTree.Composites;
|
||||
using BehaviourTree.FluentBuilder;
|
||||
using BetterGenshinImpact.GameTask.AutoFishing.Model;
|
||||
|
||||
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
{
|
||||
public partial class BehavioursTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "false worm bait")]
|
||||
[InlineData(@"20250226162217468_ThrowRod_Succeeded.png", "fruit paste bait")]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FalseWormBait)]
|
||||
[InlineData(@"20250226162217468_ThrowRod_Succeeded.png", BaitType.FruitPasteBait)]
|
||||
/// <summary>
|
||||
/// 测试各种抛竿,结果为成功
|
||||
/// </summary>
|
||||
public void ThrowRodTest_VariousFish_ShouldSuccess(string screenshot1080p, string selectedBaitName)
|
||||
public void ThrowRodTest_VariousFish_ShouldSuccess(string screenshot1080p, BaitType selectedBait)
|
||||
{
|
||||
//
|
||||
Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}");
|
||||
@@ -25,7 +26,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
|
||||
var blackboard = new Blackboard(Predictor, sleep: i => { })
|
||||
{
|
||||
selectedBaitName = selectedBaitName
|
||||
selectedBait = selectedBait
|
||||
};
|
||||
|
||||
FakeTimeProvider fakeTimeProvider = new FakeTimeProvider();
|
||||
@@ -40,12 +41,12 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "redrot bait")]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "fake fly bait")]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.RedrotBait)]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FakeFlyBait)]
|
||||
/// <summary>
|
||||
/// 测试各种抛竿,未满足HutaoFisher判定,结果为运行中
|
||||
/// </summary>
|
||||
public void ThrowRodTest_VariousFish_ShouldFail(string screenshot1080p, string selectedBaitName)
|
||||
public void ThrowRodTest_VariousFish_ShouldFail(string screenshot1080p, BaitType selectedBait)
|
||||
{
|
||||
//
|
||||
Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}");
|
||||
@@ -53,7 +54,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
|
||||
var blackboard = new Blackboard(Predictor, sleep: i => { })
|
||||
{
|
||||
selectedBaitName = selectedBaitName
|
||||
selectedBait = selectedBait
|
||||
};
|
||||
|
||||
FakeTimeProvider fakeTimeProvider = new FakeTimeProvider();
|
||||
@@ -68,11 +69,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "flashing maintenance mek bait")]
|
||||
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FlashingMaintenanceMekBait)]
|
||||
/// <summary>
|
||||
/// 测试各种抛竿,无鱼饵适用鱼,结果为失败
|
||||
/// </summary>
|
||||
public void ThrowRodTest_NoBaitFish_ShouldFail(string screenshot1080p, string selectedBaitName)
|
||||
public void ThrowRodTest_NoBaitFish_ShouldFail(string screenshot1080p, BaitType selectedBait)
|
||||
{
|
||||
//
|
||||
Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}");
|
||||
@@ -80,7 +81,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
|
||||
var blackboard = new Blackboard(Predictor, sleep: i => { })
|
||||
{
|
||||
selectedBaitName = selectedBaitName
|
||||
selectedBait = selectedBait
|
||||
};
|
||||
|
||||
FakeTimeProvider fakeTimeProvider = new FakeTimeProvider();
|
||||
@@ -119,7 +120,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
|
||||
|
||||
var blackboard = new Blackboard(Predictor, sleep: i => { })
|
||||
{
|
||||
selectedBaitName = "fake fly bait"
|
||||
selectedBait = GameTask.AutoFishing.Model.BaitType.FakeFlyBait
|
||||
};
|
||||
|
||||
//
|
||||
|
||||