mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-21 09:45:48 +08:00
Merge branch 'main' into d-v3
# Conflicts: # BetterGenshinImpact/BetterGenshinImpact.csproj
This commit is contained in:
@@ -88,7 +88,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.RichTextBoxEx.Wpf" Version="1.1.0.1" />
|
||||
<!-- <PackageReference Include="supabase-csharp" Version="0.16.2" />-->
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="9.0.4" />
|
||||
<PackageReference Include="TorchSharp" Version="0.105.0" />
|
||||
@@ -96,10 +96,10 @@
|
||||
<PackageReference Include="Vanara.PInvoke.SHCore" Version="4.1.3" />
|
||||
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.3" />
|
||||
<PackageReference Include="XamlAnimatedGif" Version="2.3.1" />
|
||||
<PackageReference Include="WPF-UI" Version="4.2.0" />
|
||||
<PackageReference Include="WPF-UI.DependencyInjection" Version="4.2.0" />
|
||||
<PackageReference Include="WPF-UI.Tray" Version="4.2.0" />
|
||||
<PackageReference Include="WPF-UI.Violeta" Version="4.2.0.10" />
|
||||
<PackageReference Include="WPF-UI" Version="4.3.0" />
|
||||
<PackageReference Include="WPF-UI.DependencyInjection" Version="4.3.0" />
|
||||
<PackageReference Include="WPF-UI.Tray" Version="4.3.0" />
|
||||
<PackageReference Include="WPF-UI.Violeta" Version="4.3.0.0" />
|
||||
<PackageReference Include="gong-wpf-dragdrop" Version="3.2.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageReference Include="YoloSharp" Version="6.0.3" />
|
||||
|
||||
@@ -21,10 +21,12 @@ public class BvLocator
|
||||
{
|
||||
private static readonly ILogger Logger = App.GetLogger<BvLocator>();
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private int? _timeout;
|
||||
private int? _retryInterval;
|
||||
|
||||
public RecognitionObject RecognitionObject { get; }
|
||||
|
||||
public Action<List<Region>>? RetryAction { get; set; }
|
||||
public Func<List<Region>, Task>? RetryAction { get; set; }
|
||||
|
||||
public static int DefaultTimeout { get; set; } = 10000;
|
||||
|
||||
@@ -98,17 +100,21 @@ public class BvLocator
|
||||
|
||||
public async Task<List<Region>> WaitFor(int? timeout = null)
|
||||
{
|
||||
var actualTimeout = timeout ?? DefaultTimeout;
|
||||
var retryCount = actualTimeout / DefaultRetryInterval;
|
||||
var actualTimeout = timeout ?? _timeout ?? DefaultTimeout;
|
||||
var actualRetryInterval = _retryInterval ?? DefaultRetryInterval;
|
||||
var retryCount = Math.Max(1, actualTimeout / actualRetryInterval);
|
||||
|
||||
List<Region> results = [];
|
||||
var retryRes = await NewRetry.WaitForAction(() =>
|
||||
var retryRes = await NewRetry.WaitForAction(async () =>
|
||||
{
|
||||
results = FindAll();
|
||||
var b = results.Count > 0;
|
||||
RetryAction?.Invoke(results);
|
||||
if (!b && RetryAction != null)
|
||||
{
|
||||
await RetryAction(results);
|
||||
}
|
||||
return b;
|
||||
}, _cancellationToken, retryCount, DefaultRetryInterval);
|
||||
}, _cancellationToken, retryCount, actualRetryInterval);
|
||||
|
||||
if (retryRes)
|
||||
{
|
||||
@@ -150,20 +156,21 @@ public class BvLocator
|
||||
|
||||
public async Task WaitForDisappear(int? timeout = null)
|
||||
{
|
||||
var actualTimeout = timeout ?? DefaultTimeout;
|
||||
var retryCount = actualTimeout / DefaultRetryInterval;
|
||||
var actualTimeout = timeout ?? _timeout ?? DefaultTimeout;
|
||||
var actualRetryInterval = _retryInterval ?? DefaultRetryInterval;
|
||||
var retryCount = Math.Max(1, actualTimeout / actualRetryInterval);
|
||||
|
||||
var retryRes = await NewRetry.WaitForAction(() =>
|
||||
var retryRes = await NewRetry.WaitForAction(async () =>
|
||||
{
|
||||
var results = FindAll();
|
||||
var b = results.Count == 0;
|
||||
if (!b)
|
||||
if (!b && RetryAction != null)
|
||||
{
|
||||
RetryAction?.Invoke(results);
|
||||
await RetryAction(results);
|
||||
}
|
||||
|
||||
return b;
|
||||
}, _cancellationToken, retryCount, DefaultRetryInterval);
|
||||
}, _cancellationToken, retryCount, actualRetryInterval);
|
||||
|
||||
if (!retryRes)
|
||||
{
|
||||
@@ -205,19 +212,77 @@ public class BvLocator
|
||||
|
||||
public BvLocator WithRetryAction(Action<List<Region>>? action)
|
||||
{
|
||||
RetryAction = action;
|
||||
if (action == null)
|
||||
{
|
||||
RetryAction = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
RetryAction = (results) =>
|
||||
{
|
||||
action(results);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置超时时间(毫秒)
|
||||
/// </summary>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns></returns>
|
||||
public BvLocator WithTimeout(int timeout)
|
||||
{
|
||||
if (timeout <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(timeout), "timeout 必须大于 0");
|
||||
}
|
||||
_timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置重试间隔(毫秒)
|
||||
/// </summary>
|
||||
/// <param name="retryInterval">重试间隔(毫秒)</param>
|
||||
/// <returns></returns>
|
||||
public BvLocator WithRetryInterval(int retryInterval)
|
||||
{
|
||||
if (retryInterval <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(retryInterval), "retryInterval 必须大于 0");
|
||||
}
|
||||
_retryInterval = retryInterval;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 JavaScript 提供的动态参数重载
|
||||
/// 解决 ClearScript 无法将 JS 函数隐式转换为 Action 委托的问题
|
||||
/// 支持同步和异步 JS 函数
|
||||
/// </summary>
|
||||
/// <param name="action">JS 回调函数</param>
|
||||
/// <returns></returns>
|
||||
public BvLocator WithRetryAction(dynamic action)
|
||||
{
|
||||
RetryAction = (results) => action(results);
|
||||
if (action == null)
|
||||
{
|
||||
RetryAction = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
RetryAction = async (results) =>
|
||||
{
|
||||
var taskResult = action(results);
|
||||
// 如果 JS 返回的是 Promise/Task,等待其完成
|
||||
if (taskResult is Task task)
|
||||
{
|
||||
await task;
|
||||
}
|
||||
};
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ public class AutoPathingScript
|
||||
|
||||
/// <summary>
|
||||
/// 读取 AutoPathing 目录下指定文件夹的内容(非递归方式)
|
||||
/// 目录不存在时返回空数组,不会自动创建目录
|
||||
/// </summary>
|
||||
/// <param name="subPath">相对于 User\AutoPathing 的子目录路径,默认为相对根目录</param>
|
||||
/// <returns>文件夹内所有文件和文件夹的相对路径数组,出错时返回空数组</returns>
|
||||
|
||||
@@ -145,6 +145,35 @@ public class HtmlMask : IDisposable
|
||||
/// </summary>
|
||||
public bool Exists(string id) => HtmlMaskWindow.Exists(id);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口的点击穿透模式
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
/// <param name="enabled">true=点击穿透,false=可交互</param>
|
||||
public void SetClickThrough(string windowId, bool enabled)
|
||||
{
|
||||
HtmlMaskWindow.SetClickThrough(windowId, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口的点击穿透状态
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
/// <returns>true=点击穿透,false=可交互</returns>
|
||||
public bool GetClickThrough(string windowId)
|
||||
{
|
||||
return HtmlMaskWindow.GetClickThrough(windowId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换窗口的点击穿透模式
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
public void ToggleClickThrough(string windowId)
|
||||
{
|
||||
HtmlMaskWindow.ToggleClickThrough(windowId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 消息通信
|
||||
|
||||
@@ -15,6 +15,7 @@ public class LimitedFile(string rootPath)
|
||||
{
|
||||
/// <summary>
|
||||
/// 读取指定文件夹内所有文件和文件夹的路径(非递归方式)。
|
||||
/// 目录不存在时返回空数组
|
||||
/// </summary>
|
||||
/// <param name="folderPath">文件夹路径(相对于根目录)</param>
|
||||
/// <returns>文件夹内所有文件和文件夹的路径数组</returns>
|
||||
@@ -23,19 +24,19 @@ public class LimitedFile(string rootPath)
|
||||
try
|
||||
{
|
||||
// 对传入的文件夹路径进行标准化
|
||||
string normalizedFolderPath = NormalizePath(folderPath);
|
||||
string fullPath = NormalizePath(folderPath);
|
||||
|
||||
// 确保目录存在
|
||||
if (!Directory.Exists(normalizedFolderPath))
|
||||
if (!Directory.Exists(fullPath))
|
||||
{
|
||||
Directory.CreateDirectory(normalizedFolderPath);
|
||||
TaskControl.Logger.LogError("ReadPathSync 目录不存在: {Path}", fullPath);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
// 获取指定文件夹下的所有文件(非递归)
|
||||
string[] files = Directory.GetFiles(normalizedFolderPath, "*", SearchOption.TopDirectoryOnly);
|
||||
string[] files = Directory.GetFiles(fullPath, "*", SearchOption.TopDirectoryOnly);
|
||||
|
||||
// 获取指定文件夹下的所有子文件夹(非递归)
|
||||
string[] directories = Directory.GetDirectories(normalizedFolderPath, "*", SearchOption.TopDirectoryOnly);
|
||||
string[] directories = Directory.GetDirectories(fullPath, "*", SearchOption.TopDirectoryOnly);
|
||||
|
||||
// 合并文件和文件夹路径
|
||||
string[] combined = files.Concat(directories).ToArray();
|
||||
@@ -45,12 +46,33 @@ public class LimitedFile(string rootPath)
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录异常并返回空数组
|
||||
TaskControl.Logger.LogError("ReadPathSync 异常: {Message}", ex.Message);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定路径的目录,如果已存在则跳过
|
||||
/// </summary>
|
||||
/// <param name="folderPath">文件夹路径(相对于根目录)</param>
|
||||
/// <returns>是否创建成功或目录已存在</returns>
|
||||
public bool CreateDirectory(string folderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 对传入的文件夹路径进行标准化
|
||||
string fullPath = NormalizePath(folderPath);
|
||||
// 如果目录不存在,自动创建
|
||||
if (!Directory.Exists(fullPath)){Directory.CreateDirectory(fullPath);}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskControl.Logger.LogError("CreateDir 异常: {Message}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定路径是否为文件夹。
|
||||
/// </summary>
|
||||
|
||||
43
BetterGenshinImpact/Core/Script/Dependence/StrategyFile.cs
Normal file
43
BetterGenshinImpact/Core/Script/Dependence/StrategyFile.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
|
||||
namespace BetterGenshinImpact.Core.Script.Dependence;
|
||||
|
||||
/// <summary>
|
||||
/// 战斗策略文件访问类
|
||||
/// 提供JS脚本环境访问 User\AutoFight 目录下战斗策略文件的方法
|
||||
/// </summary>
|
||||
public class StrategyFile
|
||||
{
|
||||
private readonly LimitedFile _strategyFile = new(Global.Absolute(@"User\AutoFight"));
|
||||
|
||||
/// <summary>
|
||||
/// 判断 User\AutoFight 目录下的路径是否为文件夹
|
||||
/// </summary>
|
||||
/// <param name="subPath">相对于 User\AutoFight 的路径</param>
|
||||
/// <returns>是文件夹返回 true,否则返回 false</returns>
|
||||
public bool IsFolder(string subPath) => _strategyFile.IsFolder(subPath);
|
||||
|
||||
/// <summary>
|
||||
/// 判断 User\AutoFight 目录下的路径是否为文件
|
||||
/// </summary>
|
||||
/// <param name="subPath">相对于 User\AutoFight 的路径</param>
|
||||
/// <returns>是文件返回 true,否则返回 false</returns>
|
||||
public bool IsFile(string subPath) => _strategyFile.IsFile(subPath);
|
||||
|
||||
/// <summary>
|
||||
/// 判断 User\AutoFight 目录下的路径是否存在
|
||||
/// </summary>
|
||||
/// <param name="subPath">相对于 User\AutoFight 的路径</param>
|
||||
/// <returns>存在返回 true,否则返回 false</returns>
|
||||
public bool IsExists(string subPath) => _strategyFile.IsExists(subPath);
|
||||
|
||||
/// <summary>
|
||||
/// 读取 User\AutoFight 目录下指定文件夹的内容(非递归方式)
|
||||
/// 目录不存在时返回空数组,不会自动创建目录
|
||||
/// </summary>
|
||||
/// <param name="subPath">相对于 User\AutoFight 的子目录路径,默认为根目录</param>
|
||||
/// <returns>文件夹内所有文件和文件夹的相对路径数组,出错时返回空数组</returns>
|
||||
public string[] ReadPathSync(string subPath = "./") => _strategyFile.ReadPathSync(subPath);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BetterGenshinImpact.Core.Script.Dependence;
|
||||
using BetterGenshinImpact.Core.Script.Dependence.Model;
|
||||
@@ -77,6 +77,7 @@ public class EngineExtend
|
||||
engine.AddHostType("AutoFightParam", typeof(AutoFightParam));
|
||||
engine.AddHostType("AutoLeyLineOutcropParam", typeof(AutoLeyLineOutcropParam));
|
||||
engine.AddHostType("AutoStygianOnslaughtParam", typeof(AutoStygianOnslaughtParam));
|
||||
engine.AddHostObject("strategyFile", new StrategyFile());
|
||||
//鼠标回调
|
||||
engine.AddHostType("KeyMouseHook", typeof(KeyMouseHook));
|
||||
// 添加C#的类型
|
||||
|
||||
@@ -680,15 +680,20 @@ public class AutoFightTask : ISoloTask
|
||||
await Delay(200, ct);
|
||||
if (picker.TrySwitch(10))
|
||||
{
|
||||
// 等待元素战技 CD 就绪
|
||||
await picker.WaitSkillCd(ct);
|
||||
picker.UseSkill(true);
|
||||
await Delay(50, ct);
|
||||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||||
await Delay(100, ct);
|
||||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||||
await Delay(100, ct);
|
||||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||||
|
||||
// 调用统一的辅助方法,模拟万叶长按 E 的输入序列:
|
||||
// 包含释放鼠标左键前摇防卡键 -> E 键 KeyDown -> 延时 800ms -> E 键 KeyUp -> 延时 50ms
|
||||
await SimulateHoldElementalSkillAsync(800, ct);
|
||||
|
||||
// 调用统一的辅助方法,模拟 6 次鼠标左键连续点击:
|
||||
// 配合万叶长 E 的滞空特性执行下落攻击,内部包含 try/finally 以保证取消任务时安全释放左键
|
||||
await SimulateMouseLeftClickLoopAsync(6, ct);
|
||||
|
||||
// 等待下落攻击和聚怪拾取动作彻底结束
|
||||
await Delay(1500, ct);
|
||||
// 截图并更新技能最新冷却时间
|
||||
picker.AfterUseSkill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
using BetterGenshinImpact.Core.Simulator;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Model;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Script;
|
||||
using BetterGenshinImpact.Model;
|
||||
@@ -74,6 +75,7 @@ public class OneKeyFightTask : Singleton<OneKeyFightTask>
|
||||
else
|
||||
{
|
||||
_cts.Cancel();
|
||||
Simulation.ReleaseAllKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +91,7 @@ public class OneKeyFightTask : Singleton<OneKeyFightTask>
|
||||
if (IsHoldOnMode())
|
||||
{
|
||||
_cts?.Cancel();
|
||||
Simulation.ReleaseAllKey();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,9 +182,9 @@ public class OneKeyFightTask : Singleton<OneKeyFightTask>
|
||||
// 通用化战斗策略
|
||||
foreach (var command in combatCommands)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
if (command.ActivatingRound != null && command.ActivatingRound.Count > 0 && !command.ActivatingRound.Contains(round))
|
||||
{
|
||||
// 跳过强制首轮指令
|
||||
continue;
|
||||
}
|
||||
command.Execute(activeAvatar);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterGenshinImpact.Core.Recognition;
|
||||
using BetterGenshinImpact.Core.Recognition;
|
||||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
@@ -11,6 +11,7 @@ using BetterGenshinImpact.GameTask.AutoPathing.Handler;
|
||||
using BetterGenshinImpact.GameTask.AutoPathing.Model;
|
||||
using BetterGenshinImpact.GameTask.AutoTrackPath;
|
||||
using BetterGenshinImpact.GameTask.AutoFight;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoPick.Assets;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Model;
|
||||
using BetterGenshinImpact.GameTask.AutoFight.Script;
|
||||
@@ -1081,15 +1082,34 @@ public class AutoLeyLineOutcropTask : ISoloTask
|
||||
|
||||
_logger.LogInformation("战后聚集拾取:万叶已切换,等待元素战技CD");
|
||||
await kazuha.WaitSkillCd(_ct);
|
||||
kazuha.UseSkill(true);
|
||||
await Delay(50, _ct);
|
||||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||||
await Delay(100, _ct);
|
||||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||||
await Delay(100, _ct);
|
||||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||||
await SimulateHoldElementalSkillAsync(1000, _ct);
|
||||
await Delay(200, _ct);
|
||||
|
||||
// 获取游戏画面,进行 OCR 及视觉状态双重验证,以确认长E技能是否真正释放成功
|
||||
using (var region = CaptureToRectArea())
|
||||
{
|
||||
// 裁剪技能 CD 区域并做 HSV 颜色过滤,分离出白色的 CD 数字
|
||||
using var eRa = region.DeriveCrop(AutoFightAssets.Instance.ECooldownRect);
|
||||
using var eRaWhite = OpenCvCommonHelper.InRangeHsv(eRa.SrcMat, new Scalar(0, 0, 235), new Scalar(0, 25, 255));
|
||||
var text = OcrFactory.Paddle.OcrWithoutDetector(eRaWhite);
|
||||
|
||||
// 如果成功读到了大于 0 的 CD 数值,说明技能已释放
|
||||
var hasOcrCd = double.TryParse(text, out var ocrCd) && ocrCd > 0;
|
||||
// 视觉上判断当前技能图标是否高亮就绪,如果不亮(false)也说明技能释放进入了冷却
|
||||
var isVisualReady = Bv.IsSkillReady(region, kazuha.Index, false);
|
||||
|
||||
// 当 OCR 没读出 CD(可能网络卡顿技能没放出来),并且视觉上技能图标依然亮着就绪时,判断为释放失败
|
||||
if (!hasOcrCd && isVisualReady)
|
||||
{
|
||||
_logger.LogWarning("战后聚集拾取:万叶长E释放确认失败(OCR:{Text}),跳过后续拾取动作", text);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新技能冷却记录,防止干扰后续冷却判断
|
||||
kazuha.AfterUseSkill(region);
|
||||
}
|
||||
await SimulateMouseLeftClickLoopAsync(6, _ct);
|
||||
await Delay(1500, _ct);
|
||||
kazuha.AfterUseSkill();
|
||||
_logger.LogInformation("战后聚集拾取:万叶长E动作完成,等待拾取动作结束");
|
||||
await Delay(KazuhaPickupPostSkillWaitMs, _ct);
|
||||
_logger.LogInformation("战后聚集拾取:万叶长E聚集动作执行完成");
|
||||
|
||||
@@ -25,7 +25,7 @@ public class PickUpCollectHandler : IActionHandler
|
||||
/// </summary>
|
||||
public static readonly string[] PickUpActions =
|
||||
[
|
||||
"枫原万叶-长E attack(0.08),keydown(E),wait(1),keyup(E),attack(0.5)",
|
||||
"枫原万叶-长E attack(0.08),keydown(E),wait(0.8),keyup(E),attack(0.5)",
|
||||
"枫原万叶-短E attack(0.08),keydown(E),wait(0.47),keyup(E),attack(0.5)",
|
||||
"琴-短E wait(0.1),keydown(E),wait(0.4),moveby(1000,0),wait(0.2),moveby(1000,0),wait(0.2),moveby(1000,0),wait(0.2),moveby(1000,-3500),wait(1.8),keyup(E),wait(0.3),click(middle)",
|
||||
"琴-长E wait(0.1),click(middle),keydown(E),click(middle),wait(0.4),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
|
||||
|
||||
@@ -67,12 +67,14 @@ public class LinneaMiningTask
|
||||
|
||||
private readonly int _scanRounds;
|
||||
private readonly int _mineCount;
|
||||
private readonly bool _preferRight;
|
||||
private int _debugIndex;
|
||||
|
||||
public LinneaMiningTask(int scanRounds = DefaultScanRounds, int mineCount = DefaultMineCount)
|
||||
{
|
||||
_scanRounds = scanRounds;
|
||||
_mineCount = mineCount;
|
||||
_preferRight = scanRounds > 1;
|
||||
_predictor = App.ServiceProvider.GetRequiredService<BgiOnnxFactory>()
|
||||
.CreateYoloPredictor(BgiOnnxModel.BgiMine);
|
||||
ClusterDistanceThreshold = BaseClusterDistance * _widthScale;
|
||||
@@ -359,7 +361,7 @@ public class LinneaMiningTask
|
||||
}
|
||||
}
|
||||
|
||||
clusters.Add(new MineralCluster(rect, AreaRatioThreshold));
|
||||
clusters.Add(new MineralCluster(rect, AreaRatioThreshold, _preferRight));
|
||||
}
|
||||
|
||||
return clusters;
|
||||
@@ -379,14 +381,16 @@ public class MineralCluster
|
||||
public double TargetWidth { get; private set; }
|
||||
public double TargetHeight { get; private set; }
|
||||
|
||||
public MineralCluster(Rect firstRect, double areaRatioThreshold = 5)
|
||||
public MineralCluster(Rect firstRect, double areaRatioThreshold = 5, bool preferRight = true)
|
||||
{
|
||||
AreaRatioThreshold = areaRatioThreshold;
|
||||
PreferRight = preferRight;
|
||||
Rects.Add(firstRect);
|
||||
RecalculateCenter();
|
||||
}
|
||||
|
||||
private readonly double AreaRatioThreshold;
|
||||
private readonly bool PreferRight;
|
||||
|
||||
public bool TryAddRect(Rect rect)
|
||||
{
|
||||
@@ -403,19 +407,28 @@ public class MineralCluster
|
||||
CenterX = Rects.Average(r => r.X + r.Width / 2.0);
|
||||
CenterY = Rects.Average(r => r.Y + r.Height / 2.0);
|
||||
|
||||
// 按距离质心排序,取最近2个中靠右的
|
||||
var candidates = Rects
|
||||
var sorted = Rects
|
||||
.Select(r => (cx: r.X + r.Width / 2.0, cy: r.Y + r.Height / 2.0,
|
||||
dist: Math.Pow(r.X + r.Width / 2.0 - CenterX, 2) + Math.Pow(r.Y + r.Height / 2.0 - CenterY, 2),
|
||||
w: (double)r.Width, h: (double)r.Height))
|
||||
.OrderBy(t => t.dist)
|
||||
.Take(2)
|
||||
.OrderByDescending(t => t.cx)
|
||||
.First();
|
||||
.ToList();
|
||||
|
||||
TargetX = candidates.cx;
|
||||
TargetY = candidates.cy;
|
||||
TargetWidth = candidates.w;
|
||||
TargetHeight = candidates.h;
|
||||
(double cx, double cy, double dist, double w, double h) target;
|
||||
if (PreferRight && sorted.Count >= 2)
|
||||
{
|
||||
// 多轮旋转时:取最近2个中靠右的,对冲左转偏移
|
||||
target = sorted.Take(2).OrderByDescending(t => t.cx).First();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 单轮时:直接选最近的
|
||||
target = sorted.First();
|
||||
}
|
||||
|
||||
TargetX = target.cx;
|
||||
TargetY = target.cy;
|
||||
TargetWidth = target.w;
|
||||
TargetHeight = target.h;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
|
||||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -89,6 +89,28 @@ public static class NewRetry
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重试执行异步 action,直到返回 true 或达到最大重试次数。
|
||||
/// </summary>
|
||||
/// <param name="action">判断条件(异步)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <param name="retryTimes">最大重试次数</param>
|
||||
/// <param name="delayMs">每次重试间隔(毫秒)</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> WaitForAction(Func<Task<bool>> action, CancellationToken ct, int retryTimes = 10, int delayMs = 1000)
|
||||
{
|
||||
for (var i = 0; i < retryTimes; i++)
|
||||
{
|
||||
await TaskControl.Delay(delayMs, ct);
|
||||
if (await action())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重试直到某个元素出现,可执行键盘或鼠标操作。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -9,6 +9,7 @@ using Fischless.GameCapture;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCvSharp;
|
||||
using Vanara.PInvoke;
|
||||
using BetterGenshinImpact.Core.Simulator.Extensions;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.Common;
|
||||
|
||||
@@ -184,6 +185,90 @@ public class TaskControl
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟长按指定动作。使用 try/finally 块确保在任务被取消或发生异常时,按键也能安全释放,防止卡键。
|
||||
/// </summary>
|
||||
/// <param name="action">需要模拟的游戏动作(如元素战技、普通攻击等)</param>
|
||||
/// <param name="holdMs">长按持续的时间(毫秒)</param>
|
||||
/// <param name="ct">用于监控任务取消的取消令牌</param>
|
||||
public static async Task SimulateHoldActionAsync(GIActions action, int holdMs, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
Simulation.SendInput.SimulateAction(action, KeyType.KeyDown);
|
||||
await Delay(holdMs, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Simulation.SendInput.SimulateAction(action, KeyType.KeyUp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟长按元素战技(如万叶长E)。包含释放前摇、长按以及释放后的缓冲延时。
|
||||
/// </summary>
|
||||
/// <param name="holdMs">元素战技按住的时间(毫秒)</param>
|
||||
/// <param name="ct">用于监控任务取消的取消令牌</param>
|
||||
/// <param name="releaseLeftMouseBefore">是否在按下元素战技前先松开鼠标左键,避免输入冲突,默认 true</param>
|
||||
/// <param name="releaseLeftMouseDelayMs">松开鼠标左键后的缓冲时间(毫秒),默认 10ms</param>
|
||||
/// <param name="postKeyUpDelayMs">元素战技释放后的缓冲时间(毫秒),默认 50ms</param>
|
||||
public static async Task SimulateHoldElementalSkillAsync(
|
||||
int holdMs,
|
||||
CancellationToken ct,
|
||||
bool releaseLeftMouseBefore = true,
|
||||
int releaseLeftMouseDelayMs = 10,
|
||||
int postKeyUpDelayMs = 50)
|
||||
{
|
||||
if (releaseLeftMouseBefore)
|
||||
{
|
||||
Simulation.SendInput.Mouse.LeftButtonUp();
|
||||
await Delay(releaseLeftMouseDelayMs, ct);
|
||||
}
|
||||
|
||||
await SimulateHoldActionAsync(GIActions.ElementalSkill, holdMs, ct);
|
||||
await Delay(postKeyUpDelayMs, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟鼠标左键连续点击循环(如万叶长E后的下落攻击)。双层 try/finally 设计以确保无论在循环的哪个阶段发生取消或异常,鼠标左键都会被强制释放。
|
||||
/// </summary>
|
||||
/// <param name="repeatCount">需要循环点击的次数</param>
|
||||
/// <param name="ct">用于监控任务取消的取消令牌</param>
|
||||
/// <param name="preUpDelayMs">每次点击前,预先抬起左键后的缓冲延时(毫秒),默认 10ms</param>
|
||||
/// <param name="downHoldMs">鼠标左键按下的保持时间(毫秒),默认 35ms</param>
|
||||
/// <param name="postUpDelayMs">每次点击完成后的等待时间(毫秒),默认 50ms</param>
|
||||
public static async Task SimulateMouseLeftClickLoopAsync(
|
||||
int repeatCount,
|
||||
CancellationToken ct,
|
||||
int preUpDelayMs = 10,
|
||||
int downHoldMs = 35,
|
||||
int postUpDelayMs = 50)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < repeatCount; i++)
|
||||
{
|
||||
Simulation.SendInput.Mouse.LeftButtonUp();
|
||||
await Delay(preUpDelayMs, ct);
|
||||
Simulation.SendInput.Mouse.LeftButtonDown();
|
||||
try
|
||||
{
|
||||
await Delay(downHoldMs, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Simulation.SendInput.Mouse.LeftButtonUp();
|
||||
}
|
||||
|
||||
await Delay(postUpDelayMs, ct);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Simulation.SendInput.Mouse.LeftButtonUp();
|
||||
}
|
||||
}
|
||||
|
||||
public static Mat CaptureGameImage(IGameCapture? gameCapture)
|
||||
{
|
||||
var image = gameCapture?.Capture();
|
||||
@@ -225,4 +310,4 @@ public class TaskControl
|
||||
var content = new CaptureContent(image, 0, 0);
|
||||
return content.CaptureRectArea;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ public class TaskRunner
|
||||
TaskTriggerDispatcher.Instance().SetTriggers(GameTaskManager.LoadInitialTriggers());
|
||||
|
||||
VisionContext.Instance().DrawContent.ClearAll();
|
||||
HtmlMaskWindow.CloseAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
WindowStartupLocation="Manual"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True">
|
||||
<Grid>
|
||||
<wv2:WebView2 x:Name="WebView" DefaultBackgroundColor="Transparent"/>
|
||||
</Grid>
|
||||
<Border x:Name="ClickThroughBorder" Background="Transparent">
|
||||
<Grid>
|
||||
<wv2:WebView2 x:Name="WebView" DefaultBackgroundColor="Transparent"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
|
||||
@@ -17,7 +17,7 @@ using Microsoft.Web.WebView2.Core;
|
||||
namespace BetterGenshinImpact.View;
|
||||
|
||||
/// <summary>
|
||||
/// HTML遮罩窗口 - 仅用于显示,不可交互(点击穿透)
|
||||
/// HTML遮罩窗口
|
||||
/// </summary>
|
||||
public partial class HtmlMaskWindow : Window
|
||||
{
|
||||
@@ -29,12 +29,21 @@ public partial class HtmlMaskWindow : Window
|
||||
private readonly string _webView2DataPath;
|
||||
private readonly string _pageUrl;
|
||||
private bool _navigationCompleted;
|
||||
private bool _styleCaptured;
|
||||
private int _originalStyle;
|
||||
private volatile bool _isClickThrough = true;
|
||||
private readonly System.Windows.Media.SolidColorBrush _backgroundBrush = new();
|
||||
|
||||
/// <summary>
|
||||
/// 窗口唯一标识
|
||||
/// </summary>
|
||||
public string MaskId => _id;
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否处于点击穿透模式
|
||||
/// </summary>
|
||||
public bool IsClickThrough => _isClickThrough;
|
||||
|
||||
private HtmlMaskWindow(string url, string? id, string workDir)
|
||||
{
|
||||
_id = id ?? Guid.NewGuid().ToString("N");
|
||||
@@ -42,6 +51,7 @@ public partial class HtmlMaskWindow : Window
|
||||
_webView2DataPath = Path.Combine(workDir, "WebView2Data");
|
||||
_pageUrl = url;
|
||||
InitializeComponent();
|
||||
ClickThroughBorder.Background = _backgroundBrush;
|
||||
Loaded += OnLoaded;
|
||||
InitializeAsync(url);
|
||||
}
|
||||
@@ -156,6 +166,47 @@ public partial class HtmlMaskWindow : Window
|
||||
return _windows.ContainsKey(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口实例,不存在则抛出异常
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
/// <returns>窗口实例</returns>
|
||||
private static HtmlMaskWindow GetWindowOrThrow(string windowId)
|
||||
{
|
||||
if (_windows.TryGetValue(windowId, out var window))
|
||||
return window;
|
||||
throw new InvalidOperationException($"HTML遮罩窗口不存在或已关闭: {windowId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定窗口的点击穿透模式
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
/// <param name="enabled">true=点击穿透,false=可交互</param>
|
||||
public static void SetClickThrough(string windowId, bool enabled)
|
||||
{
|
||||
GetWindowOrThrow(windowId).SetClickThroughMode(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定窗口的点击穿透状态
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
/// <returns>点击穿透状态</returns>
|
||||
public static bool GetClickThrough(string windowId)
|
||||
{
|
||||
return GetWindowOrThrow(windowId).IsClickThrough;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 原子切换指定窗口的点击穿透模式
|
||||
/// </summary>
|
||||
/// <param name="windowId">窗口ID</param>
|
||||
public static void ToggleClickThrough(string windowId)
|
||||
{
|
||||
GetWindowOrThrow(windowId).ToggleClickThroughCore();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知窗口刷新待推送的消息
|
||||
/// </summary>
|
||||
@@ -164,13 +215,24 @@ public partial class HtmlMaskWindow : Window
|
||||
if (!_windows.TryGetValue(windowId, out var window)) return;
|
||||
window.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
// 页面还没加载完,消息留在队列中由 NavigationCompleted 统一推送
|
||||
if (!window._navigationCompleted) return;
|
||||
if (window.WebView.CoreWebView2 == null) return;
|
||||
HtmlMask.FlushPendingMessages(windowId, json =>
|
||||
try
|
||||
{
|
||||
window.WebView.CoreWebView2.PostWebMessageAsString(json);
|
||||
});
|
||||
// 页面还没加载完,消息留在队列中由 NavigationCompleted 统一推送
|
||||
if (!window._navigationCompleted) return;
|
||||
if (window.WebView.CoreWebView2 == null) return;
|
||||
HtmlMask.FlushPendingMessages(windowId, json =>
|
||||
{
|
||||
window.WebView.CoreWebView2.PostWebMessageAsString(json);
|
||||
});
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// WebView 已被释放,忽略此消息推送
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskControl.Logger.LogDebug(ex, "HTML遮罩窗口消息推送异常");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,7 +240,7 @@ public partial class HtmlMaskWindow : Window
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetClickThrough();
|
||||
SetClickThrough(true);
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
@@ -387,14 +449,87 @@ public partial class HtmlMaskWindow : Window
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置点击穿透
|
||||
/// 设置点击穿透模式
|
||||
/// </summary>
|
||||
private void SetClickThrough()
|
||||
/// <param name="enabled">true=点击穿透,false=可交互</param>
|
||||
private void SetClickThrough(bool enabled)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
var style = (int)GetWindowLong(hwnd, WindowLongFlags.GWL_EXSTYLE);
|
||||
User32.SetWindowLong(hwnd, WindowLongFlags.GWL_EXSTYLE,
|
||||
(IntPtr)(style | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT));
|
||||
if (hwnd == IntPtr.Zero) return;
|
||||
|
||||
if (!_styleCaptured)
|
||||
{
|
||||
_originalStyle = (int)GetWindowLong(hwnd, WindowLongFlags.GWL_EXSTYLE);
|
||||
_styleCaptured = true;
|
||||
}
|
||||
|
||||
int newStyle = enabled
|
||||
? (_originalStyle | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT | (int)User32.WindowStylesEx.WS_EX_LAYERED)
|
||||
: _originalStyle;
|
||||
|
||||
User32.SetWindowLong(hwnd, WindowLongFlags.GWL_EXSTYLE, (IntPtr)newStyle);
|
||||
_isClickThrough = enabled;
|
||||
|
||||
SetBackgroundOpacity(!enabled);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
// 禁用穿透:激活遮罩窗口,确保获得键盘和鼠标焦点
|
||||
try
|
||||
{
|
||||
User32.SetForegroundWindow(hwnd);
|
||||
User32.BringWindowToTop(hwnd);
|
||||
Dispatcher.Invoke(() => Activate());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskControl.Logger.LogDebug(ex, "HTML遮罩窗口激活失败");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 开启穿透:将焦点还给游戏窗口
|
||||
try
|
||||
{
|
||||
var gameHandle = TaskContext.Instance().GameHandle;
|
||||
if (gameHandle != IntPtr.Zero)
|
||||
{
|
||||
SystemControl.FocusWindow(gameHandle);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskControl.Logger.LogDebug(ex, "HTML遮罩恢复游戏焦点失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置背景透明度
|
||||
/// </summary>
|
||||
/// <param name="isInteractive">是否处于交互模式</param>
|
||||
private void SetBackgroundOpacity(bool isInteractive)
|
||||
{
|
||||
_backgroundBrush.Color = isInteractive
|
||||
? System.Windows.Media.Color.FromArgb(1, 0, 0, 0)
|
||||
: System.Windows.Media.Color.FromArgb(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置点击穿透模式
|
||||
/// </summary>
|
||||
/// <param name="enabled">true=点击穿透,false=可交互</param>
|
||||
public void SetClickThroughMode(bool enabled)
|
||||
{
|
||||
Dispatcher.Invoke(() => SetClickThrough(enabled));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换点击穿透模式
|
||||
/// </summary>
|
||||
private void ToggleClickThroughCore()
|
||||
{
|
||||
Dispatcher.Invoke(() => SetClickThrough(!_isClickThrough));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -229,7 +229,7 @@ public partial class OneDragonFlowViewModel : ViewModel
|
||||
|
||||
[ObservableProperty] private List<string> _domainNameList = ["", ..MapLazyAssets.Instance.DomainNameList];
|
||||
|
||||
[ObservableProperty] private List<string> _completionActionList = ["无", "关闭游戏", "关闭游戏和软件", "关机"];
|
||||
[ObservableProperty] private List<string> _completionActionList = ["无", "关闭游戏", "关闭软件", "关闭游戏和软件", "关机"];
|
||||
|
||||
[ObservableProperty] private List<string> _sundayEverySelectedValueList = ["","1", "2", "3"];
|
||||
|
||||
@@ -650,6 +650,9 @@ public partial class OneDragonFlowViewModel : ViewModel
|
||||
case "关闭游戏":
|
||||
SystemControl.CloseGame();
|
||||
break;
|
||||
case "关闭软件":
|
||||
Application.Current.Dispatcher.Invoke(() => { Application.Current.Shutdown(); });
|
||||
break;
|
||||
case "关闭游戏和软件":
|
||||
SystemControl.CloseGame();
|
||||
Application.Current.Dispatcher.Invoke(() => { Application.Current.Shutdown(); });
|
||||
@@ -820,4 +823,4 @@ public partial class OneDragonFlowViewModel : ViewModel
|
||||
Toast.Error("重命名配置时失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<PackageReference Include="Vanara.Windows.Extensions" Version="4.1.3" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.20250507" />
|
||||
<PackageReference Include="OpenCvSharp4.Windows" Version="4.11.0.20250507" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user