Merge branch 'main' into d-v3

This commit is contained in:
辉鸭蛋
2025-09-18 23:28:38 +08:00
76 changed files with 3178 additions and 593 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>BetterGI</AssemblyName>
<Version>0.49.1-alpha.3</Version>
<Version>0.50.1-alpha.3</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
@@ -43,9 +43,9 @@
<PackageReference Include="BehaviourTree" Version="1.0.73" />
<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.10" />
<PackageReference Include="BetterGI.Assets.Other" Version="1.0.8" />
<PackageReference Include="BetterGI.Assets.Map" Version="1.0.11" />
<PackageReference Include="BetterGI.Assets.Model" Version="1.0.12" />
<PackageReference Include="BetterGI.Assets.Other" Version="1.0.9" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="DeviceId" Version="6.9.0" />

View File

@@ -6,15 +6,26 @@ namespace BetterGenshinImpact.Core.Script;
public class CancellationContext : Singleton<CancellationContext>
{
public CancellationTokenSource Cts { get; set; } = new();
public bool IsManualStop { get; private set; }
private bool disposed;
public void Set()
{
Cts = new CancellationTokenSource();
IsManualStop = false;
disposed = false;
}
public void ManualCancel()
{
if (!disposed)
{
IsManualStop = true;
Cts.Cancel();
}
}
public void Cancel()
{
if (!disposed)

View File

@@ -260,8 +260,8 @@ public class Dispatcher
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);
string itemName = ScriptObjectConverter.GetValue<string?>((ScriptObject)soloTask.Config, "itemName", null) ?? throw new Exception("itemName为空");
return await new CountInventoryItem(gridScreenName, itemName).Start(cancellationToken);
}
default:
throw new ArgumentException($"未知的任务名称: {soloTask.Name}", nameof(soloTask.Name));

View File

@@ -113,6 +113,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
await Task.Run(() =>
{
Repository? repo = null;
try
{
GlobalSettings.SetOwnerValidation(false);
@@ -151,7 +152,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
return;
}
using var repo = new Repository(repoPath);
repo = new Repository(repoPath);
// 检查远程URL是否需要更新
var origin = repo.Network.Remotes["origin"];
@@ -159,6 +160,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
{
// 远程URL已更改需要删除重新克隆
_logger.LogInformation($"远程URL已更改: 从 {origin.Url} 到 {repoUrl},将重新克隆");
repo?.Dispose();
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
updated = true;
return;
@@ -170,7 +172,8 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
var fetchOptions = new FetchOptions
{
ProxyOptions = { ProxyType = ProxyType.None }
ProxyOptions = { ProxyType = ProxyType.None },
Depth = 1 // 浅拉取,只获取最新的提交
};
Commands.Fetch(repo, remote.Name, refSpecs, fetchOptions, "拉取最新更新");
@@ -198,6 +201,7 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
_logger.LogInformation($"检测到远程更新: 本地 {currentCommitSha[..7]} -> 远程 {remoteCommitSha[..7]},将重新克隆");
// commit不一致删除本地仓库重新克隆
repo?.Dispose();
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
updated = true;
}
@@ -207,54 +211,48 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
{
_logger.LogError(ex, "Git仓库更新失败");
UIDispatcherHelper.Invoke(() => Toast.Error("脚本仓库更新异常,直接删除后重新克隆\n原因" + ex.Message));
repo?.Dispose();
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
}
finally
{
repo?.Dispose();
}
});
// 如果仓库有更新,则标记新repo.json中的更新节点
if (updated)
// 标记新repo.json中的更新节点
try
{
try
var newRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories)
.FirstOrDefault();
if (newRepoJsonPath != null)
{
var newRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories)
.FirstOrDefault();
if (newRepoJsonPath != null)
var newRepoJsonContent = await File.ReadAllTextAsync(newRepoJsonPath);
// 检查是否存在repo_update.json如果存在则直接与它比对
var repoUpdateJsonPath = Path.Combine(ReposPath, "repo_updated.json");
string updatedContent;
if (File.Exists(repoUpdateJsonPath))
{
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
{
var repoUpdateContent = await File.ReadAllTextAsync(repoUpdateJsonPath);
updatedContent = AddUpdateMarkersToNewRepo(repoUpdateContent, newRepoJsonContent);
}
catch (Exception ex)
{
updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent ?? "", newRepoJsonContent);
}
}
else
{
// 如果没有repo_update.json则使用备份的旧内容进行比对
updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent ?? "", newRepoJsonContent);
}
// 保存到同级目录
var updatedRepoJsonPath = Path.Combine(parentDir!, "repo_updated.json");
await File.WriteAllTextAsync(updatedRepoJsonPath, updatedContent);
_logger.LogInformation($"已标记repo.json中的更新节点并保存到: {updatedRepoJsonPath}");
var repoUpdateContent = await File.ReadAllTextAsync(repoUpdateJsonPath);
updatedContent = AddUpdateMarkersToNewRepo(repoUpdateContent, newRepoJsonContent);
}
else
{
// 如果没有repo_update.json则使用备份的旧内容进行比对
updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent ?? "", newRepoJsonContent);
}
// 保存到同级目录
var updatedRepoJsonPath = Path.Combine(ReposPath, "repo_updated.json");
await File.WriteAllTextAsync(updatedRepoJsonPath, updatedContent);
_logger.LogInformation($"已标记repo.json中的更新节点并保存到: {updatedRepoJsonPath}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "标记repo.json更新节点失败");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "标记repo.json更新节点失败");
}
return (repoPath, updated);
@@ -314,6 +312,13 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
// 检查节点本身是否有更新
if (oldNode != null)
{
// 若历史上已标记,则保留该标记
if (IsTruthy(oldNode["hasUpdate"]) || IsTruthy(oldNode["hasUpdated"]))
{
newNode["hasUpdate"] = true;
hasDirectUpdate = true;
}
// 对比时间戳
var oldTime = ParseLastUpdated(oldNode["lastUpdated"]?.ToString());
var newTime = ParseLastUpdated(newNode["lastUpdated"]?.ToString());
@@ -348,10 +353,18 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
// 如果是叶子节点更新,父节点也标记更新
var isLeafChild = newChildObj["children"] == null ||
!((JArray?)newChildObj["children"])?.Any() == true;
if (isLeafChild && !hasDirectUpdate && newChildObj["hasUpdate"] != null)
if (isLeafChild && (IsTruthy(newChildObj["hasUpdate"]) || IsTruthy(newChildObj["hasUpdated"])))
{
var parentTime = ParseLastUpdated(newNode["lastUpdated"]?.ToString());
var childTime = ParseLastUpdated(newChildObj["lastUpdated"]?.ToString());
newNode["hasUpdate"] = true;
hasDirectUpdate = true;
if (childTime > parentTime && newChildObj["lastUpdated"] != null)
{
newNode["lastUpdated"] = newChildObj["lastUpdated"];
}
}
}
}
@@ -385,6 +398,14 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
}
}
private bool IsTruthy(JToken? token)
{
if (token == null || token.Type == JTokenType.Null) return false;
if (token.Type == JTokenType.Boolean) return (bool)token;
if (token.Type == JTokenType.String) return string.Equals((string)token, "true", StringComparison.OrdinalIgnoreCase);
return false;
}
private static void SimpleCloneRepository(string repoUrl, string repoPath,
CheckoutProgressHandler? onCheckoutProgress)
{
@@ -417,32 +438,41 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
Directory.CreateDirectory(repoPath);
Repository.Init(repoPath);
using var repo = new Repository(repoPath);
GitConfig(repo);
var repo = new Repository(repoPath);
// 添加远程源
Remote remote = repo.Network.Remotes.Add("origin", repoUrl);
// 只拉取指定分支
var fetchOptions = new FetchOptions
try
{
TagFetchMode = TagFetchMode.None,
ProxyOptions = { ProxyType = ProxyType.None }
};
string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}";
Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取");
GitConfig(repo);
// 获取远程分支
var remoteBranch = repo.Branches[$"refs/remotes/origin/{branchName}"];
if (remoteBranch == null)
throw new Exception($"远程仓库中未找到 {branchName} 分支");
// 添加远程源
Remote remote = repo.Network.Remotes.Add("origin", repoUrl);
// 创建并检出本地分支
var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip);
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
// 只拉取指定分支
var fetchOptions = new FetchOptions
{
TagFetchMode = TagFetchMode.None,
ProxyOptions = { ProxyType = ProxyType.None },
Depth = 1 // 浅拉取,只获取最新的提交
};
string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}";
Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取");
var checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress };
Commands.Checkout(repo, localBranch, checkoutOptions);
// 获取远程分支
var remoteBranch = repo.Branches[$"refs/remotes/origin/{branchName}"];
if (remoteBranch == null)
throw new Exception($"远程仓库中未找到 {branchName} 分支");
// 创建并检出本地分支
var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip);
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
var checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress };
Commands.Checkout(repo, localBranch, checkoutOptions);
}
finally
{
repo?.Dispose();
}
}
private void GitConfig(Repository repo)

View File

@@ -73,7 +73,7 @@ public class AutoArtifactSalvageTask : ISoloTask
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();
var stringLocalizer = param.StringLocalizer;
this.cultureInfo = param.GameCultureInfo;
quickSelectLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "快速选择");
numOfStarLocalizedString =
@@ -136,11 +136,21 @@ public class AutoArtifactSalvageTask : ISoloTask
// B键打开背包
input.SimulateAction(GIActions.OpenInventory);
await Delay(1000, ct);
await Delay(1200, ct);
var openBagSuccess = await NewRetry.WaitForAction(() =>
{
using var ra = CaptureToRectArea();
// 判断是否在提示对话框(物品过期提示)
if (Bv.IsInPromptDialog(ra))
{
// 如果存在物品过期提示,则点击确认按钮
Bv.ClickWhiteConfirmButton(ra.DeriveCrop(0, 0, ra.Width, ra.Height - ra.Height / 0.2));
Sleep(300, ct);
return false;
}
using var artifactBtn = ra.Find(recognitionObjectChecked);
if (artifactBtn.IsEmpty())
{
@@ -203,15 +213,22 @@ public class AutoArtifactSalvageTask : ISoloTask
// 快速选择
using var ra3 = CaptureToRectArea();
var ocrList = ra3.FindMulti(RecognitionObject.Ocr(ra3.ToRect().CutLeftBottom(0.25, 0.1)));
bool quickSelectBtnFound = false;
foreach (var ocr in ocrList)
{
if (Regex.IsMatch(ocr.Text, quickSelectLocalizedString))
{
quickSelectBtnFound = true;
ocr.Click();
await Delay(500, ct);
break;
}
}
if (!quickSelectBtnFound)
{
logger.LogError("没有找到可匹配{regex}的按钮,终止分解", quickSelectLocalizedString);
return;
}
// 确认选择
// 5.5 变成反选
@@ -221,15 +238,22 @@ public class AutoArtifactSalvageTask : ISoloTask
List<Region> ocrList2 = ra4.FindMulti(RecognitionObject.Ocr(ra4.ToRect().CutLeft(0.20)));
for (int i = star; i < 4; i++)
{
bool numOfStarFound = false;
foreach (var ocr in ocrList2)
{
if (Regex.IsMatch(ocr.Text, numOfStarLocalizedString[i]))
{
numOfStarFound = true;
ocr.Click();
await Delay(500, ct);
break;
}
}
if (!numOfStarFound)
{
logger.LogError("没有找到可匹配{regex}的按钮,终止分解", numOfStarLocalizedString[i]);
return;
}
}
}
@@ -525,7 +549,7 @@ public class AutoArtifactSalvageTask : ISoloTask
#region
ArtifactAffix[] minorAffixes = levelAndMinorAffixLines.Select(l =>
{
string pattern = @"^[•·]?([^+:]+)\+([\d., ]*)(%?)$";
string pattern = @"^([^+:]+)\+([\d., ]*)(%?).*$";
pattern = pattern.Replace("%", percentStr);
Match match = Regex.Match(l, pattern);
if (match.Success)

View File

@@ -67,6 +67,7 @@ public class AutoDomainTask : ISoloTask
private readonly string matchingChallengeString;
private readonly string rapidformationString;
private readonly string limitedFullyString;
private readonly string limitedFullyAllString;
private List<ResinUseRecord> _resinPriorityListWhenSpecifyUse;
@@ -93,6 +94,7 @@ public class AutoDomainTask : ISoloTask
this.matchingChallengeString = stringLocalizer.WithCultureGet(cultureInfo, "匹配挑战");
this.rapidformationString = stringLocalizer.WithCultureGet(cultureInfo, "快速编队");
this.limitedFullyString = stringLocalizer.WithCultureGet(cultureInfo, "限时全开");
this.limitedFullyAllString = stringLocalizer.WithCultureGet(cultureInfo, "限时开放");
}
private static RecognitionObject GetConfirmRa(params string[] targetText)
@@ -381,7 +383,7 @@ public class AutoDomainTask : ISoloTask
limitedFullyStringRa.FindMulti(RecognitionObject.Ocr(0, 0, limitedFullyStringRa.Width * 0.5,
limitedFullyStringRa.Height));
var limitedFullyStringRaocrListdone = limitedFullyStringRaocrList.LastOrDefault(t =>
Regex.IsMatch(t.Text, this.limitedFullyString));
Regex.IsMatch(t.Text, this.limitedFullyString) || Regex.IsMatch(t.Text, this.limitedFullyAllString));
// 检测是否为限时全开秘境
if (limitedFullyStringRaocrListdone != null)
{
@@ -1102,7 +1104,7 @@ public class AutoDomainTask : ISoloTask
{
// 自动刷干树脂
// 识别树脂状况
var resinStatus = ResinStatus.RecogniseFromRegion(ra3);
var resinStatus = ResinStatus.RecogniseFromRegion(ra3, TaskContext.Instance().SystemInfo, OcrFactory.Paddle);
resinStatus.Print(Logger);
if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 })

View File

@@ -1,10 +1,11 @@
using System;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.GameTask.Model;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.Helpers;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
namespace BetterGenshinImpact.GameTask.AutoDomain.Model;
@@ -18,35 +19,43 @@ public class ResinStatus
/// <summary>
/// 脆弱树脂60
/// </summary>
public int FragileResinCount { get; set; } = 0;
public int FragileResinCount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
/// <summary>
/// 浓缩树脂(40
/// 浓缩树脂(60
/// </summary>
public int CondensedResinCount { get; set; } = 0;
/// <summary>
/// 须臾树脂60壶内购买
/// </summary>
public int TransientResinCount { get; set; } = 0;
public int TransientResinCount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public static ResinStatus RecogniseFromRegion(ImageRegion region)
public static ResinStatus RecogniseFromRegion(ImageRegion region, ISystemInfo systemInfo, IOcrService ocrService)
{
var status = new ResinStatus();
// 1. 原粹树脂 起点 w-(256+100) ~ w-256
var captureArea = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect;
var assetScale = TaskContext.Instance().SystemInfo.AssetScale;
var originalResinTopIconRa = AutoFightAssets.Instance.OriginalResinTopIconRa;
var originalResinRes = region.Find(originalResinTopIconRa);
// 1. 原粹树脂
var assetScale = systemInfo.AssetScale;
var originalResinTopIconRa = new RecognitionObject
{
Name = "OriginalResinTopIcon",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "original_resin_top_icon.png", systemInfo),
DrawOnWindow = false
}.InitTemplate();
using ImageRegion crop1 = region.DeriveCrop(new Rect((int)(1300 * assetScale), (int)(25 * assetScale), (int)(160 * assetScale), (int)(50 * assetScale))); // 数字位数的不同导致了水平方向上宽泛的区域
//Cv2.ImShow("test", crop1.SrcMat);
//Cv2.WaitKey();
var originalResinRes = crop1.Find(originalResinTopIconRa);
if (originalResinRes.IsEmpty())
{
throw new Exception("未找到原粹树脂图标");
}
var originalResinCountRect = new Rect(originalResinRes.Right + 30, (int)(37 * assetScale),
captureArea.Width - (originalResinRes.Right + 30) - (int)(190 * assetScale), (int)(21 * assetScale));
string cnt1 = OcrFactory.Paddle.OcrWithoutDetector(region.DeriveCrop(originalResinCountRect).SrcMat);
var originalResinCountRect = new Rect(crop1.X + originalResinRes.Right + (int)(25 * assetScale), (int)(37 * assetScale), (int)(110 * assetScale)/* 考虑最长的“200/200” */, (int)(24 * assetScale));
using ImageRegion originalResinCountRegion = region.DeriveCrop(originalResinCountRect);
string cnt1 = ocrService.OcrWithoutDetector(originalResinCountRegion.SrcMat);
var match = System.Text.RegularExpressions.Regex.Match(cnt1, @"(\d+)\s*[/17]\s*(2|20|200)");
if (match.Success)
{
@@ -55,12 +64,27 @@ public class ResinStatus
}
// 2. 浓缩树脂
var condensedResinRes = region.Find(AutoFightAssets.Instance.CondensedResinTopIconRa);
int startX = crop1.X + originalResinRes.Left - (int)(180 * assetScale); // 从原粹树脂图标位置起算
var condensedResinTopIconRa = new RecognitionObject
{
Name = "CondensedResinTopIcon",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_top_icon.png", systemInfo),
DrawOnWindow = false
}.InitTemplate();
using ImageRegion crop2 = region.DeriveCrop(new Rect(startX, (int)(25 * assetScale), (int)(90 * assetScale), (int)(50 * assetScale)));
var condensedResinRes = crop2.Find(condensedResinTopIconRa);
if (condensedResinRes.IsExist())
{
// 找出 icon 的位置 + 25 ~ icon 的位置+45 就是浓缩树脂的数字数字宽20
var condensedResinCountRect = new Rect(condensedResinRes.Right + (int)(25 * assetScale), condensedResinRes.Y, (int)(20 * assetScale), condensedResinRes.Height);
string cnt40 = OcrFactory.Paddle.OcrWithoutDetector(region.DeriveCrop(condensedResinCountRect).SrcMat);
var condensedResinCountRect = new Rect(crop2.X + condensedResinRes.Right + (int)(20 * assetScale), (int)(37 * assetScale), (int)(70 * assetScale), (int)(24 * assetScale));
using ImageRegion countRegion = region.DeriveCrop(condensedResinCountRect);
using Mat threshold = countRegion.CacheGreyMat.Threshold(180, 255, ThresholdTypes.Binary);
using Mat bitwiseNot = new Mat();
Cv2.BitwiseNot(threshold, bitwiseNot);
//Cv2.ImShow("bitwise", bitwise);
//Cv2.WaitKey();
string cnt40 = ocrService.OcrWithoutDetector(bitwiseNot);
status.CondensedResinCount = StringUtils.TryExtractPositiveInt(cnt40, 0);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -1,4 +1,4 @@
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.GameTask.Model;
using OpenCvSharp;
using System.Collections.Generic;
@@ -23,15 +23,9 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
public RecognitionObject ArtifactAreaRa;
public RecognitionObject ExitRa;
public RecognitionObject ClickAnyCloseTipRa;
public RecognitionObject UseCondensedResinRa;
// 树脂状态
public RecognitionObject CondensedResinCountRa;
public RecognitionObject FragileResinCountRa;
// 自动秘境
// public RecognitionObject LockIconRa; // 锁定辅助图标
public RecognitionObject CondensedResinTopIconRa;
public RecognitionObject OriginalResinTopIconRa;
public Dictionary<string, string> AvatarCostumeMap;
@@ -44,7 +38,7 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
// 小道具位置
public Rect GadgetRect;
public RecognitionObject AbnormalIconRa;
private AutoFightAssets()
@@ -59,7 +53,7 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
(int)(41 * AssetScale), (int)(18 * AssetScale));
QRect = new Rect(CaptureRect.Width - (int)(157 * AssetScale), CaptureRect.Height - (int)(165 * AssetScale),
(int)(110 * AssetScale), (int)(110 * AssetScale));
ZCooldownRect = new Rect(CaptureRect.Width - (int)(130 * AssetScale), (int)(814 * AssetScale),
ZCooldownRect = new Rect(CaptureRect.Width - (int)(130 * AssetScale), (int)(814 * AssetScale),
(int)(60 * AssetScale), (int)(24 * AssetScale));
// 小道具位置 1920-133,800,60,50
GadgetRect = new Rect(CaptureRect.Width - (int)(133 * AssetScale), (int)(800 * AssetScale),
@@ -205,7 +199,7 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
Name = "ArtifactArea",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "artifact_flower_logo.png"),
RegionOfInterest = new Rect(CaptureRect.Width / 2,0,CaptureRect.Width / 2, CaptureRect.Height),
RegionOfInterest = new Rect(CaptureRect.Width / 2, 0, CaptureRect.Width / 2, CaptureRect.Height),
DrawOnWindow = false
}.InitTemplate();
@@ -219,15 +213,6 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
DrawOnWindow = false
}.InitTemplate();
UseCondensedResinRa = new RecognitionObject
{
Name = "UseCondensedResin",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "use_condensed_resin.png"),
RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2),
DrawOnWindow = false
}.InitTemplate();
ExitRa = new RecognitionObject
{
Name = "Exit",
@@ -237,23 +222,6 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
DrawOnWindow = false
}.InitTemplate();
CondensedResinCountRa = new RecognitionObject
{
Name = "CondensedResinCount",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_count.png"),
RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 3 * 2, CaptureRect.Width / 2, CaptureRect.Height / 3),
DrawOnWindow = false
}.InitTemplate();
FragileResinCountRa = new RecognitionObject
{
Name = "FragileResinCount",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "fragile_resin_count.png"),
RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 3 * 2, CaptureRect.Width / 2, CaptureRect.Height / 3),
DrawOnWindow = false
}.InitTemplate();
// 自动秘境
// LockIconRa = new RecognitionObject
// {
@@ -263,30 +231,14 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
// RegionOfInterest = new Rect(CaptureRect.Width - (int)(215 * AssetScale), 0, (int)(215 * AssetScale), (int)(80 * AssetScale)),
// DrawOnWindow = false
// }.InitTemplate();
CondensedResinTopIconRa = new RecognitionObject
{
Name = "CondensedResinTopIcon",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_top_icon.png"),
RegionOfInterest = new Rect((int)(1270 * AssetScale), (int)(25 * AssetScale), (int)(520 * AssetScale), (int)(45 * AssetScale)),
DrawOnWindow = false
}.InitTemplate();
OriginalResinTopIconRa = new RecognitionObject
{
Name = "OriginalResinTopIcon",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "original_resin_top_icon.png"),
RegionOfInterest = new Rect(CaptureRect.Width - (int)(450 * AssetScale), (int)(25 * AssetScale), (int)(265 * AssetScale), (int)(45 * AssetScale)),
DrawOnWindow = false
}.InitTemplate();
AbnormalIconRa = new RecognitionObject
{
Name = "AbnormalIcon",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "abnormal_icon.png"),
RegionOfInterest = new Rect(0,(int)(CaptureRect.Height*0.08), (int)(CaptureRect.Width*0.04), (int)(CaptureRect.Height*0.07)),
RegionOfInterest = new Rect(0, (int)(CaptureRect.Height * 0.08), (int)(CaptureRect.Width * 0.04), (int)(CaptureRect.Height * 0.07)),
DrawOnWindow = false
}.InitTemplate();
}
}

View File

@@ -1902,7 +1902,9 @@
{
"alias": [
"伊涅芙",
"Ineffa"
"Ineffa",
"机器人",
"机娘"
],
"burstCD": 15,
"id": "10000116",
@@ -1910,5 +1912,42 @@
"nameEn": "Ineffa",
"skillCD": 16,
"weapon": "13"
},
{
"alias": [
"菈乌玛",
"Lauma"
],
"burstCD": 15,
"id": "10000119",
"name": "菈乌玛",
"nameEn": "Lauma",
"skillCD": 12,
"weapon": "10"
},
{
"alias": [
"菲林斯",
"Flins"
],
"burstCD": 20,
"id": "10000120",
"name": "菲林斯",
"nameEn": "Flins",
"skillCD": 6,
"skillHoldCD": 16,
"weapon": "13"
},
{
"alias": [
"爱诺",
"Aino"
],
"burstCD": 13.5,
"id": "10000121",
"name": "爱诺",
"nameEn": "Aino",
"skillCD": 10,
"weapon": "11"
}
]

View File

@@ -175,6 +175,8 @@ public class Avatar
break;
}
Offset60Fix(i);
// Debug.WriteLine($"切换到{Index}号位");
// Cv2.ImWrite($"log/切换.png", region.SrcMat);
Sleep(250, Ct);
@@ -232,6 +234,8 @@ public class Avatar
default:
break;
}
Offset60Fix(i);
Sleep(250, Ct);
}
@@ -277,11 +281,31 @@ public class Avatar
default:
break;
}
Offset60Fix(i);
Sleep(250);
}
}
private void Offset60Fix(int i)
{
// 6.0 特殊逻辑
if (i > 3 && CombatScenes.IndexRectOffset60Fix)
{
// 3次失败考虑是否偏移出现问题修改偏移位置
// 只有 草露 角色离队,然后跨地图传送后,会出现这个场景。也就是只有 偏移 -> 原始 的场景
foreach (var avatar in CombatScenes.GetAvatars())
{
var rect1 = avatar.IndexRect;
rect1.Y += 14;
avatar.IndexRect = rect1;
}
CombatScenes.IndexRectOffset60Fix = false;
}
}
/// <summary>
/// 是否出战状态
/// </summary>
@@ -294,20 +318,24 @@ public class Avatar
}
else
{
// 剪裁出IndexRect区域
var indexRa = region.DeriveCrop(IndexRect);
// Cv2.ImWrite($"log/indexRa_{Name}.png", indexRa.SrcMat);
var count = OpenCvCommonHelper.CountGrayMatColor(indexRa.CacheGreyMat, 251, 255);
if (count * 1.0 / (IndexRect.Width * IndexRect.Height) > 0.5)
{
return false;
}
else
{
return true;
}
var white = IsIndexRectWhite(region, IndexRect);
return !white;
}
}
private bool IsIndexRectWhite(ImageRegion region, Rect rect)
{
// 剪裁出IndexRect区域
var indexRa = region.DeriveCrop(rect);
var mat = indexRa.CacheGreyMat;
var count = OpenCvCommonHelper.CountGrayMatColor(mat, 251, 255);
if (count * 1.0 / (mat.Width * mat.Height) > 0.5)
{
return true;
}
return false;
}
/// <summary>
/// 是否出战状态

View File

@@ -40,6 +40,12 @@ public class CombatScenes : IDisposable
App.ServiceProvider.GetRequiredService<BgiOnnxFactory>().CreateYoloPredictor(BgiOnnxModel.BgiAvatarSide);
public int ExpectedTeamAvatarNum { get; private set; } = 4;
/// <summary>
/// 6.0 UI偏移标识
/// </summary>
public bool IndexRectOffset60Fix { get; set; }
/// <summary>
/// 获取一个只读的Avatars
@@ -100,6 +106,9 @@ public class CombatScenes : IDisposable
avatarSideIconRectList = AutoFightAssets.Instance.AvatarSideIconRectList;
avatarIndexRectList = AutoFightAssets.Instance.AvatarIndexRectList;
}
// 6.0 版本 队伍下的 草露 进度条 导致位置偏移
IndexRectOffset60Fix = AvatarSideFixOffset(imageRegion, avatarSideIconRectList, avatarIndexRectList);
// 识别队伍
var names = new string[avatarSideIconRectList.Count];
@@ -137,6 +146,53 @@ public class CombatScenes : IDisposable
return this;
}
/// <summary>
/// 6.0 版本 队伍下的 草露 进度条 导致位置偏移
///
/// </summary>
/// <param name="imageRegion"></param>
/// <param name="avatarSideIconRectList"></param>
/// <param name="avatarIndexRectList"></param>
public bool AvatarSideFixOffset(ImageRegion imageRegion, List<Rect> avatarSideIconRectList, List<Rect> avatarIndexRectList)
{
// 角色序号 左上角 坐标偏移(+2, -5后存在3个白色点则认为存在 草露 进度条
// 存在 草露 进度条时候整体上移 14 个像素
int whitePointCount = 0;
foreach (var rectIndex in avatarIndexRectList)
{
int x = rectIndex.X + 2;
int y = rectIndex.Y - 5;
var color = imageRegion.SrcMat.At<Vec3b>(y, x);
if (color is { Item0: 255, Item1: 255, Item2: 255 })
{
whitePointCount++;
}
}
if (whitePointCount >= 3)
{
Logger.LogInformation("检测到右侧队伍偏移,进行位置修正");
for (int i = 0; i < avatarSideIconRectList.Count; i++)
{
var rect = avatarSideIconRectList[i];
rect.Y -= 14;
avatarSideIconRectList[i] = rect;
}
for (int i = 0; i < avatarIndexRectList.Count; i++)
{
var rect = avatarIndexRectList[i];
rect.Y -= 14;
avatarIndexRectList[i] = rect;
}
return true;
}
return false;
}
public (string, string) ClassifyAvatarCnName(Image<Rgb24> img, int index)
{

View File

@@ -150,34 +150,13 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
logger.LogInformation("选择鱼饵 {Text}", blackboard.selectedBait.GetDescription());
// 寻找鱼饵
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);
Cv2.FindContours(canny, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null);
contours = contours
.Where(c =>
{
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)
var boxAndBaits = FindBait(imageRegion);
;
foreach ((Rect box, string? predName) in boxAndBaits)
{
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())
{
using ImageRegion resRa = imageRegion.DeriveCrop(box);
resRa.Click();
blackboard.Sleep(700);
// 可能重复点击,所以固定界面点击下
@@ -225,6 +204,37 @@ namespace BetterGenshinImpact.GameTask.AutoFishing
return BehaviourStatus.Running;
}
}
public IEnumerable<(Rect, string?)> FindBait(ImageRegion imageRegion1080p)
{
using ImageRegion singleRowGrid = imageRegion1080p.DeriveCrop(0.28 * imageRegion1080p.Width, 0.37 * imageRegion1080p.Height, 0.45 * imageRegion1080p.Width, 0.22 * imageRegion1080p.Height);
using Mat grey = singleRowGrid.SrcMat.CvtColor(ColorConversionCodes.BGR2GRAY);
using Mat canny = grey.Canny(20, 40);
Cv2.FindContours(canny, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null);
contours = contours
.Where(c =>
{
Rect r = Cv2.BoundingRect(c);
if (r.Width < 0.065 * imageRegion1080p.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);
yield return (new Rect(singleRowGrid.X + box.X, singleRowGrid.Y + box.Y, box.Width, box.Height), predName);
}
}
}
[Obsolete]

View File

@@ -21,5 +21,9 @@ public enum BaitType
[Description("澄晶果粒饵")]
SpinelgrainBait,
[Description("温火饵")]
EmberglowBait
EmberglowBait,
[Description("槲梭饵")]
BerryBait,
[Description("清白饵")]
RefreshingLakkaBait
}

View File

@@ -30,7 +30,11 @@ public class BigFishType
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 readonly BigFishType SecretSourceScoutSweeper = new ("secret source scout sweeper", BaitType.EmberglowBait, "秘源机关・巡戒使", 9);
public static readonly BigFishType MaulerShark = new ("mauler shark", BaitType.RefreshingLakkaBait, "凶凶鲨", 9);
public static readonly BigFishType CrystalEye = new("crystal eye", BaitType.RefreshingLakkaBait, "明眼鱼", 9);
public static readonly BigFishType AxeheadFish = new ("axehead fish", BaitType.BerryBait, "巨斧鱼", 9);
public static IEnumerable<BigFishType> Values
{
@@ -55,6 +59,9 @@ public class BigFishType
yield return Rapidfish;
yield return PhonyUnihornfish;
yield return MagmaRapidfish;
yield return MaulerShark;
yield return CrystalEye;
yield return AxeheadFish;
}
}

View File

@@ -88,7 +88,7 @@
"nameEn": "diona",
"type": "character",
"name": "迪奥娜",
"hp": 10,
"hp": 12,
"energy": 3,
"element": "冰元素",
"weapon": "弓",
@@ -2145,7 +2145,7 @@
"nameEn": "amber",
"type": "character",
"name": "安柏",
"hp": 10,
"hp": 12,
"energy": 2,
"element": "火元素",
"weapon": "弓",
@@ -3922,6 +3922,83 @@
}
]
},
{
"id": 1414,
"nameEn": "iansan",
"type": "character",
"name": "伊安珊",
"hp": 12,
"energy": 2,
"element": "雷元素",
"weapon": "长柄武器",
"skills": [
{
"nameEn": "weighted_spike",
"name": "负重锥击",
"skillTag": [
"普通攻击"
],
"cost": [
{
"id": 1104,
"nameEn": "electro",
"type": "雷元素",
"count": 1
},
{
"id": 1109,
"nameEn": "unaligned_element",
"type": "无色元素",
"count": 2
}
]
},
{
"nameEn": "thunderbolt_rush",
"name": "电掣雷驰",
"skillTag": [
"元素战技"
],
"cost": [
{
"id": 1104,
"nameEn": "electro",
"type": "雷元素",
"count": 3
}
]
},
{
"nameEn": "the_three_principles_of_power",
"name": "力的三原理",
"skillTag": [
"元素爆发"
],
"cost": [
{
"id": 1104,
"nameEn": "electro",
"type": "雷元素",
"count": 3
},
{
"id": 1110,
"nameEn": "energy",
"type": "充能",
"count": 2
}
]
},
{
"nameEn": "caloric_balancing_plan",
"name": "热量均衡计划",
"skillTag": [
"被动技能"
],
"cost": []
}
]
},
{
"id": 1501,
"nameEn": "sucrose",
@@ -4835,6 +4912,75 @@
}
]
},
{
"id": 1514,
"nameEn": "yumemizuki_mizuki",
"type": "character",
"name": "梦见月瑞希",
"hp": 10,
"energy": 2,
"element": "风元素",
"weapon": "法器",
"skills": [
{
"nameEn": "pure_heart_pure_dreams",
"name": "梦我梦心",
"skillTag": [
"普通攻击"
],
"cost": [
{
"id": 1105,
"nameEn": "anemo",
"type": "风元素",
"count": 1
},
{
"id": 1109,
"nameEn": "unaligned_element",
"type": "无色元素",
"count": 2
}
]
},
{
"nameEn": "aisa_utamakura_pilgrimage",
"name": "秋沙歌枕巡礼",
"skillTag": [
"元素战技"
],
"cost": [
{
"id": 1105,
"nameEn": "anemo",
"type": "风元素",
"count": 3
}
]
},
{
"nameEn": "anraku_secret_spring_therapy",
"name": "安乐秘汤疗法",
"skillTag": [
"元素爆发"
],
"cost": [
{
"id": 1105,
"nameEn": "anemo",
"type": "风元素",
"count": 3
},
{
"id": 1110,
"nameEn": "energy",
"type": "充能",
"count": 2
}
]
}
]
},
{
"id": 1601,
"nameEn": "ningguang",
@@ -5553,7 +5699,7 @@
"nameEn": "xilonen",
"type": "character",
"name": "希诺宁",
"hp": 10,
"hp": 12,
"energy": 2,
"element": "岩元素",
"weapon": "单手剑",
@@ -6128,7 +6274,7 @@
"nameEn": "kaveh",
"type": "character",
"name": "卡维",
"hp": 10,
"hp": 12,
"energy": 2,
"element": "草元素",
"weapon": "双手剑",
@@ -6643,7 +6789,7 @@
"nameEn": "rhodeia_of_loch",
"type": "character",
"name": "纯水精灵·洛蒂娅",
"hp": 10,
"hp": 11,
"energy": 3,
"element": "水元素",
"weapon": "其他武器",
@@ -7322,6 +7468,83 @@
}
]
},
{
"id": 2305,
"nameEn": "lord_of_eroded_primal_fire",
"type": "character",
"name": "蚀灭的源焰之主",
"hp": 12,
"energy": 2,
"element": "火元素",
"weapon": "其他武器",
"skills": [
{
"nameEn": "void_claw_strike",
"name": "虚界玄爪",
"skillTag": [
"普通攻击"
],
"cost": [
{
"id": 1103,
"nameEn": "pyro",
"type": "火元素",
"count": 1
},
{
"id": 1109,
"nameEn": "unaligned_element",
"type": "无色元素",
"count": 2
}
]
},
{
"nameEn": "eroded_flaming_feathers",
"name": "蚀灭火羽",
"skillTag": [
"元素战技"
],
"cost": [
{
"id": 1103,
"nameEn": "pyro",
"type": "火元素",
"count": 3
}
]
},
{
"nameEn": "severing_primal_fire",
"name": "斫劫源焰",
"skillTag": [
"元素爆发"
],
"cost": [
{
"id": 1103,
"nameEn": "pyro",
"type": "火元素",
"count": 3
},
{
"id": 1110,
"nameEn": "energy",
"type": "充能",
"count": 2
}
]
},
{
"nameEn": "resentment",
"name": "忿恨",
"skillTag": [
"被动技能"
],
"cost": []
}
]
},
{
"id": 2401,
"nameEn": "electro_hypostasis",
@@ -8480,7 +8703,7 @@
"nameEn": "gluttonous_yumkasaur_mountain_king",
"type": "character",
"name": "贪食匿叶龙山王",
"hp": 7,
"hp": 8,
"energy": 2,
"element": "草元素",
"weapon": "其他武器",

View File

@@ -41,6 +41,7 @@ public class AutoAlbumTask(AutoMusicGameParam taskParam) : ISoloTask
catch (Exception e)
{
Logger.LogError("自动音乐专辑任务异常:{Msg}", e.Message);
Logger.LogDebug(e, "自动音乐专辑任务异常详情");
Notify.Event(NotificationEvent.AlbumError).Error("自动音游专辑异常", e);
}
}

View File

@@ -1,8 +1,8 @@
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.GameTask.Model;
using OpenCvSharp;
namespace BetterGenshinImpact.GameTask.AutoFishing.Assets;
namespace BetterGenshinImpact.GameTask.AutoOpenChest.Assets;
public class AutoOpenChestAssets : BaseAssets<AutoOpenChestAssets>
{
@@ -14,7 +14,7 @@ public class AutoOpenChestAssets : BaseAssets<AutoOpenChestAssets>
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。
private AutoOpenChestAssets() : base()
{
Initialization(this.systemInfo);
Initialization(systemInfo);
}
protected AutoOpenChestAssets(ISystemInfo systemInfo) : base(systemInfo)

View File

@@ -1,8 +1,6 @@
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.AutoFishing.Assets;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.AutoOpenChest.Assets;
using BetterGenshinImpact.GameTask.Model.Area;
using Microsoft.Extensions.Logging;
using System;
@@ -11,8 +9,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using static TorchSharp.torch.distributions.constraints;
namespace GameTask.AutoOpenChest;
namespace BetterGenshinImpact.GameTask.AutoOpenChest;
/// <summary>
/// 识别宝箱图标,走向宝箱并开启。
@@ -72,7 +69,7 @@ public class AutoOpenChestTask : ISoloTask
}
else
{
var gap = (ra.Width / 2) - chestIcon.X;
var gap = ra.Width / 2 - chestIcon.X;
int rate = 2;
Simulation.SendInput.Mouse.MoveMouseBy(gap / rate, 0);
}

View File

@@ -12,6 +12,7 @@ using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
@@ -245,17 +246,34 @@ public partial class AutoPickTrigger : ITaskTrigger
else
{
var textMat = new Mat(content.CaptureRectArea.SrcMat, textRect);
var boundingRect = GetWhiteTextBoundingRect(textMat);
var boundingRect = TextRectExtractor.GetTextBoundingRect(textMat, out var bin);
// 如果找到有效区域
if (boundingRect.Width > 5 && boundingRect.Height > 5)
if (boundingRect.X <20 && boundingRect.Width > 5 && boundingRect.Height > 5)
{
// 截取只包含文字的区域
var textOnlyMat = new Mat(textMat, new Rect(0, 0,
boundingRect.Right + 3 < textMat.Width ? boundingRect.Right + 3 : textMat.Width, textMat.Height));
boundingRect.Right + 5 < textMat.Width ? boundingRect.Right + 5 : textMat.Width, textMat.Height));
text = OcrFactory.Paddle.OcrWithoutDetector(textOnlyMat);
// if (RuntimeHelper.IsDebug)
// {
// // 如果不等于正确文字,则保存图片
// if (text != "烹饪")
// {
// var path = Global.Absolute("log/pick");
// Directory.CreateDirectory(path);
// var str = $"{DateTime.Now:yyyyMMddHHmmssfff}";
// // textMat.SaveImage(Path.Combine(path, $"pick_ocr_ori_{str}.png"));
// // 画上 boundingRect
// Cv2.Rectangle(textMat, boundingRect, new Scalar(0, 0, 255), 1);
// textMat.SaveImage(Path.Combine(path, $"pick_ocr_rect_{str}.png"));
// bin.SaveImage(Path.Combine(path, $"bin_{str}.png"));
// }
// }
}
else
{
Debug.WriteLine("-- 无法识别到有效文字区域尝试直接OCR DET");
text = OcrFactory.Paddle.Ocr(textMat);
}
}
@@ -263,6 +281,9 @@ public partial class AutoPickTrigger : ITaskTrigger
speedTimer.Record("文字识别");
if (!string.IsNullOrEmpty(text))
{
// 处理OCR识别结果清理无效字符并确保引号配对
text = ProcessOcrText(text);
// 唯一一个动态拾取项,特殊处理,不拾取
if (text.Contains("长时间"))
{
@@ -275,21 +296,20 @@ public partial class AutoPickTrigger : ITaskTrigger
{
return;
}
// 挪德卡莱聚所中文名特殊处理,不拾取
if (text.Contains("聚所") && (text.Contains("霜月") || text.Contains("叮铃") ||
text.Contains("眶螂") || text.Contains("蛋卷") || text.Contains("坊")))
{
return;
}
// 单个字符不拾取
var simpleText = PunctuationAndSpacesRegex().Replace(text, "");
if (simpleText.Length <= 1)
{
return;
}
// 纯英文不拾取
if (StringUtils.IsPureEnglish(text))
if (text.Length <= 1)
{
return;
}
if (config.WhiteListEnabled && (_whiteList.Contains(text) || _whiteList.Contains(simpleText)))
if (config.WhiteListEnabled && _whiteList.Contains(text))
{
LogPick(content, text);
Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk);
@@ -304,7 +324,7 @@ public partial class AutoPickTrigger : ITaskTrigger
return;
}
if (config.BlackListEnabled && (_blackList.Contains(text) || _blackList.Contains(simpleText)))
if (config.BlackListEnabled && _blackList.Contains(text))
{
return;
}
@@ -372,8 +392,114 @@ public partial class AutoPickTrigger : ITaskTrigger
_prevClickFrameIndex = content.FrameIndex;
}
[GeneratedRegex(@"^[\p{P} ]+|[\p{P} ]+$")]
private static partial Regex PunctuationAndSpacesRegex();
/// <summary>
/// 高性能处理OCR识别的文字结果
/// 1. 替换【、[ 为「,替换】、] 为」
/// 2. 清理左边非「字符和中文的字符
/// 3. 清理右边非」字符和中文的字符
/// 4. 确保引号配对:有「必有」,有」必有「
/// </summary>
/// <param name="text">OCR识别的原始文字</param>
/// <returns>处理后的文字</returns>
private static string ProcessOcrText(string text)
{
if (string.IsNullOrEmpty(text))
return text;
// 0. 首先替换相似的括号字符并删除换行符、空格使用Span<char>进行原地替换以获得最佳性能
Span<char> chars = stackalloc char[text.Length];
text.AsSpan().CopyTo(chars);
int writeIndex = 0;
bool hasChanges = false;
for (int i = 0; i < chars.Length; i++)
{
char c = chars[i];
// 跳过换行符、回车符、空格、制表符等空白字符
if (char.IsWhiteSpace(c))
{
hasChanges = true;
continue;
}
// 替换括号字符
if (c == '【' || c == '[')
{
chars[writeIndex++] = '「';
hasChanges = true;
}
else if (c == '】' || c == ']')
{
chars[writeIndex++] = '」';
hasChanges = true;
}
else
{
chars[writeIndex++] = c;
}
}
// 如果有变化使用处理后的字符否则使用原字符串的Span
ReadOnlySpan<char> span = hasChanges ? chars.Slice(0, writeIndex) : text.AsSpan();
int start = 0;
int end = span.Length - 1;
// 1. 从左边开始,删除非「字符和中文的字符
while (start <= end)
{
char c = span[start];
if (c == '「' || (c >= 0x4E00 && c <= 0x9FFF)) // 「字符或中文字符
break;
start++;
}
// 2. 从右边开始,删除非」字符和中文的字符
while (end >= start)
{
char c = span[end];
if (c == '」' || c == '' || (c >= 0x4E00 && c <= 0x9FFF)) // 」字符或中文字符
break;
end--;
}
// 如果所有字符都被删除了
if (start > end)
return string.Empty;
// 获取清理后的文字
var cleanedSpan = span.Slice(start, end - start + 1);
// 3. 检查并补充引号配对
bool hasLeftQuote = false;
bool hasRightQuote = false;
// 快速扫描是否存在引号
for (int i = 0; i < cleanedSpan.Length; i++)
{
if (cleanedSpan[i] == '「')
hasLeftQuote = true;
else if (cleanedSpan[i] == '」')
hasRightQuote = true;
}
// 根据引号配对规则补充
if (hasLeftQuote && !hasRightQuote)
{
// 有「但没有」,在末尾补充」
Debug.WriteLine("补充缺失的右引号");
return string.Concat(cleanedSpan, "」");
}
else if (hasRightQuote && !hasLeftQuote)
{
// 有」但没有「,在开头补充「
Debug.WriteLine("补充缺失的左引号");
return string.Concat("「", cleanedSpan);
}
return cleanedSpan.ToString();
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Linq;
namespace BetterGenshinImpact.GameTask.AutoPick;
using OpenCvSharp;
public static class TextRectExtractor
{
/// <summary>
/// 从图片中提取文字范围(假定文字从最左边贴边开始,向右连续)
/// 结果矩形固定 x=0,y=0,h=原图高度,只计算连续文字宽度。
/// </summary>
public static Rect GetTextBoundingRect(Mat textMat, out Mat bin)
{
// 转换为灰度图
Mat gray = new Mat();
if (textMat.Channels() == 3)
{
Cv2.CvtColor(textMat, gray, ColorConversionCodes.BGR2GRAY);
}
else
{
gray = textMat.Clone();
}
// 使用阈值160进行二值化处理
bin = new Mat();
Cv2.Threshold(gray, bin, 160, 255, ThresholdTypes.Binary);
// 形态学操作:先腐蚀后膨胀,去除噪点并保持文字完整
Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
Cv2.Erode(bin, bin, kernel, iterations: 1);
Cv2.Dilate(bin, bin, kernel, iterations: 2);
kernel.Dispose();
gray.Dispose();
return ProjectionRect(textMat, bin);
}
private static Rect ProjectionRect(Mat textMat, Mat bin)
{
// 投影:对行做 ReduceSum得到 1 x width 的列和
using var projection = new Mat();
Cv2.Reduce(bin, projection, 0, ReduceTypes.Sum, MatType.CV_32S);
int width = projection.Cols;
projection.GetArray(out int[] colSums);
int maxGap = 30; // 允许的最大连续空列数
int gapCount = 0;
int lastNonEmpty = -1;
for (int x = 0; x < width; x++)
{
bool hasInk = colSums[x] > 0;
if (hasInk)
{
lastNonEmpty = x;
gapCount = 0;
}
else
{
gapCount++;
if (gapCount > maxGap)
{
break;
}
}
}
if (lastNonEmpty == -1)
{
// 没有检测到文字
return new Rect();
}
Rect boundingRect = new Rect(0, 0, lastNonEmpty, textMat.Height);
return boundingRect;
}
}

View File

@@ -1,5 +1,6 @@
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;
@@ -254,7 +255,7 @@ public class AutoStygianOnslaughtTask : ISoloTask
var ra = CaptureToRectArea();
var ocrList = ra.FindMulti(RecognitionObject.OcrThis);
if (ocrList.Any(o => o.Text.Contains("好友挑战")) && ocrList.Any(o => o.Text.Contains("开始挑战")))
if (ocrList.Any(o => o.Text.Contains("角色预览")) && ocrList.Any(o => o.Text.Contains("开始挑战")))
{
// 选择boss
_logger.LogInformation($"{Name}选择BOSS编号{{Text}}", _taskParam.BossNum);
@@ -398,7 +399,7 @@ public class AutoStygianOnslaughtTask : ISoloTask
{
// 自动刷干树脂
// 识别树脂状况
var resinStatus = ResinStatus.RecogniseFromRegion(ra3);
var resinStatus = ResinStatus.RecogniseFromRegion(ra3, TaskContext.Instance().SystemInfo, OcrFactory.Paddle);
resinStatus.Print(_logger);
if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 })
@@ -627,4 +628,4 @@ public class AutoStygianOnslaughtTask : ISoloTask
await Delay(3000, _ct);
}
}
}

View File

@@ -12209,6 +12209,807 @@
"country": "纳塔",
"name": "传送锚点",
"area": "悠悠集市"
},
{
"id": "1700",
"gadgetId": "70600001",
"gadgetType": "TransPointFirst",
"type": "Goddess",
"position": [
1732.4111328125,
0,
9492.888671875
],
"tranPosition": [
1738.2041015625,
0,
9485.640625
],
"country": "挪德卡莱",
"name": "新月神像",
"area": "伦波岛"
},
{
"id": "1701",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Dungeon",
"position": [
1829.96875,
0,
9968.9326171875
],
"tranPosition": [
1832.07861328125,
0,
9966.6796875
],
"country": "挪德卡莱",
"name": "无光的深都",
"description": "材料本",
"area": "伦波岛"
},
{
"id": "1702",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1426.26953125,
0,
9364.361328125
],
"tranPosition": [
1435.646484375,
0,
9367.15625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "那夏镇"
},
{
"id": "1703",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1661.486328125,
0,
9452.7646484375
],
"tranPosition": [
1660.6416015625,
0,
9458.02734375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "那夏镇"
},
{
"id": "1704",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1628.921875,
0,
9529.51171875
],
"tranPosition": [
1637.3427734375,
0,
9537.419921875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "那夏镇"
},
{
"id": "1705",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1728.60400390625,
0,
9657.93359375
],
"tranPosition": [
1724.68017578125,
0,
9657.390625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "伦波岛"
},
{
"id": "1706",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1913.2666015625,
0,
9745.7763671875
],
"tranPosition": [
1916.314453125,
0,
9746.0458984375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "伦波岛"
},
{
"id": "1707",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2349.251953125,
0,
9250.142578125
],
"tranPosition": [
2349.97314453125,
0,
9247.923828125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "伦波岛"
},
{
"id": "1708",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2503.34375,
0,
8952.8544921875
],
"tranPosition": [
2498.614501953125,
0,
8945.59375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "伦波岛"
},
{
"id": "1709",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1808.001953125,
0,
9727.181640625
],
"tranPosition": [
1803.5458984375,
0,
9711.166015625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "北方训练场"
},
{
"id": "1710",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1717.00439453125,
0,
9874.6220703125
],
"tranPosition": [
1718.310546875,
0,
9885.201171875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "北方训练场"
},
{
"id": "1711",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1538.5693359375,
0,
10020.7421875
],
"tranPosition": [
1539.25537109375,
0,
10018.369140625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "叮铃哐啷蛋卷工坊"
},
{
"id": "1712",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2206.139892578125,
0,
8836.6396484375
],
"tranPosition": [
2203.386474609375,
0,
8833.7333984375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "刻拉蒂之眼"
},
{
"id": "1713",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2846.696044921875,
0,
9188.033203125
],
"tranPosition": [
2851.84765625,
0,
9191.2724609375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "空寂走廊"
},
{
"id": "1714",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2239.43603515625,
0,
9228.87109375
],
"tranPosition": [
2246.41064453125,
0,
9224.48046875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "蓝珀湖"
},
{
"id": "1715",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2297.338623046875,
0,
9096.4111328125
],
"tranPosition": [
2296.144287109375,
0,
9099.3046875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "蓝珀湖"
},
{
"id": "1716",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1856.17041015625,
0,
9358.6474609375
],
"tranPosition": [
1865.16357421875,
0,
9370.08984375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "苔骨荒原"
},
{
"id": "1717",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1901.662109375,
0,
9220.4033203125
],
"tranPosition": [
1898.93798828125,
0,
9222.14453125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "苔骨荒原"
},
{
"id": "1718",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2107.943603515625,
0,
9354.046875
],
"tranPosition": [
2103.78955078125,
0,
9351.6474609375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "苔骨荒原"
},
{
"id": "1719",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2522.204345703125,
0,
9211.5556640625
],
"tranPosition": [
2526.032470703125,
0,
9207.884765625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "苔骨荒原"
},
{
"id": "1720",
"gadgetId": "70600001",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2133.54931640625,
0,
9559.87890625
],
"tranPosition": [
2134.95654296875,
0,
9562.84375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "星砂滩"
},
{
"id": "1721",
"gadgetId": "70600002",
"gadgetType": "TransPointFirst",
"type": "Goddess",
"position": [
1652.9140625,
0,
10408.8828125
],
"tranPosition": [
1657.12158203125,
0,
10416.3681640625
],
"country": "挪德卡莱",
"name": "新月神像",
"area": "希汐岛"
},
{
"id": "1722",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Dungeon",
"position": [
1938.21826171875,
0,
10824.91015625
],
"tranPosition": [
1935.74609375,
0,
10828.0751953125
],
"country": "挪德卡莱",
"name": "失落的月庭",
"description": "材料本",
"area": "希汐岛"
},
{
"id": "1723",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1691.3837890625,
0,
10960.89453125
],
"tranPosition": [
1682.15673828125,
0,
10973.19921875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "雷图礁"
},
{
"id": "1724",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1589.37060546875,
0,
11061.8388671875
],
"tranPosition": [
1592.021484375,
0,
11060.626953125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "雷图礁"
},
{
"id": "1725",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1919.638671875,
0,
10525.666015625
],
"tranPosition": [
1931.4033203125,
0,
10524.564453125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "沐光之台"
},
{
"id": "1726",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2055.183349609375,
0,
10755.9384765625
],
"tranPosition": [
2060.8837890625,
0,
10754.037109375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "守誓者的圣所"
},
{
"id": "1727",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1854.90283203125,
0,
10386.330078125
],
"tranPosition": [
1854.55810546875,
0,
10391.689453125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "霜月之坊"
},
{
"id": "1728",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1783.83056640625,
0,
10552.1005859375
],
"tranPosition": [
1793.5361328125,
0,
10552.92578125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "希汐岛"
},
{
"id": "1729",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
1794.2666015625,
0,
10782.90234375
],
"tranPosition": [
1793.4140625,
0,
10781.017578125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "希汐岛"
},
{
"id": "1730",
"gadgetId": "70600002",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2277.250244140625,
0,
10790.84375
],
"tranPosition": [
2277.42919921875,
0,
10788.6953125
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "希汐岛"
},
{
"id": "1731",
"gadgetId": "70600003",
"gadgetType": "TransPointFirst",
"type": "Goddess",
"position": [
3045.697265625,
0,
9428.623046875
],
"tranPosition": [
3050.987548828125,
0,
9426.021484375
],
"country": "挪德卡莱",
"name": "新月神像",
"area": "帕哈岛"
},
{
"id": "1732",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Dungeon",
"position": [
3151.607666015625,
0,
9378.9453125
],
"tranPosition": [
3150.29931640625,
0,
9375.095703125
],
"country": "挪德卡莱",
"name": "霜凝的机枢",
"description": "圣遗物本",
"area": "帕哈岛"
},
{
"id": "1733",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3593.0986328125,
0,
9873.548828125
],
"tranPosition": [
3588.912109375,
0,
9874.380859375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "北港"
},
{
"id": "1734",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3303.773193359375,
0,
9203.404296875
],
"tranPosition": [
3301.760986328125,
0,
9204.46875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "绯沙盐沼"
},
{
"id": "1735",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3543.756103515625,
0,
9310.923828125
],
"tranPosition": [
3543.58056640625,
0,
9315.1259765625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "绯沙盐沼"
},
{
"id": "1736",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3025.621337890625,
0,
9822.30078125
],
"tranPosition": [
3031.96875,
0,
9825.13671875
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "南港"
},
{
"id": "1737",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
2829.17529296875,
0,
9899.525390625
],
"tranPosition": [
2832.10302734375,
0,
9899.994140625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "南港"
},
{
"id": "1738",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3090.505859375,
0,
9551.904296875
],
"tranPosition": [
3104.404541015625,
0,
9550.71484375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "月矩力试验设计局"
},
{
"id": "1739",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3130.807861328125,
0,
9703.982421875
],
"tranPosition": [
3144.232421875,
0,
9701.650390625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "月矩力试验设计局"
},
{
"id": "1740",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3397.948974609375,
0,
9594.3544921875
],
"tranPosition": [
3399.092041015625,
0,
9590.52734375
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "月矩力试验设计局"
},
{
"id": "1741",
"gadgetId": "70600003",
"gadgetType": "TransPointSecond",
"type": "Teleport",
"position": [
3139.771240234375,
0,
10106.5361328125
],
"tranPosition": [
3141.948486328125,
0,
10102.869140625
],
"country": "挪德卡莱",
"name": "传送锚点",
"area": "月矩力试验设计局"
}
]
},

View File

@@ -1,10 +1,3 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.Core.Script.Dependence;
@@ -29,6 +22,13 @@ using BetterGenshinImpact.Helpers.Extensions;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
@@ -37,12 +37,15 @@ namespace BetterGenshinImpact.GameTask.AutoTrackPath;
/// <summary>
/// 传送任务
/// </summary>
public class TpTask(CancellationToken ct)
public class TpTask
{
private readonly QuickTeleportAssets _assets = QuickTeleportAssets.Instance;
private readonly Rect _captureRect = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect;
private readonly double _zoomOutMax1080PRatio = TaskContext.Instance().SystemInfo.ZoomOutMax1080PRatio;
private readonly TpConfig _tpConfig = TaskContext.Instance().Config.TpConfig;
private readonly CancellationToken ct;
private readonly CultureInfo cultureInfo;
private readonly IStringLocalizer stringLocalizer;
/// <summary>
/// 直接通过缩放比例按钮计算放大按钮的Y坐标
@@ -56,6 +59,14 @@ public class TpTask(CancellationToken ct)
private const double DisplayTpPointZoomLevel = 4.4; // 传送点显示的时候的地图比例
public TpTask(CancellationToken ct)
{
this.ct = ct;
TpTaskParam param = new TpTaskParam();
this.cultureInfo = param.GameCultureInfo;
this.stringLocalizer = param.StringLocalizer;
}
/// <summary>
/// 传送到七天神像
/// </summary>
@@ -321,7 +332,7 @@ public class TpTask(CancellationToken ct)
return;
}
//增加容错,小概率情况下碰到,前面点击传送失败
capture.Find(_assets.TeleportButtonRo,rg=>rg.Click());
capture.Find(_assets.TeleportButtonRo, rg => rg.Click());
await Delay(delayMs, ct);
}
@@ -859,7 +870,7 @@ public class TpTask(CancellationToken ct)
return false;
}
public async Task SwitchArea(string areaName)
internal async Task SwitchArea(string areaName)
{
GameCaptureRegion.GameRegionClick((rect, scale) => (rect.Width - 160 * scale, rect.Height - 60 * scale));
await Delay(300, ct);
@@ -869,11 +880,8 @@ public class TpTask(CancellationToken ct)
RecognitionType = RecognitionTypes.Ocr,
RegionOfInterest = new Rect(ra.Width / 2, 0, ra.Width / 2, ra.Height)
});
IStringLocalizer<MapLazyAssets> stringLocalizer = App.GetService<IStringLocalizer<MapLazyAssets>>() ?? throw new NullReferenceException(nameof(stringLocalizer));
CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
string minCountryLocalized = stringLocalizer.WithCultureGet(cultureInfo, areaName);
string commissionLocalized = stringLocalizer.WithCultureGet(cultureInfo, "委托");
Region? matchRect = list.FirstOrDefault(r => !r.Text.Contains(commissionLocalized) && r.Text.Contains(minCountryLocalized));
string minCountryLocalized = this.stringLocalizer.WithCultureGet(this.cultureInfo, areaName);
Region? matchRect = list.OrderByDescending(r => r.Y).FirstOrDefault(r => r.Text.Contains(minCountryLocalized));
if (matchRect == null)
{
Logger.LogWarning("切换区域失败:{Country}", areaName);
@@ -891,7 +899,6 @@ public class TpTask(CancellationToken ct)
await Delay(500, ct);
}
public async Task Tp(string name)
{
// 通过大地图传送到指定传送点
@@ -907,7 +914,7 @@ public class TpTask(CancellationToken ct)
public async Task ClickTpPoint(ImageRegion imageRegion)
{
// 1.判断是否在地图界面
if(!Bv.IsInBigMapUi(imageRegion)) throw new RetryException("不在地图界面");
if (!Bv.IsInBigMapUi(imageRegion)) throw new RetryException("不在地图界面");
// 2. 判断是否已经点出传送按钮
var hasTeleportButton = CheckTeleportButton(imageRegion);
@@ -920,7 +927,7 @@ public class TpTask(CancellationToken ct)
// 4. 循环判断选项列表是否有传送点(未激活点位也在里面)
var hasMapChooseIcon = CheckMapChooseIcon(imageRegion);
// 没有传送点说明不是传送点
if(!hasMapChooseIcon) throw new TpPointNotActivate("选项列表不存在传送点");
if (!hasMapChooseIcon) throw new TpPointNotActivate("选项列表不存在传送点");
var teleportButtonFound = await NewRetry.WaitForElementAppear(
_assets.TeleportButtonRo,
() => { },
@@ -931,10 +938,12 @@ public class TpTask(CancellationToken ct)
if (!teleportButtonFound) throw new TpPointNotActivate("选项列表的传送点未激活");
await NewRetry.WaitForElementDisappear(
_assets.TeleportButtonRo,
screen => {
screen.Find(_assets.TeleportButtonRo, ra => {
ra.Click();
ra.Dispose();
screen =>
{
screen.Find(_assets.TeleportButtonRo, ra =>
{
ra.Click();
ra.Dispose();
});
},
ct,

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -117,8 +117,11 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="委托" xml:space="preserve">
<value>Commission</value>
<data name="尘歌壶" xml:space="preserve">
<value>Serenitea Pot</value>
</data>
<data name="挪德卡莱" xml:space="preserve">
<value>Nod-Krai</value>
</data>
<data name="枫丹" xml:space="preserve">
<value>Fontaine</value>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -117,8 +117,11 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="委托" xml:space="preserve">
<value>Mission</value>
<data name="尘歌壶" xml:space="preserve">
<value>Sérénithéière</value>
</data>
<data name="挪德卡莱" xml:space="preserve">
<value>Nod-Krai</value>
</data>
<data name="枫丹" xml:space="preserve">
<value>Fontaine</value>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -117,8 +117,11 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="委托" xml:space="preserve">
<value>委托</value>
<data name="尘歌壶" xml:space="preserve">
<value>尘歌壶</value>
</data>
<data name="挪德卡莱" xml:space="preserve">
<value>挪德卡莱</value>
</data>
<data name="枫丹" xml:space="preserve">
<value>枫丹</value>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -117,8 +117,11 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="委托" xml:space="preserve">
<value>委[託話]</value>
<data name="尘歌壶" xml:space="preserve">
<value>塵歌壺</value>
</data>
<data name="挪德卡莱" xml:space="preserve">
<value>挪德卡萊</value>
</data>
<data name="枫丹" xml:space="preserve">
<value>楓丹</value>

View File

@@ -0,0 +1,16 @@
using BetterGenshinImpact.GameTask.Model;
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace BetterGenshinImpact.GameTask.AutoTrackPath
{
public class TpTaskParam : BaseTaskParam<TpTask>
{
public TpTaskParam(CultureInfo? gameCultureInfo = null, IStringLocalizer<TpTask>? stringLocalizer = null) : base(gameCultureInfo, stringLocalizer)
{
}
}
}

View File

@@ -0,0 +1,4 @@
namespace BetterGenshinImpact.GameTask.Common.BgiVision
{
internal abstract class BvResxHelper; // Bv是静态类无法作为IStringLocalizer<T>泛型类,用这个类代替
}

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="复苏" xml:space="preserve">
<value>revival</value>
</data>
</root>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="复苏" xml:space="preserve">
<value>réanimation</value>
</data>
</root>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="复苏" xml:space="preserve">
<value>复苏</value>
</data>
</root>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="复苏" xml:space="preserve">
<value>復甦</value>
</data>
</root>

View File

@@ -1,16 +1,18 @@
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.QuickTeleport.Assets;
using OpenCvSharp;
using System;
using System.Linq;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Recognition;
using System.Threading;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.GameTask.AutoSkip.Assets;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.GameLoading.Assets;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.QuickTeleport.Assets;
using BetterGenshinImpact.Helpers;
using Microsoft.Extensions.Localization;
using OpenCvSharp;
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BetterGenshinImpact.GameTask.Common.BgiVision;
@@ -23,7 +25,7 @@ namespace BetterGenshinImpact.GameTask.Common.BgiVision;
/// </summary>
public static partial class Bv
{
public static string WhichGameUi()
{
throw new NotImplementedException();
@@ -36,7 +38,7 @@ public static partial class Bv
/// <returns></returns>
public static bool IsInMainUi(ImageRegion captureRa)
{
return captureRa.Find(ElementAssets.Instance.PaimonMenuRo).IsExist() && !IsInRevivePrompt(captureRa);
return captureRa.Find(ElementAssets.Instance.PaimonMenuRo).IsExist() && !IsInRevivePrompt(captureRa);
}
/// <summary>
@@ -59,7 +61,7 @@ public static partial class Bv
return false;
}
/// <summary>
/// 是否在秘境中
/// </summary>
@@ -186,7 +188,7 @@ public static partial class Bv
/// </summary>
/// <param name="region"></param>
/// <returns></returns>
public static bool IsInRevivePrompt(ImageRegion region)
internal static bool IsInRevivePrompt(ImageRegion region)
{
using var confirmRectArea = region.Find(AutoFightAssets.Instance.ConfirmRa);
if (!confirmRectArea.IsEmpty())
@@ -196,7 +198,11 @@ public static partial class Bv
RecognitionType = RecognitionTypes.Ocr,
RegionOfInterest = new Rect(0, 0, region.Width, region.Height / 2)
});
if (list.Any(r => r.Text.Contains("复苏")))
CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
IStringLocalizer stringLocalizer = App.GetService<IStringLocalizer<BvResxHelper>>() ?? throw new Exception();
string revival = stringLocalizer.WithCultureGet(cultureInfo, "复苏");
if (list.Any(r => r.Text.Contains(revival)))
{
return true;
}
@@ -273,6 +279,17 @@ public static partial class Bv
{
return await NewRetry.WaitForAction(() => IsInTalkUi(TaskControl.CaptureToRectArea()), ct, retryTimes, 500);
}
/// <summary>
/// 是否存在提示框/确认框
/// 黑白款都能识别
/// </summary>
/// <param name="captureRa"></param>
/// <returns></returns>
public static bool IsInPromptDialog(ImageRegion captureRa)
{
return captureRa.Find(ElementAssets.Instance.PromptDialogLeftBottomStar).IsExist();
}
}
public enum MotionStatus

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -8,6 +8,8 @@ namespace BetterGenshinImpact.GameTask.Common.Element.Assets;
public class ElementAssets : BaseAssets<ElementAssets>
{
public RecognitionObject PromptDialogLeftBottomStar; // 弹出框左下角的星星
public RecognitionObject BtnWhiteConfirm;
public RecognitionObject BtnWhiteCancel;
public RecognitionObject BtnBlackConfirm;
@@ -87,6 +89,14 @@ public class ElementAssets : BaseAssets<ElementAssets>
private ElementAssets()
{
PromptDialogLeftBottomStar = new RecognitionObject
{
Name = "PromptDialogLeftBottomStar",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "prompt_dialog_left_bottom_star.png"),
RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height - CaptureRect.Height / 2),
Threshold = 0.8,
}.InitTemplate();
// 按钮
BtnWhiteConfirm = new RecognitionObject
{
@@ -242,7 +252,7 @@ public class ElementAssets : BaseAssets<ElementAssets>
Name = "fragileResinCount",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "fragile_resin_count.png"),
RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2),
RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height * 3/ 4, CaptureRect.Width / 3, CaptureRect.Height / 6),
DrawOnWindow = true
}.InitTemplate();
CondensedResinCount = new RecognitionObject

View File

@@ -1,7 +1,5 @@
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.GameTask.AutoTrackPath.Model;
using BetterGenshinImpact.Service;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.IO;

View File

@@ -24,19 +24,19 @@ namespace BetterGenshinImpact.GameTask.Common.Job
private readonly InputSimulator input = Simulation.SendInput;
private CancellationToken ct;
private readonly GridScreenName gridScreenName;
private readonly string foodName;
private readonly string itemName;
public CountInventoryItem(GridScreenName gridScreenName, string foodName)
public CountInventoryItem(GridScreenName gridScreenName, string itemName)
{
this.gridScreenName = gridScreenName;
this.foodName = foodName;
this.itemName = itemName;
}
public async Task<int> Start(CancellationToken ct)
{
this.ct = ct;
logger.LogInformation("打开背包并在{grid}寻找{name}……", this.gridScreenName, this.foodName);
logger.LogInformation("打开背包并在{grid}寻找{name}……", this.gridScreenName, this.itemName);
await new ReturnMainUiTask().Start(ct);
await AutoArtifactSalvageTask.OpenInventory(this.gridScreenName, input, logger, this.ct);
@@ -50,7 +50,7 @@ namespace BetterGenshinImpact.GameTask.Common.Job
using Mat icon = itemRegion.SrcMat.GetGridIcon();
var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes);
string predName = result.Item1;
if (predName == this.foodName)
if (predName == this.itemName)
{
string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle);
if (int.TryParse(numStr, out int num))
@@ -69,7 +69,7 @@ namespace BetterGenshinImpact.GameTask.Common.Job
if (count == null)
{
count = -1;
logger.LogInformation("没有找到{name}", this.foodName);
logger.LogInformation("没有找到{name}", this.itemName);
}
await new ReturnMainUiTask().Start(ct);

View File

@@ -94,7 +94,7 @@ public class GoToCraftingBenchTask
{
InitConfigList();
// 3. 点击合成树脂
if (SelectedConfig.MinResinToKeep > 0){//开关判断填写的数量大于0时启用 SelectedConfig.MinResinToKeep
if (SelectedConfig?.MinResinToKeep > 0){//开关判断填写的数量大于0时启用 SelectedConfig.MinResinToKeep
var fragileResinCount = 0;
var condensedResinCount = 0;
var fragileResinCountRa = ra.Find(ElementAssets.Instance.fragileResinCount);
@@ -105,7 +105,7 @@ public class GoToCraftingBenchTask
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)");
var match = System.Text.RegularExpressions.Regex.Match(count, @"(\d+)\s*[/17]\s*(6|60)");
if (match.Success)
{
var numericPart = match.Groups[1].Value;
@@ -113,18 +113,30 @@ public class GoToCraftingBenchTask
Logger.LogInformation("提取到的原粹树脂数量:{fragileResinCount}", fragileResinCount);
}
}
var condensedResinCountRa = ra.Find(ElementAssets.Instance.CondensedResinCount);
if (!condensedResinCountRa.IsEmpty())
//浓缩纠缠重试
var condensed =await NewRetry.WaitForAction(() =>
{
// 图像右侧就是浓缩树脂数量
var countArea = ra.DeriveCrop(condensedResinCountRa.X + condensedResinCountRa.Width,
condensedResinCountRa.Y, condensedResinCountRa.Width*5/3, condensedResinCountRa.Height);
var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat);
condensedResinCount = StringUtils.TryParseInt(count);
var condensedResinCountRa = ra.Find(ElementAssets.Instance.CondensedResinCount);
if (!condensedResinCountRa.IsEmpty())
{
// 图像右侧就是浓缩树脂数量
var countArea = ra.DeriveCrop(condensedResinCountRa.X + condensedResinCountRa.Width,
condensedResinCountRa.Y, condensedResinCountRa.Width*5/3, condensedResinCountRa.Height);
var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat);
condensedResinCount = StringUtils.TryParseInt(count);
}
return condensedResinCount >= 0 && condensedResinCount <=5;
},ct,3,200);
if (!condensed)
{
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE);
await new ReturnMainUiTask().Start(ct);
throw new Exception($"识别浓缩树脂数量失败: {condensedResinCount}");
}
//todo 可加纠错机制判断树脂数量是否正确
// 每次合成消耗的数量
const int resinConsumedPerCraft = 40;
const int resinConsumedPerCraft = 60;
// 需要保留的最小数量
int minResinToKeep = SelectedConfig.MinResinToKeep;
// 可以用来合成的树脂数量

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
@@ -11,17 +6,25 @@ using BetterGenshinImpact.GameTask.AutoTrackPath;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.QuickSereniteaPot;
using BetterGenshinImpact.GameTask.QuickTeleport.Assets;
using BetterGenshinImpact.Helpers;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using OpenCvSharp;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using BetterGenshinImpact.Core.Config;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using BetterGenshinImpact.GameTask.QuickSereniteaPot;
using BetterGenshinImpact.Core.Recognition.OCR;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
namespace BetterGenshinImpact.GameTask.Common.Job;
@@ -83,9 +86,10 @@ internal class GoToSereniteaPotTask
TaskContext.Instance().PostMessageSimulator.SimulateAction(GIActions.OpenMap); // 打开地图
await Delay(900, ct);
// 进入 壶
await ChangeCountryForce("尘歌壶", ct);
TpTask tpTask = new TpTask(ct);
await tpTask.SwitchArea("尘歌壶");
// 若未找到 ElementAssets.Instance.SereniteaPotRo 就是已经在尘歌壶了
var ra = CaptureToRectArea();
@@ -377,9 +381,7 @@ internal class GoToSereniteaPotTask
RecognitionType = RecognitionTypes.Ocr,
RegionOfInterest = new Rect((int)(ra.Width * 0.7), (int)(ra.Height * 0.35), (int)(ra.Width * 0.2), (int)(ra.Height * 0.15))
});
IStringLocalizer<MapLazyAssets> stringLocalizer = App.GetService<IStringLocalizer<MapLazyAssets>>() ?? throw new NullReferenceException(nameof(stringLocalizer));
CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
string shopOff = stringLocalizer.WithCultureGet(cultureInfo, "已售");
string shopOff = "已售";
var shopOffRo = list.FirstOrDefault(r => r.Text.Contains(shopOff));
if (shopOffRo != null)
{
@@ -419,7 +421,28 @@ internal class GoToSereniteaPotTask
{
Logger.LogInformation("领取尘歌壶奖励:{text}", "领取好感和宝钱");
await Delay(1000, ct);
CaptureToRectArea().Find(ElementAssets.Instance.SereniteaPotLoveRo, a => a.Click());
var getAare = CaptureToRectArea();
var count = OcrFactory.Paddle.OcrWithoutDetector(getAare.DeriveCrop(getAare.Width* 1801 / 1920,
getAare.Height* 609 / 1080,getAare.Width * 75 / 1920,getAare.Width * 46 / 1920).SrcMat);
var match = System.Text.RegularExpressions.Regex.Match(count, @"(\d+)\s*[/17]\s*(8)");
var shouldClick = true;
if (match.Success)
{
var numericPart = StringUtils.TryParseInt(match.Groups[1].Value);
if (numericPart == 0)
{
Logger.LogWarning("领取尘歌壶奖励:{text}", "没有角色可领取好感"); //存好感
shouldClick = false;
}
}
if (shouldClick)
{
getAare.Find(ElementAssets.Instance.SereniteaPotLoveRo, a => a.Click());
}
await Delay(500, ct);
var ra = CaptureToRectArea();
var list = ra.FindMulti(new RecognitionObject
@@ -603,34 +626,6 @@ internal class GoToSereniteaPotTask
await tp.Tp(4508.97509765625, 3630.557373046875); // TP到枫丹
}
private async Task ChangeCountryForce(string country, CancellationToken ct)
{
GameCaptureRegion.GameRegionClick((rect, scale) => (rect.Width - 160 * scale, rect.Height - 60 * scale));
await Delay(500, ct);
using var ra = CaptureToRectArea();
var list = ra.FindMulti(new RecognitionObject
{
RecognitionType = RecognitionTypes.Ocr,
RegionOfInterest = new Rect(ra.Width / 2, 0, ra.Width / 2, ra.Height)
});
IStringLocalizer<MapLazyAssets> stringLocalizer = App.GetService<IStringLocalizer<MapLazyAssets>>() ?? throw new NullReferenceException(nameof(stringLocalizer));
CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
string minCountryLocalized = stringLocalizer.WithCultureGet(cultureInfo, country);
string commissionLocalized = stringLocalizer.WithCultureGet(cultureInfo, "委托");
Region? matchRect = list.FirstOrDefault(r => r.Text.Length == minCountryLocalized.Length && !r.Text.Contains(commissionLocalized) && r.Text.Contains(minCountryLocalized));
if (matchRect == null)
{
Logger.LogWarning("切换区域失败:{Country}", country);
}
else
{
matchRect.Click();
Logger.LogInformation("切换到区域:{Country}", country);
}
await Delay(500, ct);
}
public async Task DoOnce(CancellationToken ct)
{
InitConfigList();

View File

@@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.AutoSkip.Assets;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.View.Drawable;
@@ -66,14 +67,16 @@ public class SetTimeTask
GameCaptureRegion.GameRegion1080PPosClick(45, 715);
await Delay(400, ct);
await _returnMainUiTask.Start(ct);
// 跳过动画不总能成功
if (Bv.IsInMainUi(CaptureToRectArea()))
{
return;
}
}
else
{
await Delay(3000, ct);
// 出现X的时候代表时间切换成功
await NewRetry.WaitForAction(() => CaptureToRectArea().Find(ElementAssets.Instance.PageCloseWhiteRo).IsExist(), ct, 25);
await _returnMainUiTask.Start(ct);
}
await Delay(3000, ct);
// 出现X的时候代表时间切换成功
await NewRetry.WaitForAction(() => CaptureToRectArea().Find(ElementAssets.Instance.PageCloseWhiteRo).IsExist(), ct, 25);
await _returnMainUiTask.Start(ct);
}
// 取消动画函数

View File

@@ -1,19 +1,20 @@
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Common.Exceptions;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.View.Drawable;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.GameTask.Common.Exceptions;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using System.Text.RegularExpressions;
namespace BetterGenshinImpact.GameTask.Common.Job;
@@ -28,10 +29,10 @@ public class SwitchPartyTask
public async Task<bool> Start(string partyName, CancellationToken ct)
{
bool isInPartyViewUi = false;
Logger.LogInformation("尝试切换至队伍: {Name}", partyName);
using var ra1 = CaptureToRectArea();
if (!Bv.IsInPartyViewUi(ra1))
{
isInPartyViewUi = true;
@@ -55,7 +56,7 @@ public class SwitchPartyTask
Simulation.SendInput.SimulateAction(GIActions.OpenPartySetupScreen);
// 考虑加载时间 2s共检查 4.2s,如果失败则抛出异常
for (int i = 0; i < 7; i++) // 检查 7 次
{
await Delay(600, ct);
@@ -102,7 +103,7 @@ public class SwitchPartyTask
await Delay(500, ct);
await _returnMainUiTask.Start(ct);
}
return true;
}
@@ -135,9 +136,6 @@ public class SwitchPartyTask
}
}
var nextX = partyDeleteBtn.Left;
var nextY = partyDeleteBtn.Top - partyDeleteBtn.Height * 2;
// 点击到最上方
await Task.Delay(50, ct);
GameCaptureRegion.GameRegion1080PPosClick(700, 120);
@@ -146,26 +144,68 @@ public class SwitchPartyTask
await Task.Delay(450, ct);
Simulation.SendInput.Mouse.LeftButtonUp();
// 逐页查找
for (var i = 0; i < 11; i++)
Rect regionOfInterest = new Rect(0, (int)(80 * _assetScale), partyDeleteBtn.Right, partyDeleteBtn.Top - (int)(80 * _assetScale));
RecognitionObject recognitionObject = new RecognitionObject
{
using var page = CaptureToRectArea();
var found = await FindPage(partyName, page, partyDeleteBtn, ct, isInPartyViewUi);
if (found)
RecognitionType = RecognitionTypes.Ocr,
RegionOfInterest = regionOfInterest,
DrawOnWindow = true,
Name = "队伍名称",
DrawOnWindowPen= new System.Drawing.Pen(System.Drawing.Color.White)
};
// 逐页查找
try
{
for (var i = 0; i < 16; i++) // 6.0版本最多20个队伍
{
RunnerContext.Instance.ClearCombatScenes();
return true;
}
using var page = CaptureToRectArea();
// 点击下一页
if (i == 0)
{
// #ebe4d8 首次点一下第一个,防止第五个被点击过
page.ClickTo(600 * _assetScale, 200 * _assetScale);
}
var partySwitchNameRaList = page.FindMulti(recognitionObject);
page.ClickTo(nextX, nextY); // 点击最下方队伍下移
await Delay(400, ct);
if (partySwitchNameRaList == null || partySwitchNameRaList.Count <= 0)
{
Logger.LogInformation("管理队伍界面文字识别失败");
break;
}
// 当前页存在则直接点击
foreach (var textRegion in partySwitchNameRaList)
{
if (Regex.IsMatch(textRegion.Text, partyName))
{
page.ClickTo(textRegion.Right + textRegion.Width, textRegion.Bottom);
await Delay(200, ct);
Logger.LogInformation("切换队伍成功: {Text}", textRegion.Text);
await ConfirmParty(page, ct, isInPartyViewUi);
RunnerContext.Instance.ClearCombatScenes();
return true;
}
}
Region lowest = partySwitchNameRaList.Where(r => r.X > 35 * _assetScale && r.X < 100 * _assetScale).OrderBy(r => r.Y).Last();
lowest.DrawSelf("底部的队伍");
if (lowest.Y < 777 * _assetScale) // 如果最底下是空队伍则不会有队伍名,以此判断是否已遍历完成
{
Logger.LogInformation("已抵达最后一个队伍");
break;
}
// 点击下一页
if (i == 0)
{
// #ebe4d8 首次点一下第一个,防止第五个被点击过
page.ClickTo(600 * _assetScale, 200 * _assetScale);
}
page.ClickTo(regionOfInterest.X + regionOfInterest.Width / 2, lowest.Bottom); // 点击最下方队伍下移
await Delay(400, ct);
}
}
finally
{
VisionContext.Instance().DrawContent.ClearAll();
}
// 未找到
@@ -175,30 +215,6 @@ public class SwitchPartyTask
return false;
}
private async Task<bool> FindPage(string partyName, ImageRegion page, Region partyDeleteBtn, CancellationToken ct, bool isInPartyViewUi = false)
{
var partySwitchNameRaList = page.FindMulti(new RecognitionObject
{
RecognitionType = RecognitionTypes.Ocr,
RegionOfInterest = new Rect(0, (int)(80 * _assetScale), partyDeleteBtn.Right, partyDeleteBtn.Top - (int)(80 * _assetScale))
});
// 当前页存在则直接点击
foreach (var textRegion in partySwitchNameRaList)
{
if (Regex.IsMatch(textRegion.Text, partyName))
{
page.ClickTo(textRegion.Right + textRegion.Width, textRegion.Bottom);
await Delay(200, ct);
Logger.LogInformation("切换队伍成功: {Text}", textRegion.Text);
await ConfirmParty(page, ct, isInPartyViewUi);
return true;
}
}
return false;
}
private async Task ConfirmParty(ImageRegion page, CancellationToken ct, bool isInPartyViewUi = false)
{
var r1 = Bv.ClickWhiteConfirmButton(page.DeriveCrop(0, page.Height / 4, page.Width / 4, page.Height - page.Height / 4));
@@ -215,6 +231,6 @@ public class SwitchPartyTask
using var ra = CaptureToRectArea();
var r2 = Bv.ClickWhiteConfirmButton(ra.DeriveCrop(page.Width - page.Width / 4, page.Height / 4, page.Width / 4, page.Height - page.Height / 4));
await Delay(500, ct);
if (isInPartyViewUi)await _returnMainUiTask.Start(ct);
if (isInPartyViewUi) await _returnMainUiTask.Start(ct);
}
}

View File

@@ -19,9 +19,11 @@ namespace BetterGenshinImpact.GameTask.GameLoading;
public class GameLoadingTrigger : ITaskTrigger
{
public static bool GlobalEnabled = true;
public string Name => "自动开门";
public bool IsEnabled { get; set; }
public bool IsEnabled { get => GlobalEnabled; set {} }
public int Priority => 999;
@@ -61,9 +63,17 @@ public class GameLoadingTrigger : ITaskTrigger
_assets = GameLoadingAssets.Instance;
}
public void InnerSetEnabled(bool enabled)
{
GlobalEnabled = enabled;
}
public void Init()
{
IsEnabled = _config.AutoEnterGameEnabled;
if (!_config.AutoEnterGameEnabled)
{
InnerSetEnabled(false);
}
// // 前面没有联动启动原神,这个任务也不用启动
// if ((DateTime.Now - TaskContext.Instance().LinkedStartGenshinTime).TotalMinutes >= 5)
@@ -234,14 +244,14 @@ public class GameLoadingTrigger : ITaskTrigger
// 5min 后自动停止
if ((DateTime.Now - _triggerStartTime).TotalMinutes >= 5)
{
IsEnabled = false;
InnerSetEnabled(false);
return;
}
// 成功进入游戏判断
if (Bv.IsInMainUi(content.CaptureRectArea) || Bv.IsInAnyClosableUi(content.CaptureRectArea) || Bv.IsInDomain(content.CaptureRectArea))
{
_logger.LogInformation("已进入游戏");
IsEnabled = false;
// _logger.LogInformation("当前在游戏主界面");
InnerSetEnabled(false);
return;
}

View File

@@ -16,6 +16,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Config;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
namespace BetterGenshinImpact.GameTask.GetGridIcons;
@@ -51,7 +52,7 @@ public class GridIconsAccuracyTestTask : ISoloTask
public static InferenceSession LoadModel(out Dictionary<string, float[]> prototypes)
{
#region model
var session = new InferenceSession(@".\Assets\Model\Item\gridIcon.onnx");
var session = new InferenceSession(Global.Absolute(@"Assets\Model\Item\gridIcon.onnx"));
var metadata = session.ModelMetadata;
@@ -62,7 +63,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(@".\Assets\Model\Item\items.csv").Skip(1); // 跳过首行列名
var allLines = File.ReadLines(Global.Absolute(@"Assets\Model\Item\items.csv")).Skip(1); // 跳过首行列名
prototypes = new Dictionary<string, float[]>();
foreach (string line in allLines)
{
@@ -113,15 +114,15 @@ public class GridIconsAccuracyTestTask : ISoloTask
Task task1 = Delay(300, ct);
// 用模型推理得到的结果
Task<(string, int)> task2 = Task.Run(() =>
Task<(string?, int)> task2 = Task.Run(() =>
{
using Mat icon = itemRegion.SrcMat.GetGridIcon();
return Infer(icon, session, prototypes);
}, ct);
await Task.WhenAll(task1, task2);
(string, int) result = task2.Result;
string predName = result.Item1;
(string?, int) result = task2.Result;
string? predName = result.Item1;
int predStarNum = result.Item2;
// 用CV方法得到的结果
@@ -135,7 +136,11 @@ public class GridIconsAccuracyTestTask : ISoloTask
// 统计结果
total_count++;
if (itemName.Contains(predName) && predStarNum == itemStarNum)
if (predName == null)
{
logger.LogInformation($"模型没有识别,应为:{itemName}|{itemStarNum}星,❌,正确率{total_acc / total_count:0.00}");
}
else if (itemName.Contains(predName) && predStarNum == itemStarNum)
{
total_acc++;
logger.LogInformation($"{predName}|{predStarNum}星,✔,正确率{total_acc / total_count:0.00}");
@@ -162,7 +167,7 @@ public class GridIconsAccuracyTestTask : ISoloTask
/// <param name="prototypes"></param>
/// <returns>(预测名称, 预测星级)</returns>
/// <exception cref="Exception"></exception>
public static (string, int) Infer(Mat mat, InferenceSession session, Dictionary<string, float[]> prototypes)
public static (string?, int) Infer(Mat mat, InferenceSession session, Dictionary<string, float[]> prototypes)
{
if (mat.Size().Width != 125 || mat.Size().Height != 125)
{
@@ -193,15 +198,18 @@ public class GridIconsAccuracyTestTask : ISoloTask
}
if (min2 == null || distance2 < min2)
{
pred_name = prototype.Key;
min2 = distance2;
if (min2 < 10 * 10) // todo负样本距离10直接读取模型
{
pred_name = prototype.Key;
}
}
}
if (pred_name == null || min2 == null)
if (min2 == null)
{
throw new Exception("特征数据为空");
}
min2 = Math.Sqrt(min2.Value);
// min2 = Math.Sqrt(min2.Value);
int pred_star = results[2].AsEnumerable<float>().ToList().IndexOf(results[2].AsEnumerable<float>().Max());
return (pred_name, pred_star);
}

View File

@@ -12,6 +12,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using BetterGenshinImpact.GameTask.GameLoading;
using Fischless.GameCapture.Graphics;
using Vanara.PInvoke;
@@ -116,6 +117,7 @@ namespace BetterGenshinImpact.GameTask
// 初始化触发器(一定要在任务上下文初始化完毕后使用)
_triggers = GameTaskManager.LoadInitialTriggers();
GameLoadingTrigger.GlobalEnabled = TaskContext.Instance().Config.GenshinStartConfig.AutoEnterGameEnabled;
// if (GraphicsCapture.IsHdrEnabled(hWnd))
// {

View File

@@ -1,11 +1,9 @@
using BetterGenshinImpact.GameTask.Common.BgiVision;
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace BetterGenshinImpact.Helpers;
public static class CultureHelper

View File

@@ -89,19 +89,19 @@ public static class ConsoleHelper
// 重定向标准输出流
var stdOutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
var stdOutStream = new FileStream(stdOutHandle, FileAccess.Write);
var stdOutWriter = new StreamWriter(stdOutStream, Encoding.UTF8) { AutoFlush = true };
var stdOutWriter = new StreamWriter(stdOutStream, Console.OutputEncoding) { AutoFlush = true };
Console.SetOut(stdOutWriter);
// 重定向标准错误流
var stdErrHandle = GetStdHandle(STD_ERROR_HANDLE);
var stdErrStream = new FileStream(stdErrHandle, FileAccess.Write);
var stdErrWriter = new StreamWriter(stdErrStream, Encoding.UTF8) { AutoFlush = true };
var stdErrWriter = new StreamWriter(stdErrStream, Console.OutputEncoding) { AutoFlush = true };
Console.SetError(stdErrWriter);
// 重定向标准输入流
var stdInHandle = GetStdHandle(STD_INPUT_HANDLE);
var stdInStream = new FileStream(stdInHandle, FileAccess.Read);
var stdInReader = new StreamReader(stdInStream, Encoding.UTF8);
var stdInReader = new StreamReader(stdInStream, Console.InputEncoding);
Console.SetIn(stdInReader);
}

View File

@@ -119,7 +119,7 @@ public class ConditionDefinitions
{
Subject = "动作",
Description = "路线中含有特殊动作时使用的队伍名称,优先级高于采集物配置。队伍名称是你在游戏中手动设置的队伍名称文字。队伍中,必须存在和动作相关的角色。程序会自动识别并使用对应的角色执行动作,具体见文档",
ObjectOptions = new List<string> { "纳西妲采集", "水元素采集", "雷元素采集", "风元素采集" },
ObjectOptions = new List<string> { "纳西妲采集", "水元素采集", "雷元素采集", "风元素采集", "火元素采集" },
}
},
{
@@ -145,5 +145,6 @@ public class ConditionDefinitions
{ "hydro_collect", "水元素采集" },
{ "electro_collect", "雷元素采集" },
{ "anemo_collect", "风元素采集" },
{ "pyro_collect", "火元素采集" },
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

@@ -429,7 +429,10 @@ public partial class ScriptService : IScriptService
if (!fisrt&&!RunnerContext.Instance.IsPreExecution)
{
Notify.Event(NotificationEvent.GroupEnd).Success($"配置组{groupName}结束");
if (CancellationContext.Instance.IsManualStop is false)
{
Notify.Event(NotificationEvent.GroupEnd).Success($"配置组{groupName}结束");
}
}
if (taskProgress != null)
@@ -611,7 +614,6 @@ public partial class ScriptService : IScriptService
GlobalMethod.MoveMouseTo(300, 300);
}
}
}
});

View File

@@ -0,0 +1,54 @@
// 作者:火山
// 描述万能战斗策略新手推荐。需要在调度器→配置组→设置→战斗配置开启→自动检测战斗结束开启→更快检查结束战斗开启→旋转寻找敌人位置开启→检查战斗结束的延时0.1→按键触发后检查延时0.35→盾奶角色优先释放技能开启设置你的盾位→战斗结束后使用万叶长E手机掉落物开启
// 盾(刚需)
茜特菈莉 attack,e,wait(0.2),keypress(q),wait(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
伊涅芙 e,attack(0.22),keypress(q),wait(0.1),keypress(q),attack(0.2),keypress(q),attack(0.2)
钟离 s(0.2), e(hold), wait(0.2), w(0.2),keypress(q),wait(0.2),keypress(q),attack(0.1)
莱依拉 e,wait(0.2), keypress(q),wait(0.2),keypress(q),attack(0.2),keypress(q),attack(0.2)
绮良良 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
托马 e,attack(0.22),keypress(q),wait(0.1),keypress(q),attack(0.2),keypress(q),attack(0.2)
蓝砚 e,attack(0.15), click(middle),attack(0.15),click(middle),attack(0.15),click(middle),wait(0.2).dash(0.1),attack(0.2)
// 后台、挂元素、副C、先手
玛薇卡 attack(0.2),e
迪希雅 e,attack(0.2),e
香菱 e,wait(0.3),keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
仆人 attack,e
那维莱特 attack(0.23),e
纳西妲 e(hold),click(middle),keypress(q),wait(0.3),keypress(q),attack(0.3),keypress(q),attack(0.2)
艾梅莉埃 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
丝柯克 attack(0.2),click(middle),keypress(q),wait(0.05),keypress(q),attack(0.05),click(middle),keydown(E),wait(0.22),attack(0.08),click(middle),keyup(E),keypress(q),wait(0.08),keypress(q)
芙宁娜 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
白术 e,attack(0.2)
芭芭拉 e,attack(0.2)
希格雯 e(hold),wait(0.2),keypress(q),wait(0.2),keypress(q)
爱可菲 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
菲谢尔 e
欧洛伦 e,attack(0.3), keypress(q),wait(0.2),attack(0.3),keypress(q),wait(0.2),attack(0.3),keypress(q),wait(0.3)
雷电将军 e,attack(0.22),keypress(q),wait(0.1),keypress(q),attack(0.2),keypress(q),attack(0.2)
久岐忍 e,wait(0.2),keypress(q),attack(0.15),keypress(q),e
瓦雷莎 e, attack(1.25),wait(0.45), s(0.4), click(middle), e, attack(1.25), wait(0.3),keypress(q), wait(0.45)
// 中置位
夏沃蕾 attack(0.08),keypress(q),wait(0.2),keypress(q),wait(0.2),attack(0.2),keydown(e),wait(0.15), moveby(0,900),wait(0.15),keyup(e),attack(0.15)
白术 attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2)
那维莱特 attack(0.08),keypress(q),wait(0.22),keypress(q),wait(0.2),keypress(q),e
//减抗
希诺宁 s(0.2),e,w(0.2),attack(0.35),wait(0.1),attack(0.35),keypress(x), wait(0.2), keypress(q), wait(0.3), keypress(q),keypress(x), wait(0.08), keypress(x),attack(0.2)
枫原万叶 attack(0.08),keypress(q),wait(0.3),keypress(q),wait(0.3),attack(0.2),keydown(E),wait(0.48),keyup(E),attack(0.3), wait(0.5),attack(0.1)
砂糖 e,attack(0.2),keypress(q),attack(0.2),keypress(q),e,attack(0.2)
//爆发
玛薇卡 e,click(middle),wait(0.12), keypress(q), wait(0.3), keypress(q), wait(0.3), charge(3.8), keydown(space), wait(0.1), keyup(space), attack(0.2),wait(0.2)
//收尾,长轴
那维莱特 charge(3),j,wait(0.3)
丝柯克 attack(0.05),keypress(e),wait(0.05),keypress(e),wait(0.2),attack(2.27),keypress(Q),dash,attack(2.27),keydown(S),keypress(Q),dash,keyup(S),attack(2.27),wait(0.11),charge(0.3),attack(1)
迪希雅 keypress(q),attack(0.1),dash(0.2),keypress(q),attack(0.3),keypress(q),attack(0.3),keypress(q),attack(0.3),keydown(S),attack(0.5),keyup(S),keydown(W),attack(0.5),keyup(W),keydown(S),attack(0.5),keyup(S),keydown(W),attack(0.5),keyup(W),keydown(S),attack(0.5),keyup(S),keydown(W),attack(0.5),keyup(W),keydown(S),attack(0.5),keyup(S)
娜维娅 keypress(q),attack(0.1),keypress(q),attack(0.1),keypress(q),attack(0.1),keypress(q),keydown(E),wait(0.8),keyup(E),attack(1.6),keydown(E),wait(0.8),keyup(E),attack(0.1),keydown(S),attack(0.33),keyup(S),keydown(W),attack(0.3),keyup(W),keydown(S),attack(0.3),keyup(S),keydown(W),attack(0.3),keyup(W),keydown(S),attack(0.3),keyup(S),keydown(W),attack(0.3),keyup(W),attack(0.2)
瓦雷莎 e, attack(1.25),wait(0.45), s(0.4), e, attack(1.25), wait(0.3),keypress(q), wait(0.45),s(0.4),e, attack(1.25),wait(0.45), s(0.4), e, attack(1.25), wait(0.3),keypress(q), attack(0.45)
仆人 charge(0.35),j,attack(3.2),j,attack(0.3),attack(0.52),keypress(q),attack(0.2)

View File

@@ -358,6 +358,22 @@
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Command="{Binding OpenScriptFolderCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="打开所在目录">
<MenuItem.Style>
<Style TargetType="MenuItem">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem.Type}"
Value="Shell">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Command="{Binding DeleteScriptCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="移除" />

View File

@@ -69,7 +69,6 @@
<ComboBoxItem Content="CNB" />
</ComboBox>
<ui:Button
Appearance="Success"
Icon="{ui:SymbolIcon ArrowDownload24}"
Content="立即更新"
Command="{Binding UpdateFromGitHostPlatformCommand}" />
@@ -92,7 +91,7 @@
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="普通用户可点此直接更新"
Text="【国内】普通用户可点此直接更新"
TextWrapping="Wrap" />
</Grid>
</ui:CardControl.Header>
@@ -121,7 +120,7 @@
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="Mirror酱用户可以输入CDK高速更新"
Text="【国内】Mirror酱用户可以输入CDK高速更新"
TextWrapping="Wrap" />
</Grid>
</ui:CardControl.Header>

View File

@@ -14,6 +14,7 @@ using BetterGenshinImpact.Model;
using Meziantou.Framework.Win32;
using Wpf.Ui.Controls;
using Wpf.Ui.Violeta.Controls;
using MessageBoxResult = System.Windows.MessageBoxResult;
namespace BetterGenshinImpact.View.Windows;
@@ -30,7 +31,7 @@ public partial class CheckUpdateWindow : FluentWindow
[ObservableProperty] private string selectedGitSource = "Github";
public string GitSourceDescription => SelectedGitSource == "CNB" ? "直接从 CNB 下载并更新" : "直接从 Github 下载并更新";
public string GitSourceDescription => SelectedGitSource == "CNB" ? "【国内】直接从 CNB 下载并更新" : "【国外】直接从 Github 下载并更新";
partial void OnSelectedGitSourceChanged(string value)
{
@@ -44,13 +45,13 @@ public partial class CheckUpdateWindow : FluentWindow
_option = option ?? throw new ArgumentNullException(nameof(option));
DataContext = this;
InitializeComponent();
// 存在CDK则显示修改按钮
if (string.IsNullOrEmpty(MirrorChyanHelper.GetCdk()))
{
EditCdkButton.Visibility = Visibility.Collapsed;
}
if (option.Trigger == UpdateTrigger.Manual)
{
IgnoreButton.Visibility = Visibility.Collapsed;
@@ -62,11 +63,11 @@ public partial class CheckUpdateWindow : FluentWindow
WebpagePanel.Visibility = Visibility.Collapsed;
UpdateStatusMessageGrid.Height = 0;
ShowUpdateStatus = false;
// 隐藏开源渠道和Steambird服务卡片
GitSourceCard.Visibility = Visibility.Collapsed;
SteambirdCard.Visibility = Visibility.Collapsed;
// // 删除前几行
// MyGrid.RowDefinitions.RemoveAt(0);
// MyGrid.RowDefinitions.RemoveAt(0);
@@ -91,7 +92,7 @@ public partial class CheckUpdateWindow : FluentWindow
Closing += OnClosing;
// 延迟显示气泡提示
if (option.Channel != UpdateChannel.Alpha)
{
@@ -103,7 +104,7 @@ public partial class CheckUpdateWindow : FluentWindow
{
showTimer.Stop();
ShowOtherUpdateTip = true;
// 5秒后自动消失
var hideTimer = new DispatcherTimer
{
@@ -118,7 +119,6 @@ public partial class CheckUpdateWindow : FluentWindow
};
showTimer.Start();
}
}
protected void OnClosing(object? sender, CancelEventArgs e)
@@ -165,9 +165,21 @@ public partial class CheckUpdateWindow : FluentWindow
private async Task UpdateFromGitHostPlatformAsync()
{
string source = SelectedGitSource == "CNB" ? "cnb" : "github";
if (source == "github")
{
// 提示用户这个是国外服务器,可能会很慢
var result = await MessageBox.ShowAsync("您已选择「Github」作为更新源。\n请确认您当前网络可正常访问 Github 文件服务?\n若不确定能否访问建议切换至其他更新渠道。\n是否继续使用 Github 渠道更新?",
"警告", System.Windows.MessageBoxButton.OKCancel, MessageBoxImage.Exclamation, System.Windows.MessageBoxResult.None);
if (result != MessageBoxResult.OK)
{
return;
}
}
await RunUpdaterAsync($"-I --source {source}");
}
[RelayCommand]
private async Task UpdateFromSteambirdAsync()
@@ -183,7 +195,7 @@ public partial class CheckUpdateWindow : FluentWindow
{
return;
}
if (_option.Channel == UpdateChannel.Stable)
{
await RunUpdaterAsync("-I --source mirrorc");
@@ -234,7 +246,7 @@ public partial class CheckUpdateWindow : FluentWindow
await UserInteraction.Invoke(this, CheckUpdateWindowButton.Cancel);
}
}
[RelayCommand]
private void EditCdk()
{

View File

@@ -218,7 +218,7 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel
nameof(Config.HotKeyConfig.CancelTaskHotkey),
Config.HotKeyConfig.CancelTaskHotkey,
Config.HotKeyConfig.CancelTaskHotkeyType,
(_, _) => { CancellationContext.Instance.Cancel(); }
(_, _) => { CancellationContext.Instance.ManualCancel(); }
));
systemDirectory.Children.Add(new HotKeySettingModel(
"暂停当前脚本/独立任务",

View File

@@ -665,7 +665,10 @@ public partial class OneDragonFlowViewModel : ViewModel
if (CancellationContext.Instance.Cts.IsCancellationRequested)
{
_logger.LogInformation("任务被取消,退出执行");
Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束");
if (CancellationContext.Instance.IsManualStop is false)
{
Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束");
}
return; // 后续的检查任务也不执行
}
}
@@ -676,7 +679,10 @@ public partial class OneDragonFlowViewModel : ViewModel
{
await new CheckRewardsTask().Start(CancellationContext.Instance.Cts.Token);
await Task.Delay(500);
Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束");
if (CancellationContext.Instance.IsManualStop is false)
{
Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束");
}
_logger.LogInformation("一条龙和配置组任务结束");
// 执行完成后操作

View File

@@ -1,16 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Core.Script.Group;
@@ -32,15 +19,30 @@ using BetterGenshinImpact.ViewModel.Windows.Editable;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using Wpf.Ui;
using Wpf.Ui.Controls;
using Wpf.Ui.Violeta.Controls;
using StackPanel = Wpf.Ui.Controls.StackPanel;
using TextBox = Wpf.Ui.Controls.TextBox;
using Button = Wpf.Ui.Controls.Button;
using MessageBoxButton = System.Windows.MessageBoxButton;
using MessageBoxResult = Wpf.Ui.Controls.MessageBoxResult;
using StackPanel = Wpf.Ui.Controls.StackPanel;
using TextBlock = Wpf.Ui.Controls.TextBlock;
using TextBox = Wpf.Ui.Controls.TextBox;
namespace BetterGenshinImpact.ViewModel.Pages;
@@ -870,183 +872,302 @@ public partial class ScriptControlViewModel : ViewModel
}
[RelayCommand]
private void OnAddPathing()
private async Task OnAddPathing()
{
var root = FileTreeNodeHelper.LoadDirectory<PathingTask>(MapPathingViewModel.PathJsonPath);
var stackPanel = CreatePathingScriptSelectionPanel(root.Children);
var result = PromptDialog.Prompt("请选择需要添加的地图追踪任务", "请选择需要添加的地图追踪任务", stackPanel, new Size(600, 720));
if (!string.IsNullOrEmpty(result))
try
{
AddSelectedPathingScripts((StackPanel)stackPanel.Content);
// 在后台线程中加载数据
var root = await Task.Run(() => FileTreeNodeHelper.LoadDirectory<PathingTask>(MapPathingViewModel.PathJsonPath));
// 异步创建选择面板
var stackPanel = await CreatePathingScriptSelectionPanelAsync(root.Children);
// 显示选择对话框
var result = PromptDialog.Prompt("请选择需要添加的地图追踪任务", "请选择需要添加的地图追踪任务", stackPanel, new Size(600, 720));
if (!string.IsNullOrEmpty(result))
{
AddSelectedPathingScripts((StackPanel)stackPanel.Content);
}
}
catch (Exception ex)
{
Toast.Error($"加载地图追踪任务失败: {ex.Message}");
_logger.LogError(ex, "加载地图追踪任务时发生错误");
}
}
private ScrollViewer CreatePathingScriptSelectionPanel(IEnumerable<FileTreeNode<PathingTask>> list)
// 添加防抖计时器字段
private DispatcherTimer? _debounceTimer;
private const int DebounceDelayMs = 300;
// 存储路径与UI元素的映射
private readonly Dictionary<string, FrameworkElement> _nodeUIElements = [];
/// <summary>
/// 异步创建地图追踪任务选择面板
/// </summary>
private async Task<ScrollViewer> CreatePathingScriptSelectionPanelAsync(IEnumerable<FileTreeNode<PathingTask>> list)
{
var stackPanel = new StackPanel();
CheckBox excludeCheckBox = new CheckBox
CheckBox excludeCheckBox = new()
{
Content = "排除已选择过的目录",
VerticalAlignment = VerticalAlignment.Center,
};
CheckBox deepCheckBox = new CheckBox
CheckBox deepCheckBox = new()
{
Content = "深度搜索",
VerticalAlignment = VerticalAlignment.Center,
};
stackPanel.Children.Add(excludeCheckBox);
stackPanel.Children.Add(deepCheckBox);
var filterTextBox = new TextBox
TextBox filterTextBox = new()
{
Margin = new Thickness(0, 0, 0, 10),
PlaceholderText = "输入筛选条件...",
};
// 设置文本框自动聚焦
filterTextBox.Loaded += (s, e) => filterTextBox.Focus();
filterTextBox.TextChanged += delegate { ApplyFilter(stackPanel, list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); };
excludeCheckBox.Click += delegate { ApplyFilter(stackPanel, list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); };
deepCheckBox.Click += delegate { ApplyFilter(stackPanel, list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); };
// 初始化防抖计时器
_debounceTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(DebounceDelayMs)
};
excludeCheckBox.Click += delegate
{
_ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked);
};
deepCheckBox.Click += delegate
{
_ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked);
};
filterTextBox.TextChanged += delegate
{
_debounceTimer.Stop();
// 设置计时器回调
_debounceTimer.Tick -= OnDebounceTimerTick;
_debounceTimer.Tick += OnDebounceTimerTick;
_debounceTimer.Start();
void OnDebounceTimerTick(object? sender, EventArgs e)
{
_debounceTimer.Stop();
_debounceTimer.Tick -= OnDebounceTimerTick;
_ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked);
}
};
stackPanel.Children.Add(excludeCheckBox);
stackPanel.Children.Add(deepCheckBox);
stackPanel.Children.Add(filterTextBox);
AddNodesToPanel(stackPanel, list, 0, filterTextBox.Text, deepCheckBox.IsChecked);
// 异步构建UI树
await BuildCompleteUITreeAsync(stackPanel, list, 0);
filterTextBox.Focus();
var scrollViewer = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
//Height = 435 // 固定高度
};
return scrollViewer;
}
/// <summary>
/// 应用筛选条件并更新面板显示的文件树节点
/// 异步构建完整的UI树
/// </summary>
/// <param name="parentPanel">要更新的父面板</param>
/// <param name="nodes">要处理的文件树节点集合</param>
/// <param name="filter">用户输入的筛选关键词</param>
/// <param name="excludeSelectedFolder">是否排除已选择的文件夹</param>
/// <param name="isDeepSearch">是否启用深度搜索</param>
private void ApplyFilter(StackPanel parentPanel, IEnumerable<FileTreeNode<PathingTask>> nodes, string filter, bool? excludeSelectedFolder = false, bool? isDeepSearch = false)
/// <param name="parentPanel">构建内容的父容器</param>
/// <param name="nodes">要构建的节点集合</param>
/// <param name="depth">构建的深度</param>
private async Task BuildCompleteUITreeAsync(StackPanel parentPanel, IEnumerable<FileTreeNode<PathingTask>>? nodes, int depth)
{
if (parentPanel.Children.Count > 0)
if (nodes == null)
return;
var nodeList = nodes.ToList();
for (int i = 0; i < nodeList.Count; i += 1)
{
List<UIElement> removeElements = new List<UIElement>();
foreach (UIElement parentPanelChild in parentPanel.Children)
var batch = nodeList.Skip(i).Take(1);
// 在UI线程中创建UI元素并添加到面板
await Application.Current.Dispatcher.InvokeAsync(async () =>
{
if (parentPanelChild is FrameworkElement frameworkElement && frameworkElement.Name.StartsWith("dynamic_"))
foreach (var node in batch)
{
removeElements.Add(frameworkElement);
var element = CreateUIElementForNode(node, depth);
parentPanel.Children.Add(element);
// 如果是目录且有子节点,递归构建子节点
if (node.IsDirectory && node.Children?.Any() == true && element is Expander expander)
{
if (expander.Content is StackPanel childPanel)
{
await BuildCompleteUITreeAsync(childPanel, node.Children, depth + 1);
}
}
}
}
}, DispatcherPriority.Background);
removeElements.ForEach(parentPanel.Children.Remove);
// 让出控制权避免长时间阻塞UI线程
await Task.Delay(1);
}
if (excludeSelectedFolder ?? false)
}
/// <summary>
/// 为单个节点创建UI元素
/// </summary>
/// <param name="node">文件树节点</param>
/// <param name="depth">节点深度</param>
/// <returns>创建的UI元素</returns>
private FrameworkElement CreateUIElementForNode(FileTreeNode<PathingTask> node, int depth)
{
var checkBox = new CheckBox
{
List<string> skipFolderNames = SelectedScriptGroup?.Projects.ToList().Select(item => item.FolderName).Distinct().ToList() ?? [];
//复制Nodes
string jsonString = JsonSerializer.Serialize(nodes);
var copiedNodes = JsonSerializer.Deserialize<ObservableCollection<FileTreeNode<PathingTask>>>(jsonString);
if (copiedNodes != null)
Content = node.FileName,
Tag = node.FilePath,
Margin = new Thickness(depth * 30, 0, 0, 0),
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
};
// 存储路径与UI元素的映射
if (!string.IsNullOrEmpty(node.FilePath))
_nodeUIElements[node.FilePath] = checkBox;
if (node.IsDirectory)
{
// 如果父节点没有任何子内容,则不可勾选
if (node.Children == null || node.Children.Count == 0)
checkBox.IsEnabled = false;
var childPanel = new StackPanel();
checkBox.IsThreeState = true;
var expander = new Expander
{
//路径过滤
copiedNodes = FileTreeNodeHelper.FilterTree(copiedNodes, skipFolderNames);
copiedNodes = FileTreeNodeHelper.FilterEmptyNodes(copiedNodes);
AddNodesToPanel(parentPanel, copiedNodes, 0, filter, isDeepSearch);
Header = checkBox,
Content = childPanel,
IsExpanded = false,
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_"),
Visibility = Visibility.Visible
};
// 存储路径与UI元素的映射
if (!string.IsNullOrEmpty(node.FilePath))
{
_nodeUIElements[node.FilePath + "_expander"] = expander;
}
// 修改事件处理:用户点击时只在全选和全不选之间切换
checkBox.Click += (s, e) => HandleDirectoryCheckBoxClick(checkBox, childPanel);
return expander;
}
else
{
AddNodesToPanel(parentPanel, nodes, 0, filter, isDeepSearch);
// 为文件复选框添加状态改变事件,用于更新父级状态
checkBox.Checked += (s, e) => UpdateParentCheckBoxState(checkBox);
checkBox.Unchecked += (s, e) => UpdateParentCheckBoxState(checkBox);
return checkBox;
}
}
/// <summary>
/// 递归地将文件树节点添加到面板中,支持筛选和深度控制
/// 异步应用筛选到已存在的节点
/// <param name="nodes">要筛选的节点集合</param>
/// <param name="filter">用户输入的筛选关键词</param>
/// <param name="excludeSelectedFolder">排除选择的目录</param>
/// <param name="isDeepSearch">深度搜索功能</param>
/// </summary>
/// <param name="parentPanel">要添加节点的父面板</param>
/// <param name="nodes">要处理的文件树节点集合</param>
/// <param name="depth">当前节点在树中的深度级别</param>
/// <param name="filter">用户输入的筛选关键词,为空时显示所有节点</param>
/// <param name="isDeepSearch">是否启用深度搜索</param>
/// <param name="parentMatched">当前节点的父级是否已经匹配筛选条件</param>
/// <returns>返回是否在当前层级找到了直接匹配的节点以用于递归</returns>
private bool AddNodesToPanel(StackPanel parentPanel, IEnumerable<FileTreeNode<PathingTask>> nodes, int depth, string filter, bool? isDeepSearch = false, bool parentMatched = false)
private async Task ApplyFilterToExistingNodesAsync(IEnumerable<FileTreeNode<PathingTask>> nodes, string filter, bool? excludeSelectedFolder = false, bool? isDeepSearch = false)
{
bool containsDirectMatch = false;
var filteredResult = await Task.Run(() =>
{
IEnumerable<FileTreeNode<PathingTask>> filteredNodes = nodes;
// 如果启用排除已选择过的目录,先过滤掉这些目录
if (excludeSelectedFolder ?? false)
{
List<string> skipFolderNames = SelectedScriptGroup?.Projects.ToList().Select(item => item.FolderName).Distinct().ToList() ?? [];
string jsonString = JsonSerializer.Serialize(nodes);
var copiedNodes = JsonSerializer.Deserialize<ObservableCollection<FileTreeNode<PathingTask>>>(jsonString);
if (copiedNodes != null)
{
copiedNodes = FileTreeNodeHelper.FilterTree(copiedNodes, skipFolderNames);
copiedNodes = FileTreeNodeHelper.FilterEmptyNodes(copiedNodes);
filteredNodes = copiedNodes;
}
}
return filteredNodes;
});
// 在UI线程中更新可见性
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// 重置所有节点的可见性
foreach (var element in _nodeUIElements.Values)
element.Visibility = Visibility.Collapsed;
UpdateNodesVisibility(filteredResult, filter, isDeepSearch);
}, DispatcherPriority.Background);
}
/// <summary>
/// 更新节点的可见性和展开状态
/// </summary>
/// <param name="nodes">要处理的文件树节点集合</param>
/// <param name="filter">用户输入的筛选关键词</param>
/// <param name="isDeepSearch">是否启用深度搜索</param>
/// <param name="depth">当前节点在树中的深度级别</param>
/// <param name="parentMatched">当前节点的父级是否已经匹配筛选条件</param>
/// <param name="returnMatchStatus">是否返回匹配状态(用于子节点处理)</param>
/// <returns>returnMatchStatus则返回是否包含匹配的节点</returns>
private bool UpdateNodesVisibility(IEnumerable<FileTreeNode<PathingTask>> nodes, string filter, bool? isDeepSearch, int depth = 0, bool parentMatched = false, bool returnMatchStatus = false)
{
bool containsMatch = false;
foreach (var node in nodes)
{
// 过滤不符合条件的节点
if (!ShouldShowNode(node, filter, isDeepSearch, depth, parentMatched))
if (string.IsNullOrEmpty(node.FilePath))
continue;
var checkBox = new CheckBox
bool nodeMatches = !string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter);
bool shouldShow = ShouldShowNode(node, filter, isDeepSearch, depth, parentMatched);
// 更新节点可见性
if (_nodeUIElements.TryGetValue(node.FilePath, out var element))
{
Content = node.FileName,
Tag = node.FilePath,
Margin = new Thickness(depth * 30, 0, 0, 0), // 根据深度计算Margin
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
};
if (node.IsDirectory)
{
var childPanel = new StackPanel();
// 获取父文件夹名称,用于特殊深度控制规则(因“地方特产”目录中的详细项目的深度与其他目录不同)
string? parentFolderName = GetParentFolderName(node);
// 获取当前节点是否匹配
bool nodeMatches = !string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter);
// 判断是否应该处理子节点
// 1. 无筛选条件,总是处理
// 2. 有筛选条件,只有深度允许下才处理
bool shouldAddChildren = string.IsNullOrEmpty(filter) || depth < GetMaxDepth(isDeepSearch, parentFolderName, nodeMatches, parentMatched);
// 递归处理子节点
// 1. 只有在应该添加子节点时才进行递归调用
// 2. 传入更新的匹配状态:当前节点匹配或当前节点的父节点匹配
// 3. 返回值表示该节点的子树中是否包含匹配的节点
bool childContainsMatch = shouldAddChildren &&
AddNodesToPanel(childPanel, node.Children, depth + 1, filter, isDeepSearch, nodeMatches || parentMatched);
// 如果子树中包含匹配,当前层级也标记为包含匹配
if (childContainsMatch)
containsDirectMatch = true;
// 如果当前节点匹配,也标记为包含匹配
if (nodeMatches)
containsDirectMatch = true;
var expander = new Expander
{
Header = checkBox,
Content = childPanel,
IsExpanded = ShouldExpandNode(filter, nodeMatches, parentMatched, childContainsMatch, depth, isDeepSearch, parentFolderName),
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
};
checkBox.Checked += (s, e) => SetChildCheckBoxesState(childPanel, true);
checkBox.Unchecked += (s, e) => SetChildCheckBoxesState(childPanel, false);
parentPanel.Children.Add(expander);
element.Visibility = shouldShow ? Visibility.Visible : Visibility.Collapsed;
if (shouldShow && nodeMatches && returnMatchStatus)
containsMatch = true;
}
else
{
parentPanel.Children.Add(checkBox);
// 如果是文件节点且匹配,标记为包含匹配
if (!string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter))
containsDirectMatch = true;
// 如果是目录节点,递归处理子节点并更新展开状态
if (node.IsDirectory && _nodeUIElements.TryGetValue(node.FilePath + "_expander", out var expanderElement) && expanderElement is Expander expander)
{
if (shouldShow)
{
// 递归处理子节点传入returnMatchStatus = true来获取子节点匹配状态
bool childContainsMatch = UpdateNodesVisibility(node.Children, filter, isDeepSearch, depth + 1, nodeMatches || parentMatched, true);
// 如果子节点包含匹配且需要返回匹配状态,当前层级也标记为包含匹配
if (childContainsMatch && returnMatchStatus)
containsMatch = true;
expander.IsExpanded = ShouldExpandNode(filter, nodeMatches, parentMatched, childContainsMatch, depth, isDeepSearch, GetParentFolderName(node));
expander.Visibility = Visibility.Visible;
}
else
{
expander.Visibility = Visibility.Collapsed;
}
}
}
return containsDirectMatch;
return containsMatch;
}
/// <summary>
@@ -1082,8 +1203,7 @@ public partial class ScriptControlViewModel : ViewModel
{
foreach (var child in node.Children)
{
// 递归时,传递当前节点的匹配状态
// 每个子节点深度相同,所以如果递归过程中任意子节点应该显示,则当前节点也应该显示
// 递归时,传递当前节点的匹配状态,任意当前深度的节点应该显示,则当前节点也应该显示
if (ShouldShowNode(child, filter, isDeepSearch, currentDepth + 1, currentNodeMatches))
return true;
}
@@ -1210,7 +1330,154 @@ public partial class ScriptControlViewModel : ViewModel
return defaultDepth;
}
private void SetChildCheckBoxesState(StackPanel childStackPanel, bool state)
/// <summary>
/// 处理目录复选框点击事件
/// </summary>
/// <param name="checkBox">被点击的目录复选框</param>
/// <param name="childPanel">子面板</param>
private void HandleDirectoryCheckBoxClick(CheckBox checkBox, StackPanel childPanel)
{
var childCheckBoxes = GetAllChildCheckBoxes(childPanel);
// 判断目标状态:如果所有子项都已选中,则全不选;否则全选
bool allChildrenChecked = childCheckBoxes.Count > 0 && childCheckBoxes.All(cb => cb.IsChecked == true);
bool targetState = !allChildrenChecked;
checkBox.IsChecked = targetState;
SetChildCheckBoxesState(childPanel, targetState);
UpdateParentCheckBoxState(checkBox);
}
/// <summary>
/// 递归获取面板中所有的子复选框
/// </summary>
/// <param name="panel">获取的面板</param>
/// <returns>所有子复选框列表</returns>
private List<CheckBox> GetAllChildCheckBoxes(StackPanel panel)
{
var checkBoxes = new List<CheckBox>();
foreach (var child in panel.Children)
{
if (child is CheckBox checkBox)
{
checkBoxes.Add(checkBox);
}
else if (child is Expander expander)
{
if (expander.Header is CheckBox headerCheckBox)
{
checkBoxes.Add(headerCheckBox);
}
if (expander.Content is StackPanel nestedPanel)
{
checkBoxes.AddRange(GetAllChildCheckBoxes(nestedPanel));
}
}
}
return checkBoxes;
}
/// <summary>
/// 更新父级复选框的三态状态
/// </summary>
/// <param name="changedCheckBox">状态发生改变的复选框</param>
private void UpdateParentCheckBoxState(CheckBox changedCheckBox)
{
// 查找父级复选框
var parentCheckBox = FindParentCheckBox(changedCheckBox);
if (parentCheckBox == null)
return;
// 获取同级所有复选框
var siblingCheckBoxes = GetSiblingCheckBoxes(changedCheckBox);
// 计算状态
int checkedCount = siblingCheckBoxes.Count(cb => cb.IsChecked == true);
int uncheckedCount = siblingCheckBoxes.Count(cb => cb.IsChecked == false);
int indeterminateCount = siblingCheckBoxes.Count(cb => cb.IsChecked == null);
// 设置父级复选框状态
if (checkedCount == siblingCheckBoxes.Count)
parentCheckBox.IsChecked = true;
else if (uncheckedCount == siblingCheckBoxes.Count)
parentCheckBox.IsChecked = false;
else
parentCheckBox.IsChecked = null;
// 递归更新上级父级
UpdateParentCheckBoxState(parentCheckBox);
}
/// <summary>
/// 查找指定复选框的父级复选框
/// </summary>
/// <param name="checkBox">当前复选框</param>
/// <returns>父级复选框如果没有则返回null</returns>
private CheckBox? FindParentCheckBox(CheckBox checkBox)
{
var filePath = checkBox.Tag as string;
if (string.IsNullOrEmpty(filePath))
return null;
// 获取父目录路径
var parentPath = Path.GetDirectoryName(filePath);
if (string.IsNullOrEmpty(parentPath))
return null;
// 查找父级复选框
if (_nodeUIElements.TryGetValue(parentPath, out var parentElement) && parentElement is CheckBox parentCheckBox)
{
return parentCheckBox;
}
return null;
}
/// <summary>
/// 获取同级的所有复选框
/// </summary>
/// <param name="checkBox">当前复选框</param>
/// <returns>同级复选框列表</returns>
private static List<CheckBox> GetSiblingCheckBoxes(CheckBox checkBox)
{
// 先尝试获取逻辑父级
var parent = LogicalTreeHelper.GetParent(checkBox) as FrameworkElement;
// 如果逻辑父级不存在,尝试可视化父级
parent ??= VisualTreeHelper.GetParent(checkBox) as FrameworkElement;
// 如果当前元素是Expander的Header获取Expander的父级
if (parent is Expander expander)
{
parent = LogicalTreeHelper.GetParent(expander) as FrameworkElement ??
VisualTreeHelper.GetParent(expander) as FrameworkElement;
}
// 遍历同级元素
var siblings = new List<CheckBox>();
if (parent is StackPanel stackPanel)
{
foreach (var child in stackPanel.Children)
{
if (child is CheckBox siblingCheckBox)
siblings.Add(siblingCheckBox);
else if (child is Expander childExpander && childExpander.Header is CheckBox expanderCheckBox)
siblings.Add(expanderCheckBox);
}
}
return siblings;
}
/// <summary>
/// 递归设置子复选框状态
/// </summary>
/// <param name="childStackPanel">子面板</param>
/// <param name="state">目标状态</param>
private static void SetChildCheckBoxesState(StackPanel childStackPanel, bool state)
{
foreach (var child in childStackPanel.Children)
{
@@ -1458,6 +1725,45 @@ public partial class ScriptControlViewModel : ViewModel
);
}
[RelayCommand]
public void OnOpenScriptFolder(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
try
{
string? path = null;
switch (item.Type)
{
case "Javascript":
path = Path.Combine(Global.ScriptPath(), item.FolderName);
break;
case "KeyMouse":
path = Global.Absolute(@"User\KeyMouseScript");
break;
case "Pathing":
path = Path.Combine(MapPathingViewModel.PathJsonPath, item.FolderName);
break;
}
if (path != null && Directory.Exists(path))
{
Process.Start("explorer.exe", path);
}
else
{
_snackbarService.Show("打开失败", "目录不存在", ControlAppearance.Caution, null, TimeSpan.FromSeconds(2));
}
}
catch (Exception e)
{
_logger.LogDebug(e, "打开脚本目录失败");
_snackbarService.Show("打开失败", e.Message, ControlAppearance.Danger, null, TimeSpan.FromSeconds(3));
}
}
private void ScriptGroupsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)

View File

@@ -0,0 +1,81 @@
using OpenCvSharp;
namespace BetterGenshinImpact.Test.Cv;
public class ThresholdWindow
{
private Mat? _originalImage;
private Mat? _grayImage;
private int _currentThreshold = 160;
public static void Test()
{
var window = new ThresholdWindow();
window.ShowThresholdAdjuster(@"E:\HuiTask\更好的原神\自动拾取\pick_ocr_ori_20250915011455192.png");
}
/// <summary>
/// 对指定图片进行二值化阈值拉条调整
/// </summary>
/// <param name="imagePath">图片路径</param>
public void ShowThresholdAdjuster(string imagePath)
{
// 加载原始图像
_originalImage = Cv2.ImRead(imagePath);
if (_originalImage.Empty())
{
throw new ArgumentException("无法加载图像文件");
}
// 转换为灰度图像
_grayImage = new Mat();
Cv2.CvtColor(_originalImage, _grayImage, ColorConversionCodes.BGR2GRAY);
// 创建窗口
const string windowName = "Threshold Adjuster";
const string trackbarName = "Threshold";
Cv2.NamedWindow(windowName, WindowFlags.AutoSize);
// 创建拉条范围0-255
Cv2.CreateTrackbar(trackbarName, windowName, ref _currentThreshold, 255, OnThresholdChanged);
// 初始显示
UpdateThresholdImage(windowName);
// 等待用户按键
Console.WriteLine("按任意键关闭窗口...");
Cv2.WaitKey(0);
// 清理资源
Cv2.DestroyAllWindows();
_originalImage?.Dispose();
_grayImage?.Dispose();
}
/// <summary>
/// 阈值变化回调函数
/// </summary>
/// <param name="value">阈值</param>
/// <param name="userdata">用户数据指针(未使用)</param>
private void OnThresholdChanged(int value, IntPtr userdata)
{
_currentThreshold = value;
UpdateThresholdImage("Threshold Adjuster");
}
/// <summary>
/// 更新二值化图像显示
/// </summary>
/// <param name="windowName">窗口名称</param>
private void UpdateThresholdImage(string windowName)
{
if (_grayImage == null) return;
using var thresholdImage = new Mat();
Cv2.Threshold(_grayImage, thresholdImage, _currentThreshold, 255, ThresholdTypes.Binary);
Cv2.ImShow(windowName, thresholdImage);
}
}

View File

@@ -7,17 +7,17 @@ namespace BetterGenshinImpact.Test.Dataset;
public class AvatarClassifyGen
{
// 基础图像文件夹
private const string BaseDir = @"E:\HuiTask\更好的原神\数据源\Snap.Static\AvatarIcon";
private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\源数据\AvatarIcon";
// 产出文件夹
private const string OutputDir = @"E:\HuiAi\YOLOv8\3.avatar-side";
private const string OutputDir = @"E:\HuiTask\更好的原神\侧面头像\dateset";
// 背景图像文件夹
private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\数据源\background";
private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\侧面头像\background";
private static readonly Random Rd = new Random();
public static readonly List<string> ImgNames = ["UI_AvatarIcon_Side_BennettCostumeSummer.png","UI_AvatarIcon_Side_YelanCostumeSummer.png", "UI_AvatarIcon_Side_Ineffa.png"];
public static readonly List<string> ImgNames = ["UI_AvatarIcon_Side_Aino.png","UI_AvatarIcon_Side_Flins.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Lauma.png"];
public static void GenAll()
{

View File

@@ -7,13 +7,13 @@ namespace BetterGenshinImpact.Test.Dataset;
public class AvatarClassifyTransparentGen
{
// 基础图像文件夹
private const string BaseDir = @"E:\HuiTask\更好的原神\数据源\Snap.Static\AvatarIcon";
private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\源数据\AvatarIcon";
// 产出文件夹
private const string OutputDir = @"E:\HuiAi\YOLOv8\3.avatar-side";
private const string OutputDir = @"E:\HuiTask\更好的原神\侧面头像\dateset";
// 背景图像文件夹
private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\数据源\background";
private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\侧面头像\background";
private static readonly Random Rd = new Random();

View File

@@ -22,7 +22,7 @@ public class LargeSiftExtractor
public static void Gen1024()
{
var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8";
var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0";
var mainMap2048BlockMat = new Mat($@"{rootPath}\map_2048.png", ImreadModes.Color);
// 缩小 2048/1024 = 2
var targetFilePath = $@"{rootPath}\1024_map.png";
@@ -38,16 +38,21 @@ public class LargeSiftExtractor
{
Environment.SetEnvironmentVariable("OPENCV_IO_MAX_IMAGE_PIXELS", Math.Pow(2, 40).ToString("F0"));
var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8";
var mainMap2048BlockMat = new Mat($@"{rootPath}\map_2048.png", ImreadModes.Color);
var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0";
// 缩小 2048/256 = 8
var targetFilePath = $@"{rootPath}\Teyvat_0_256.png";
// opencv 缩小
var mainMap256BlockMat =
mainMap2048BlockMat.Resize(new Size(mainMap2048BlockMat.Width / 8, mainMap2048BlockMat.Height / 8));
// 转化为灰度图
mainMap256BlockMat = mainMap256BlockMat.CvtColor(ColorConversionCodes.BGR2GRAY);
mainMap256BlockMat.SaveImage(targetFilePath);
if (!File.Exists(targetFilePath))
{
var mainMap2048BlockMat = new Mat($@"{rootPath}\map_2048.png", ImreadModes.Color);
// opencv 缩小
var mainMap256BlockMat =
mainMap2048BlockMat.Resize(new Size(mainMap2048BlockMat.Width / 8, mainMap2048BlockMat.Height / 8));
// 转化为灰度图
mainMap256BlockMat = mainMap256BlockMat.CvtColor(ColorConversionCodes.BGR2GRAY);
mainMap256BlockMat.SaveImage(targetFilePath);
}
FeatureMatcher featureMatcher = new(new Mat(targetFilePath, ImreadModes.Grayscale),
new FeatureStorage("Teyvat_0_256", rootPath));
@@ -58,8 +63,8 @@ public class LargeSiftExtractor
{
Environment.SetEnvironmentVariable("OPENCV_IO_MAX_IMAGE_PIXELS", Math.Pow(2, 40).ToString("F0"));
var extractor = new LargeSiftExtractor();
extractor.ExtractAndSaveSift(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_2048.png",
@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\");
extractor.ExtractAndSaveSift(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0\map_2048.png",
@"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0\");
}
public void ExtractAndSaveSift(string imagePath, string outputPath)

View File

@@ -27,18 +27,21 @@ public class MapPuzzle
public static void PutAll()
{
var folder = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0";
Directory.CreateDirectory(folder);
// 保存2048大图
var img2048 = Put(2048);
Cv2.ImWrite(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_2048.png", img2048);
Cv2.ImWrite(@$"{folder}\map_2048.png", img2048);
// 保存1024
var img1024 = Put(1024);
Cv2.ImWrite(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_1024.png", img1024);
Cv2.ImWrite(@$"{folder}\map_1024.png", img1024);
// 保存256
var grayImage = new Mat();
Cv2.CvtColor(img2048.Resize(new Size(img2048.Width / 8, img2048.Height / 8), 0, 0, InterpolationFlags.Cubic), grayImage, ColorConversionCodes.BGR2GRAY);
Cv2.ImWrite(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_256.png", grayImage);
Cv2.ImWrite(@$"{folder}\Teyvat_0_256.png", grayImage);
}

View File

@@ -9,16 +9,26 @@ namespace BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests
{
private readonly ConcurrentDictionary<string, PaddleOcrService> _paddleOcrServices = new();
public PaddleOcrService Get(string cultureInfoName = "zh-Hans")
public PaddleOcrService Get(string cultureInfoName = "zh-Hans", string version = "V5")
{
return _paddleOcrServices.GetOrAdd(cultureInfoName, name =>
{
lock (_paddleOcrServices)
{
return new PaddleOcrService(
new BgiOnnxFactory(new FakeLogger<BgiOnnxFactory>()),
PaddleOcrService.PaddleOcrModelType.FromCultureInfo(new CultureInfo(name)) ??
PaddleOcrService.PaddleOcrModelType.V5);
if (version == "V5")
{
return new PaddleOcrService(new BgiOnnxFactory(new FakeLogger<BgiOnnxFactory>()),
PaddleOcrService.PaddleOcrModelType.FromCultureInfo(new CultureInfo(name)) ?? PaddleOcrService.PaddleOcrModelType.V5);
}
else if (version == "V4")
{
return new PaddleOcrService(new BgiOnnxFactory(new FakeLogger<BgiOnnxFactory>()),
PaddleOcrService.PaddleOcrModelType.FromCultureInfoV4(new CultureInfo(name)) ?? PaddleOcrService.PaddleOcrModelType.V4);
}
else
{
throw new NotSupportedException();
}
}
});
}

View File

@@ -0,0 +1,45 @@
using BetterGenshinImpact.GameTask.AutoDomain.Model;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.Model.Area.Converter;
using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoDomainTests
{
[Collection("Init Collection")]
public class ResinStatusTests
{
private readonly PaddleFixture paddle;
public ResinStatusTests(PaddleFixture paddle)
{
this.paddle = paddle;
}
[Theory]
[InlineData(@"AutoDomain\SelectRevitalization.png", 21, 0, 2, 1)]
[InlineData(@"AutoDomain\SelectRevitalizationOcrV4.png", 11, 0, 1, 149, "V4")]
/// <summary>
/// 测试识别四种树脂数量,数量应正确
/// </summary>
public void RecogniseFromRegion_ResinStatusShouldBeRight(string screenshot1080p, int originalResinCount, int fragileResinCount, int condensedResinCount, int transientResinCount, string ocrVersion = "V5")
{
//
Mat mat = new Mat(@$"..\..\..\Assets\{screenshot1080p}");
var imageRegion = new ImageRegion(mat, 0, 0, converter: new ScaleConverter(1d));
FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1);
//
var result = ResinStatus.RecogniseFromRegion(imageRegion, systemInfo, this.paddle.Get(version: ocrVersion)); // todoSystem.Exception : 未找到原粹树脂图标
//
Assert.Equal(originalResinCount, result.OriginalResinCount);
Assert.Equal(condensedResinCount, result.CondensedResinCount);
}
}
}

View File

@@ -3,6 +3,7 @@ using BetterGenshinImpact.GameTask.AutoFishing;
using BetterGenshinImpact.GameTask.AutoFishing.Model;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.Model.Area.Converter;
using BetterGenshinImpact.Helpers.Extensions;
using Microsoft.Extensions.Time.Testing;
using OpenCvSharp;
using System;
@@ -15,10 +16,36 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
{
public partial class BehavioursTests
{
[Fact]
/// <summary>
/// 测试识别数量不足的鱼饵,由于图标变灰,识别应失败
/// </summary>
public void FindBaitTest_RecognitionShouldFail()
{
//
Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\202509141339218213_ChooseBait.png");
var imageRegion = new ImageRegion(mat, 0, 0, new DesktopRegion(new FakeMouseSimulator()), converter: new ScaleConverter(1d));
FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1);
var blackboard = new Blackboard();
//
ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes);
var result = sut.FindBait(imageRegion).OrderBy(r => r.Item1.X).ToArray();
//
Assert.Equal(3, result.Length);
Assert.Equal(BaitType.FruitPasteBait.GetDescription(), result[0].Item2);
Assert.Equal(BaitType.BerryBait.GetDescription(), result[1].Item2);
Assert.Null(result[2].Item2);
}
[Theory]
[InlineData(@"20250225101300361_ChooseBait_Succeeded.png", new string[] { "medaka", "butterflyfish", "butterflyfish", "pufferfish" })]
[InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka", "medaka" })] // todo 更新用例
[InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka" })] // 不稳定的测试用例,因未学习被照亮的场景
[InlineData(@"202503160917566615@900p.png", new string[] { "pufferfish" })]
[InlineData(@"202509141339218213_ChooseBait.png", new string[] { "axehead fish" })]
[InlineData(@"202509141339218213_ChooseBait.png", new string[] { "mauler shark", "crystal eye", "medaka", "medaka", "medaka" })]
/// <summary>
/// 测试各种选取鱼饵,结果为成功
/// </summary>
@@ -50,6 +77,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
[Theory]
[InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "koi" })]
[InlineData(@"202509141339218213_ChooseBait.png", new string[] { "mauler shark", "crystal eye" })]
/// <summary>
/// 测试各种选取鱼饵,结果为失败
/// </summary>

View File

@@ -14,7 +14,6 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
{
[Theory]
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FalseWormBait)]
[InlineData(@"20250226162217468_ThrowRod_Succeeded.png", BaitType.FruitPasteBait)]
/// <summary>
/// 测试各种抛竿,结果为成功
/// </summary>
@@ -43,6 +42,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
[Theory]
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.RedrotBait)]
[InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FakeFlyBait)]
[InlineData(@"20250226162217468_ThrowRod_Succeeded.png", BaitType.FruitPasteBait)]
/// <summary>
/// 测试各种抛竿未满足HutaoFisher判定结果为运行中
/// </summary>

View File

@@ -1,4 +1,4 @@
using BetterGenshinImpact.GameTask.Model;
using BetterGenshinImpact.GameTask.Model;
using BetterGenshinImpact.GameTask.Model.Area;
using OpenCvSharp;
using System;
@@ -9,7 +9,7 @@ using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
namespace BetterGenshinImpact.UnitTest.GameTaskTests
{
internal class FakeSystemInfo : ISystemInfo
{
@@ -29,7 +29,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
public RECT GameScreenSize { get; }
public double AssetScale { get; }
public double AssetScale { get; } = 1;
public double ZoomOutMax1080PRatio { get; }

View File

@@ -27,8 +27,8 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests
yield return new object[] { @"GetGridIcons\FoodGrid.png", 8, true, new[] { ("苹果", 0), ("日落果", 0), ("星蕈", 0), ("泡泡桔", 0), ("烛伞蘑菇", 0), ("宝石闪闪", 4), ("咚咚", 4), ("枫达", 2),
("雾凇秋分", 4), ("蒙德土豆饼", 3), /*("爪爪土豆饼", 3), ("北地苹果焖肉", 3), ("四方和平", 3), ("盛世太平", 3), ("三彩团子", 3), ("夏祭游鱼", 3)*/} };
// todo 爪爪土豆饼被吃掉了没进训练集。。。
yield return new object[] { @"GetGridIcons\FoodGrid_Attack.png", 8, false, new[] { ("果果软糖", 3), ("方块戏法", 3), ("「缥雨一滴」", 3), ("繁弦急管", 3), ("繁弦急管", 3), ("「簇火赞歌」", 3), ("轻策家常菜", 3), ("冒险家蛋堡", 3), ("冒险家蛋堡", 3), ("测绘员蛋堡", 3), ("串串三味", 3), ("连心面", 3), ("摩拉急速来", 3), ("双果清露", 3), ("双果清露", 3), ("双果清露", 3), ("四喜圆满", 3), ("祝圣交响乐", 3), ("满足沙拉", 2), ("满足沙拉", 2), ("至高的智慧(生活)", 2), ("摇·滚·鸡!", 2), ("岩港三鲜", 2), ("鲜鱼萝卜", 2), ("炸萝卜丸子", 2), ("炸萝卜丸子", 2), ("杏仁豆腐", 2), ("杏仁豆腐", 2), ("「美梦」", 2), ("凉拌薄荷", 2), ("炸肉排三明治", 2), ("唯一的真相", 2) } };
yield return new object[] { @"GetGridIcons\MaterialGrid_TreesAndBaits.png", 8, false, new[] { ("松木", 1), ("却砂木", 1), ("竹节", 1), ("垂香木", 1), ("杉木", 1), ("梦见木", 1), ("枫木", 1), ("孔雀木", 1), ("御伽木", 1), ("辉木", 1), ("业果木", 1), ("证悟木", 1), ("木", 1), ("黑铜号角", 1), ("悬铃木", 1), ("白梣木", 1), ("香柏木", 1), ("白栗栎木", 1), ("灰灰楼林木", 1), ("燃爆木", 1), ("布匹", 0), ("红色染料", 0), ("黄色染料", 0), ("蓝色染料", 0), ("果酿饵", 2), ("赤糜饵", 2), ("蠕虫假饵", 2), ("飞蝇假饵", 2), ("甘露饵", 2), ("酸桔饵", 2), ("维护机关频闪诱饵", 2), ("澄晶果粒饵", 2) } };
yield return new object[] { @"GetGridIcons\FoodGrid_Attack.png", 8, false, new[] { ("果果软糖", 3), ("方块戏法", 3), ("「缥雨一滴」", 3), ("繁弦急管", 3), ("繁弦急管", 3), ("「簇火赞歌」", 3), ("轻策家常菜", 3), ("冒险家蛋堡", 3), ("冒险家蛋堡", 3), ("测绘员蛋堡", 3), ("串串三味", 3), ("连心面", 3), ("摩拉急速来", 3), ("双果清露", 3), ("双果清露", 3), ("双果清露", 3), ("四喜圆满", 3), ("祝圣交响乐", 3), ("满足沙拉", 2), ("满足沙拉", 2), ("至高的智慧(生活)", 2), ("摇·滚·鸡!", 2), ("岩港三鲜", 2), ("鲜鱼萝卜", 2), ("炸萝卜丸子", 2), ("炸萝卜丸子", 2), ("杏仁豆腐", 2), ("杏仁豆腐", 2), ("「美梦」", 2), ("凉拌薄荷", 2), ("炸肉排三明治", 2), ("唯一的真相", 2) } };
yield return new object[] { @"GetGridIcons\MaterialGrid_TreesAndBaits.png", 8, false, new[] { ("松木", 1), ("却砂木", 1), ("竹节", 1), ("垂香木", 1), ("杉木", 1), ("梦见木", 1), ("枫木", 1), ("孔雀木", 1), ("御伽木", 1), ("辉木", 1), ("业果木", 1), ("证悟木", 1), ("刺葵木", 1), ("柽木", 1), ("悬铃木", 1), ("白梣木", 1), ("香柏木", 1), ("白栗栎木", 1), ("灰灰楼林木", 1), ("燃爆木", 1), ("布匹", 0), ("红色染料", 0), ("黄色染料", 0), ("蓝色染料", 0), ("果酿饵", 2), ("赤糜饵", 2), ("蠕虫假饵", 2), ("飞蝇假饵", 2), ("甘露饵", 2), ("酸桔饵", 2), ("维护机关频闪诱饵", 2), ("澄晶果粒饵", 2) } };
// string.Join(", ",result.Select(s=>$"(\"{s.Item1}\", {s.Item2})"))
}