Merge branch 'main' into d-v3
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 6.4 KiB |
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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>
|
||||
/// 是否出战状态
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -21,5 +21,9 @@ public enum BaitType
|
||||
[Description("澄晶果粒饵")]
|
||||
SpinelgrainBait,
|
||||
[Description("温火饵")]
|
||||
EmberglowBait
|
||||
EmberglowBait,
|
||||
[Description("槲梭饵")]
|
||||
BerryBait,
|
||||
[Description("清白饵")]
|
||||
RefreshingLakkaBait
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": "其他武器",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
79
BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "月矩力试验设计局"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
16
BetterGenshinImpact/GameTask/AutoTrackPath/TpTaskParam.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace BetterGenshinImpact.GameTask.Common.BgiVision
|
||||
{
|
||||
internal abstract class BvResxHelper; // Bv是静态类,无法作为IStringLocalizer<T>泛型类,用这个类代替
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
// 可以用来合成的树脂数量
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 取消动画函数
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
// {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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", "火元素采集" },
|
||||
};
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 195 KiB |
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
54
BetterGenshinImpact/User/AutoFight/万能战斗策略(萌新推荐).txt
Normal 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)
|
||||
@@ -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="移除" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
"暂停当前脚本/独立任务",
|
||||
|
||||
@@ -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("一条龙和配置组任务结束");
|
||||
|
||||
// 执行完成后操作
|
||||
|
||||
@@ -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)
|
||||
|
||||
81
Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)); // todo:System.Exception : 未找到原粹树脂图标
|
||||
|
||||
//
|
||||
Assert.Equal(originalResinCount, result.OriginalResinCount);
|
||||
Assert.Equal(condensedResinCount, result.CondensedResinCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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})"))
|
||||
}
|
||||
|
||||
|
||||