Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs
2026-01-19 21:13:55 +08:00

1032 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.Core.Script.Dependence;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask.AutoFight.Config;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.Helpers;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
using BetterGenshinImpact.GameTask.AutoTrackPath;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.ViewModel.Pages;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
using BetterGenshinImpact.GameTask.AutoPathing;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
using BetterGenshinImpact.GameTask.AutoPathing.Model.Enum;
namespace BetterGenshinImpact.GameTask.AutoFight.Model;
/// <summary>
/// 队伍内的角色
/// </summary>
public class Avatar
{
/// <summary>
/// 配置文件中的角色信息
/// </summary>
public readonly CombatAvatar CombatAvatar;
/// <summary>
/// 角色名称 中文
/// </summary>
public string Name { get; set; }
/// <summary>
/// 队伍内序号
/// </summary>
public int Index { get; set; }
/// <summary>
/// 最近一次OCR识别出的CD到期时间
/// </summary>
private DateTime OcrSkillCd { get; set; }
/// <summary>
/// 手动配置的技能CD有它就不使用OCR,小于0为自动
/// </summary>
public double ManualSkillCd { get; set; }
/// <summary>
/// 最近一次使用元素战技的时间
/// </summary>
public DateTime LastSkillTime { get; set; }
/// <summary>
/// 元素爆发是否就绪
/// </summary>
public bool IsBurstReady { get; set; }
/// <summary>
/// 名字所在矩形位置
/// </summary>
public Rect NameRect { get; set; }
/// <summary>
/// 名字右边的编号位置
/// </summary>
public Rect IndexRect { get; set; }
/// <summary>
/// 任务取消令牌
/// </summary>
public CancellationToken Ct { get; set; }
/// <summary>
/// 战斗场景
/// </summary>
public CombatScenes CombatScenes { get; set; }
public Avatar(CombatScenes combatScenes, string name, int index, Rect nameRect, double manualSkillCd = -1)
{
CombatScenes = combatScenes;
Name = name;
Index = index;
NameRect = nameRect;
CombatAvatar = DefaultAutoFightConfig.CombatAvatarMap[name];
ManualSkillCd = manualSkillCd;
AutoFightTask.FightStatusFlag = false;
}
/// <summary>
/// 是否存在角色被击败
/// 通过判断确认按钮
/// </summary>
/// <param name="region"></param>
/// <param name="ct"></param>
/// <returns></returns>
public static void ThrowWhenDefeated(ImageRegion region, CancellationToken ct)
{
if (Bv.IsInRevivePrompt(region))
{
Logger.LogWarning("检测到复苏界面,存在角色被击败,前往七天神像复活");
// 先打开地图
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); // NOTE: 此处按下Esc是为了关闭复苏界面无需改键
Sleep(600, ct);
TpForRecover(ct, new RetryException("检测到复苏界面,存在角色被击败,前往七天神像复活"));
}
else if(AutoFightParam.SwimmingEnabled && AutoFightTask.FightStatusFlag && SwimmingConfirm(region))
{
if (AutoFightTask.FightWaypoint is not null)
{
using var ra = CaptureToRectArea();
if (!SwimmingConfirm(ra)) //二次确认
{
return;
}
Logger.LogInformation("游泳检测:尝试回到战斗地点");
var cts = new CancellationTokenSource();
var pathExecutor = new PathExecutor(cts.Token);
try
{
pathExecutor.FaceTo(AutoFightTask.FightWaypoint).Wait(2000,cts.Token);
AutoFightTask.FightWaypoint.MoveMode = MoveModeEnum.Fly.Code; // 改为跳飞
Simulation.SendInput.Mouse.RightButtonDown();
pathExecutor.MoveTo(AutoFightTask.FightWaypoint).Wait(15000,cts.Token);
cts.Cancel();
AutoFightTask.FightWaypoint = null;
Simulation.SendInput.Mouse.RightButtonUp();
}
catch (TaskCanceledException)
{
Logger.LogError("游泳检测:回到战斗地点任务被取消");
}
catch (Exception ex)
{
Logger.LogError(ex, "游泳检测:回到战斗地点异常");
}
finally
{
Simulation.ReleaseAllKey();
}
using var bitmap2 = CaptureToRectArea();
if (!SwimmingConfirm(bitmap2))
{
Logger.LogInformation("游泳检测:游泳脱困成功");
return;
}
Logger.LogWarning("游泳检测:回到战斗地点失败");
}
Logger.LogWarning("战斗过程检测到游泳,前往七天神像重试");
TpForRecover(ct, new RetryException("战斗过程检测到游泳,前往七天神像重试"));
}
}
/// <summary>
/// 游泳检测(色块连通性检测)
/// 游泳时右下角会出现鼠标图标,带有黄色色块,不受改按键影响
/// </summary>
private static bool SwimmingConfirm(Region region)
{
using var mask = OpenCvCommonHelper.Threshold(region.ToImageRegion().DeriveCrop(1819, 1025, 9, 11).SrcMat,
new Scalar(242, 223, 39),new Scalar(255, 233, 44));
using var labels = new Mat();
using var stats = new Mat();
using var centroids = new Mat();
var numLabels = Cv2.ConnectedComponentsWithStats(mask, labels, stats, centroids,
connectivity: PixelConnectivity.Connectivity4, ltype: MatType.CV_32S);
return numLabels > 1;
}
/// <summary>
/// tp 到七天神像恢复
/// </summary>
/// <param name="ct"></param>
/// <param name="ex"></param>
/// <exception cref="RetryException"></exception>
public static void TpForRecover(CancellationToken ct, Exception ex)
{
// tp 到七天神像复活
var tpTask = new TpTask(ct);
tpTask.TpToStatueOfTheSeven().Wait(ct);
Logger.LogInformation("血量恢复完成。【设置】-【七天神像设置】可以修改回血相关配置。");
throw ex;
}
/// <summary>
/// 切换到本角色
/// 切换cd是1秒如果切换失败会尝试再次切换最多尝试5次
/// </summary>
public void Switch()
{
var context = new AvatarActiveCheckContext();
for (var i = 0; i < 30; i++)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
using var region = CaptureToRectArea();
ThrowWhenDefeated(region, Ct);
// 切换成功
if (CombatScenes.GetActiveAvatarIndex(region, context) == Index)
{
return;
}
SimulateSwitchAction(Index);
// Debug.WriteLine($"切换到{Index}号位");
// Cv2.ImWrite($"log/切换.png", region.SrcMat);
Sleep(250, Ct);
}
}
/// <summary>
/// 尝试切换到本角色
/// </summary>
/// <param name="tryTimes"></param>
/// <param name="needLog"></param>
/// <returns></returns>
public bool TrySwitch(int tryTimes = 4, bool needLog = true)
{
var context = new AvatarActiveCheckContext();
for (var i = 0; i < tryTimes; i++)
{
if (Ct is { IsCancellationRequested: true })
{
return false;
}
using var region = CaptureToRectArea();
ThrowWhenDefeated(region, Ct);
// 切换成功
if (CombatScenes.GetActiveAvatarIndex(region, context) == Index)
{
if (needLog && i > 0)
{
Logger.LogInformation("成功切换角色:{Name}", Name);
}
return true;
}
SimulateSwitchAction(Index);
Sleep(250, Ct);
}
return false;
}
private void SimulateSwitchAction(int index)
{
Simulation.SendInput.SimulateAction(GIActions.Drop); //反正会重试就不等落地了
switch (index)
{
case 1:
Simulation.SendInput.SimulateAction(GIActions.SwitchMember1);
break;
case 2:
Simulation.SendInput.SimulateAction(GIActions.SwitchMember2);
break;
case 3:
Simulation.SendInput.SimulateAction(GIActions.SwitchMember3);
break;
case 4:
Simulation.SendInput.SimulateAction(GIActions.SwitchMember4);
break;
case 5:
Simulation.SendInput.SimulateAction(GIActions.SwitchMember5);
break;
default:
break;
}
}
/// <summary>
/// 切换到本角色
/// 切换cd是1秒如果切换失败会尝试再次切换最多尝试5次
/// </summary>
public void SwitchWithoutCts()
{
var context = new AvatarActiveCheckContext();
for (var i = 0; i < 10; i++)
{
using var region = CaptureToRectArea();
ThrowWhenDefeated(region, Ct);
if (CombatScenes.GetActiveAvatarIndex(region, context) == Index)
{
return;
}
SimulateSwitchAction(Index);
Sleep(250);
}
}
/// <summary>
/// 是否出战状态
/// </summary>
/// <returns></returns>
public bool IsActive(ImageRegion region)
{
if (IndexRect == default)
{
throw new Exception("IndexRect为空");
}
else
{
var white = IsIndexRectWhite(region, IndexRect);
return !white;
}
}
private bool IsIndexRectWhite(ImageRegion region, Rect rect)
{
// 剪裁出IndexRect区域
using var indexRa = region.DeriveCrop(rect);
using 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>
/// 是否出战状态
/// </summary>
/// <returns></returns>
[Obsolete]
public bool IsActiveNoIndexRect(ImageRegion region)
{
// 通过寻找右侧人物编号来判断是否出战
if (IndexRect == default)
{
var assetScale = TaskContext.Instance().SystemInfo.AssetScale;
// 剪裁出队伍区域
var teamRa = region.DeriveCrop(AutoFightAssets.Instance.TeamRect);
var blockX = NameRect.X + NameRect.Width * 2 - 10;
var block = teamRa.DeriveCrop(new Rect(blockX, NameRect.Y, teamRa.Width - blockX, NameRect.Height * 2));
// Cv2.ImWrite($"block_{Name}.png", block.SrcMat);
// 取白色区域
var bMat = OpenCvCommonHelper.Threshold(block.SrcMat, new Scalar(255, 255, 255), new Scalar(255, 255, 255));
// Cv2.ImWrite($"block_b_{Name}.png", bMat);
// 矩形识别
Cv2.FindContours(bMat, out var contours, out _, RetrievalModes.External,
ContourApproximationModes.ApproxSimple);
if (contours.Length > 0)
{
var boxes = contours.Select(Cv2.BoundingRect)
.Where(w => w.Width >= 20 * assetScale && w.Height >= 18 * assetScale)
.OrderByDescending(w => w.Width).ToList();
if (boxes.Count is not 0)
{
IndexRect = boxes.First();
return false;
}
}
}
else
{
// 剪裁出IndexRect区域
var teamRa = region.DeriveCrop(AutoFightAssets.Instance.TeamRect);
var blockX = NameRect.X + NameRect.Width * 2 - 10;
var indexBlock = teamRa.DeriveCrop(new Rect(blockX + IndexRect.X, NameRect.Y + IndexRect.Y, IndexRect.Width,
IndexRect.Height));
// Cv2.ImWrite($"indexBlock_{Name}.png", indexBlock.SrcMat);
var count = OpenCvCommonHelper.CountGrayMatColor(indexBlock.CacheGreyMat, 255);
if (count * 1.0 / (IndexRect.Width * IndexRect.Height) > 0.5)
{
return false;
}
}
Logger.LogInformation("{Name} 当前出战", Name);
return true;
}
/// <summary>
/// 普通攻击
/// </summary>
/// <param name="ms">攻击时长建议是200的倍数</param>
public void Attack(int ms = 0)
{
while (ms >= 0)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
ms -= 200;
Sleep(200, Ct);
}
}
/// <summary>
/// 使用元素战技 E
/// </summary>
public void UseSkill(bool hold = false)
{
for (var i = 0; i < 1; i++)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
if (hold)
{
if (Name == "纳西妲")
{
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyDown);
Sleep(300, Ct);
for (int j = 0; j < 10; j++)
{
Simulation.SendInput.Mouse.MoveMouseBy(1000, 0);
Sleep(50); // 持续操作不应该被cts取消
}
Sleep(300); // 持续操作不应该被cts取消
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyUp);
}
else if (Name == "坎蒂丝")
{
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyDown);
Thread.Sleep(3000);
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyUp);
}
else
{
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.Hold);
}
}
else
{
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill);
}
Sleep(200, Ct);
using var region = CaptureToRectArea();
ThrowWhenDefeated(region, Ct); // 检测是不是要跑神像
var cd = AfterUseSkill(region);
if (cd > 0)
{
Logger.LogInformation(hold ? "{Name} 长按元素战技cd:{Cd} 秒" : "{Name} 点按元素战技cd:{Cd} 秒", Name,
Math.Round(cd, 2));
return;
}
}
}
/// <summary>
/// 使用完元素战技的回调,注意,不会在这里检测是不是需要跑七天神像 <br/>
/// UseSkill 方法内会调用如果没有使用UseSkill但是释放了技能之后记得调用一下这个方法
/// </summary>
/// <returns>当前技能CD</returns>
public double AfterUseSkill(ImageRegion? givenRegion = null)
{
LastSkillTime = DateTime.UtcNow;
if (ManualSkillCd > 0)
{
return GetSkillCdSeconds();
}
using var region = givenRegion ?? CaptureToRectArea();
return GetSkillCurrentCd(region);
}
/// <summary>
/// 元素战技是否正在CD中
/// 右下 267x132
/// 77x77
/// </summary>
private double GetSkillCurrentCd(ImageRegion imageRegion)
{
using var eRa = imageRegion.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);
var cd = StringUtils.TryParseDouble(text);
if (cd > 0 && cd <= CombatAvatar.SkillCd)
{
OcrSkillCd = DateTime.UtcNow.AddSeconds(cd);
}
return cd;
}
/// <summary>
/// 使用元素爆发 Q
/// Q释放等待 2s 超时认为没有Q技能
/// </summary>
public void UseBurst()
{
for (var i = 0; i < 10; i++)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
Simulation.SendInput.SimulateAction(GIActions.ElementalBurst);
Sleep(200, Ct);
using var region = CaptureToRectArea();
ThrowWhenDefeated(region, Ct);
if (!PartyAvatarSideIndexHelper.HasAnyIndexRect(region))
{
// 找不到角色编号块意味者技能释放成功
Sleep(1500, Ct);
return;
}
}
}
// /// <summary>
// /// 元素爆发是否正在CD中
// /// 右下 157x165
// /// 110x110
// /// </summary>
// public double GetBurstCurrentCd(CaptureContent content)
// {
// var qRa = content.CaptureRectArea.Crop(AutoFightAssets.Instance.QRect);
// var text = OcrFactory.Paddle.Ocr(qRa.SrcGreyMat);
// return StringUtils.TryParseDouble(text);
// }
/// <summary>
/// 冲刺
/// </summary>
public void Dash(int ms = 0)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
if (ms == 0)
{
ms = 200;
}
Simulation.SendInput.SimulateAction(GIActions.SprintMouse, KeyType.KeyDown);
Sleep(ms); // 冲刺不能被cts取消
Simulation.SendInput.SimulateAction(GIActions.SprintMouse, KeyType.KeyUp);
}
public void Walk(string key, int ms)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
User32.VK vk = User32.VK.VK_NONAME;
if (key == "w")
{
vk = GIActions.MoveForward.ToActionKey().ToVK();
}
else if (key == "s")
{
vk = GIActions.MoveBackward.ToActionKey().ToVK();
}
else if (key == "a")
{
vk = GIActions.MoveLeft.ToActionKey().ToVK();
}
else if (key == "d")
{
vk = GIActions.MoveRight.ToActionKey().ToVK();
}
if (vk == User32.VK.VK_NONAME)
{
return;
}
Simulation.SendInput.Keyboard.KeyDown(vk);
Sleep(ms); // 行走不能被cts取消
Simulation.SendInput.Keyboard.KeyUp(vk);
}
/// <summary>
/// 移动摄像机
/// </summary>
/// <param name="pixelDeltaX">负数是左移,正数是右移</param>
/// <param name="pixelDeltaY"></param>
public void MoveCamera(int pixelDeltaX, int pixelDeltaY)
{
Simulation.SendInput.Mouse.MoveMouseBy(pixelDeltaX, pixelDeltaY);
}
/// <summary>
/// 等待
/// </summary>
/// <param name="ms"></param>
public void Wait(int ms)
{
Sleep(ms); // 由于存在宏操作等待不应被cts取消
}
/// <summary>
/// 等待完成
/// </summary>
public void Ready()
{
Sleep(200, Ct);
for (int i = 0; i < 20; i++)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
using var region = CaptureToRectArea();
// 等待角色编号块出现
if (PartyAvatarSideIndexHelper.HasAnyIndexRect(region))
{
region.Dispose();
return;
}
Sleep(150, Ct);
}
}
/// <summary>
///
/// 根据cd推算E技能是否好了
/// </summary>
/// <param name="skillCd">强制指定技能CD</param>
/// <param name="printLog">log是否输出</param>
/// <returns>是否好了</returns>
public bool IsSkillReady(bool printLog = false)
{
var cd = GetSkillCdSeconds();
if (cd > 0)
{
if (printLog)
{
Logger.LogInformation("{Name}的E技能未准备好,CD还有{Seconds}秒", Name, Math.Round(cd, 2));
}
return false;
}
return true;
}
/// <summary>
/// 计算上一次使用技能到现在还剩下多长时间的cd
/// </summary>
/// <returns></returns>
public double GetSkillCdSeconds()
{
switch (ManualSkillCd)
{
case < 0:
{
var now = DateTime.UtcNow;
// 若未经过OCR的技能释放,上次时间加上最长的技能时间
var maxCd = Math.Max(CombatAvatar.SkillHoldCd, CombatAvatar.SkillCd);
var target =
LastSkillTime >= OcrSkillCd
? LastSkillTime.AddSeconds(Math.Max(CombatAvatar.SkillHoldCd, CombatAvatar.SkillCd))
: OcrSkillCd;
var result = now > target ? 0d : (target - now).TotalSeconds;
if (!(result > maxCd)) return result;
Logger.LogWarning("{Name}的当前技能CD大于其最大技能CD{MaxCd}。如果你没有调整系统时间的话这是一个bug。", Name, maxCd);
return maxCd;
}
case > 0:
{
// 用户设置,所以直接通过上次释放技能的时间计算
var dif = DateTime.UtcNow - LastSkillTime;
if (ManualSkillCd > dif.TotalSeconds)
{
return ManualSkillCd - dif.TotalSeconds;
}
break;
}
}
return 0;
}
/// <summary>
/// 等待技能CD
/// </summary>
/// <param name="ct">CancellationToken</param>
public async Task WaitSkillCd(CancellationToken ct = default)
{
// 获取CD时间
if (IsSkillReady())
{
return;
}
var s = GetSkillCdSeconds() + 0.2;
Logger.LogInformation("{Name}的E技能CD未结束等待{Seconds}秒", Name, Math.Round(s, 2));
await Delay((int)Math.Ceiling(s * 1000), ct);
}
/// <summary>
/// 跳跃
/// </summary>
public void Jump()
{
Simulation.SendInput.SimulateAction(GIActions.Jump);
}
/// <summary>
/// 重击
/// </summary>
public void Charge(int ms = 0)
{
if (ms == 0)
{
ms = 1000;
}
if (Name == "那维莱特")
{
var dpi = TaskContext.Instance().DpiScale;
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyDown);
while (ms >= 0)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
Simulation.SendInput.Mouse.MoveMouseBy((int)(1000 * dpi), 0);
ms -= 50;
Sleep(50); // 持续操作不应该被cts取消
}
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyUp);
}
else if (Name == "恰斯卡")
{
var dpi = TaskContext.Instance().DpiScale;
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyDown);
int tick = -4; // 起飞那一刻需要多一点点时间用来矫正视角高度
while (ms >= 0)
{
if (Ct is { IsCancellationRequested: true })
{
return;
}
// 恰在蓄力时转得越快越容易把视角趋向于水平
// 基于上面这个特性,如果我们用同一个鼠标方向向量,大致能在所有设备上控制视角高低(只要帧率不太低)
// 恰的子弹上膛机制怪物要在HUD准星框内超过一定时长体感0.2-0.3秒)才能让子弹上膛。所以搜索敌人要低速。不然敌人体型小或者远就很容易锁不上。
const double lowspeed = 0.7, highspeed = 50;
double rateX, rateY;
if (tick < 3)
{
rateX = highspeed;
rateY = highspeed * 0.23;
}
else if (tick < 40)
{
rateX = lowspeed * 0.7;
rateY = 0;
}
else if (tick < 43)
{
rateX = highspeed;
rateY = highspeed * 0.4;
}
else if (tick < 70)
{
rateX = lowspeed * 0.9;
rateY = 0;
}
else if (tick < 73)
{
rateX = highspeed;
rateY = highspeed;
}
else
{
rateX = lowspeed;
rateY = 0;
}
Simulation.SendInput.Mouse.MoveMouseBy((int)(rateX * 50 * dpi), (int)(rateY * 50 * dpi));
tick = (tick + 1) % 100;
Sleep(25);
ms -= 25;
}
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyUp);
}
else
{
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyDown);
Sleep(ms); // 持续操作不应该被cts取消
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyUp);
}
}
public void MouseDown(string key = "left")
{
key = key.ToLower();
if (key == "left")
{
Simulation.SendInput.Mouse.LeftButtonDown();
}
else if (key == "right")
{
Simulation.SendInput.Mouse.RightButtonDown();
}
else if (key == "middle")
{
Simulation.SendInput.Mouse.MiddleButtonDown();
}
}
public void MouseUp(string key = "left")
{
key = key.ToLower();
if (key == "left")
{
Simulation.SendInput.Mouse.LeftButtonUp();
}
else if (key == "right")
{
Simulation.SendInput.Mouse.RightButtonUp();
}
else if (key == "middle")
{
Simulation.SendInput.Mouse.MiddleButtonUp();
}
}
public void Click(string key = "left")
{
key = key.ToLower();
if (key == "left")
{
Simulation.SendInput.Mouse.LeftButtonClick();
}
else if (key == "right")
{
Simulation.SendInput.Mouse.RightButtonClick();
}
else if (key == "middle")
{
Simulation.SendInput.Mouse.MiddleButtonClick();
}
}
public void MoveBy(int x, int y)
{
GlobalMethod.MoveMouseBy(x, y);
}
public void Scroll(int scrollAmountInClicks)
{
Simulation.SendInput.Mouse.VerticalScroll(scrollAmountInClicks);
}
public void KeyDown(string key)
{
var vk = KeyBindingsSettingsPageViewModel.MappingKey(User32Helper.ToVk(key));
switch (key)
{
case "VK_LBUTTON":
Simulation.SendInput.Mouse.LeftButtonDown();
break;
case "VK_RBUTTON":
Simulation.SendInput.Mouse.RightButtonDown();
break;
case "VK_MBUTTON":
Simulation.SendInput.Mouse.MiddleButtonDown();
break;
case "VK_XBUTTON1":
Simulation.SendInput.Mouse.XButtonDown(0x0001);
break;
case "VK_XBUTTON2":
Simulation.SendInput.Mouse.XButtonDown(0x0001);
break;
default:
Simulation.SendInput.Keyboard.KeyDown(vk);
break;
}
}
public void KeyUp(string key)
{
var vk = KeyBindingsSettingsPageViewModel.MappingKey(User32Helper.ToVk(key));
switch (key)
{
case "VK_LBUTTON":
Simulation.SendInput.Mouse.LeftButtonUp();
break;
case "VK_RBUTTON":
Simulation.SendInput.Mouse.RightButtonUp();
break;
case "VK_MBUTTON":
Simulation.SendInput.Mouse.MiddleButtonUp();
break;
case "VK_XBUTTON1":
Simulation.SendInput.Mouse.XButtonUp(0x0001);
break;
case "VK_XBUTTON2":
Simulation.SendInput.Mouse.XButtonUp(0x0001);
break;
default:
Simulation.SendInput.Keyboard.KeyUp(vk);
break;
}
}
public void KeyPress(string key)
{
var vk = KeyBindingsSettingsPageViewModel.MappingKey(User32Helper.ToVk(key));
switch (key)
{
case "VK_LBUTTON":
Simulation.SendInput.Mouse.LeftButtonClick();
break;
case "VK_RBUTTON":
Simulation.SendInput.Mouse.RightButtonClick();
break;
case "VK_MBUTTON":
Simulation.SendInput.Mouse.MiddleButtonClick();
break;
case "VK_XBUTTON1":
Simulation.SendInput.Mouse.XButtonClick(0x0001);
break;
case "VK_XBUTTON2":
Simulation.SendInput.Mouse.XButtonClick(0x0001);
break;
default:
Simulation.SendInput.Keyboard.KeyPress(vk);
break;
}
}
/// <summary>
/// 从配置字符串中查找角色cd
/// 仅有角色名时返回 -1 ,没找到角色返回null
/// </summary>
/// <param name="avatarName">角色名</param>
/// <param name="input">序列</param>
/// <returns></returns>
public static double? ParseActionSchedulerByCd(string avatarName, string input)
{
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(avatarName))
return null;
var searchIndex = input.Length - 1;
while (true)
{
// 逆向查找角色名最后一次出现的位置
var foundIndex = input.LastIndexOf(avatarName, searchIndex, StringComparison.Ordinal);
if (foundIndex == -1) return null;
// 验证前向边界(分号或字符串起点)
var startValid = foundIndex == 0 ||
input[foundIndex - 1] == ';';
// 验证后向边界(逗号或分号/字符串终点)
var endValid = foundIndex + avatarName.Length == input.Length ||
input[foundIndex + avatarName.Length] == ',' ||
input[foundIndex + avatarName.Length] == ';';
if (startValid && endValid)
{
var valueStart = foundIndex + avatarName.Length;
// 处理逗号后的数值部分
if (valueStart >= input.Length || input[valueStart] != ',') return -1;
var valueEnd = input.IndexOf(';', valueStart);
if (valueEnd == -1) valueEnd = input.Length;
if (double.TryParse(input.AsSpan(valueStart + 1, valueEnd - valueStart - 1),
out var result))
{
return result;
}
// 存在角色名但没有数值的情况
return -1;
}
// 更新搜索范围继续查找
searchIndex = foundIndex - 1;
if (searchIndex < 0) break;
}
return null;
}
}