using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Model;
using BetterGenshinImpact.GameTask.Model.Area;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
namespace BetterGenshinImpact.GameTask.AutoFight.Model;
///
/// 用于处理主界面右侧角色编号的一些方法
///
public class PartyAvatarSideIndexHelper
{
///
/// 角色编号以当前模板匹配结果的情况下的Y轴公差
///
private static readonly int IndexRectDistanceY = 96;
///
/// 检查当前联机状态
///
///
///
///
public static MultiGameStatus DetectedMultiGameStatus(ImageRegion imageRegion, AutoFightAssets? autoFightAssets = null, ILogger? logger = null)
{
if (autoFightAssets == null)
{
autoFightAssets = AutoFightAssets.Instance;
}
if (logger == null)
{
logger = TaskControl.Logger;
}
var status = new MultiGameStatus();
// 判断当前联机人数
var pRaList = imageRegion.FindMulti(autoFightAssets.PRa);
if (pRaList.Count > 0)
{
status.IsInMultiGame = true;
var num = pRaList.Count + 1;
if (num > 4)
{
throw new Exception("当前处于联机状态,但是队伍人数超过4人,无法识别");
}
status.PlayerCount = num;
// 联机状态下判断
var onePRa = imageRegion.Find(autoFightAssets.OnePRa);
if (onePRa.IsExist())
{
logger.LogInformation("当前处于联机状态,且当前账号是房主,联机人数{Num}人", num);
status.IsHost = true;
}
else
{
logger.LogInformation("当前处于联机状态,且在别人世界中,联机人数{Num}人", num);
}
}
else
{
// 没有其他联机玩家的情况下,也有可能是单人房主
var onePRa = imageRegion.Find(autoFightAssets.OnePRa);
if (onePRa.IsExist())
{
logger.LogInformation("当前处于联机状态,但是没有其他玩家连入");
status.IsInMultiGame = true;
status.IsHost = true;
status.PlayerCount = 1;
}
}
return status;
}
///
/// 根据已知的某个角色编号位置,计算其他角色编号的位置
///
/// 已知编号
/// 已知编号矩形
/// 目标编号
/// 目标编号矩形
public static Rect GetIndexRectFromKnownIndexRect(int knownIndex, Rect knownRect, int targetIndex)
{
var s = TaskContext.Instance().SystemInfo.AssetScale;
// y_k + (n - k) * d
int y = knownRect.Y + (targetIndex - knownIndex) * (int)(IndexRectDistanceY * s);
return new Rect(knownRect.X, y, knownRect.Width, knownRect.Height);
}
public static Rect GetIndexRectFromKnownCurrentAvatarFlag(Rect currRect)
{
var s = TaskContext.Instance().SystemInfo.AssetScale;
return new Rect(currRect.X + (int)(126 * s), currRect.Y - (int)(194 * s), (int)(16 * s), (int)(17 * s));
}
public static (List, List) GetAllIndexRects(ImageRegion imageRegion, MultiGameStatus multiGameStatus, ILogger logger, ElementAssets elementAssets, ISystemInfo systemInfo)
{
try
{
// 新的动态获取角色编号位置逻辑
return GetAllIndexRectsNew(imageRegion, multiGameStatus, logger, elementAssets, systemInfo);
}
catch (Exception ex)
{
logger.LogDebug(ex, "使用新方法获取角色编号位置失败");
logger.LogWarning("使用新方法获取角色编号位置失败,原因:" + ex.Message);
logger.LogWarning("尝试使用旧的写死位置逻辑");
// 旧的写死位置逻辑
return GetAllIndexRectsOld(imageRegion, multiGameStatus);
}
}
private static (List, List) GetAllIndexRectsOld(ImageRegion imageRegion, MultiGameStatus multiGameStatus)
{
List avatarSideIconRectList;
List avatarIndexRectList;
if (multiGameStatus.IsInMultiGame)
{
var p = multiGameStatus.IsHost ? "1p" : "p";
avatarSideIconRectList = new List(AutoFightAssets.Instance.AvatarSideIconRectListMap[$"{p}_{multiGameStatus.PlayerCount}"]);
avatarIndexRectList = new List(AutoFightAssets.Instance.AvatarIndexRectListMap[$"{p}_{multiGameStatus.PlayerCount}"]);
}
else
{
avatarSideIconRectList = new List(AutoFightAssets.Instance.AvatarSideIconRectList);
avatarIndexRectList = new List(AutoFightAssets.Instance.AvatarIndexRectList);
}
// 6.0 版本 队伍下的 草露 进度条 导致位置偏移
AvatarSideFixOffset(imageRegion, avatarSideIconRectList, avatarIndexRectList);
return (avatarIndexRectList, avatarSideIconRectList);
}
public static bool HasAnyIndexRect(ImageRegion imageRegion)
{
return ElementAssets.Instance.IndexList.Select(indexRo => imageRegion.Find(indexRo)).Any(indexRes => indexRes.IsExist());
}
public static int CountIndexRect(ImageRegion imageRegion)
{
return ElementAssets.Instance.IndexList.Select(indexRo => imageRegion.Find(indexRo)).Count(indexRes => indexRes.IsExist());
}
public static bool HasActiveAvatarArrow(ImageRegion imageRegion)
{
return imageRegion.Find(ElementAssets.Instance.CurrentAvatarThreshold).IsExist();
}
public static (List, List) GetAllIndexRectsNew(ImageRegion imageRegion, MultiGameStatus multiGameStatus, ILogger logger, ElementAssets elementAssets, ISystemInfo systemInfo)
{
// 找到编号块
var i1 = imageRegion.Find(elementAssets.Index1);
var i2 = imageRegion.Find(elementAssets.Index2);
var i3 = imageRegion.Find(elementAssets.Index3);
var i4 = imageRegion.Find(elementAssets.Index4);
List indexRectList = [i1.ToRect(), i2.ToRect(), i3.ToRect(), i4.ToRect()];
int existNum = indexRectList.Count(indexRect => indexRect != default);
if (existNum == multiGameStatus.MaxControlAvatarCount)
{
// 识别存在个数和当前能控制的最大角色数相等,意味者全部识别,直接返回
var notNullIndexRectList = indexRectList.Where(r => r != default).ToList();
return (notNullIndexRectList, GetAvatarSideIconRectFromIndexRect(notNullIndexRectList, systemInfo));
}
else
{
// 为什么这里要用箭头确认一遍?因为出战角色编号框的识别率不是100%,需要用箭头来辅助确认。这也是为了保证非满队情况下的队伍识别率
// 非出战角色编号框识别率100%
var curr = imageRegion.Find(elementAssets.CurrentAvatarThreshold); // 当前出战角色标识
if (curr.IsExist())
{
var (knownIndex, knownRect) = GetKnownIndexAndRect(indexRectList);
if (knownRect == default)
{
// 没有已知的编号位置,这种情况下可能是单人队
// 直接用出战角色标识来反推
var oneIndexRect = GetIndexRectFromKnownCurrentAvatarFlag(curr.ToRect());
logger.LogInformation("当前编队中可能只存在一个角色(且角色编号未正确识别)");
return ([oneIndexRect], [GetAvatarSideIconRectFromIndexRect(oneIndexRect, systemInfo)]);
}
else
{
// 有已知的编号位置,通过已知位置来推测其他位置
for (int i = 0; i < indexRectList.Count; i++)
{
if (indexRectList[i] == default)
{
var rect = GetIndexRectFromKnownIndexRect(knownIndex, knownRect, i + 1);
if (IsIntersecting(curr.Y, curr.Height, rect.Y, rect.Height))
{
// 如果和当前出战角色标识相交,说明这个位置是正确的
indexRectList[i] = rect;
logger.LogInformation("当前出战角色未正确识别,通过出战标识推测角色编号为{Index}", i + 1);
}
}
}
// 校验推测结果(编号从 1 开始必定连续)
if (AreNullsAtEnd(indexRectList))
{
var notNullIndexRectList = indexRectList.Where(r => r != default).ToList();
return (notNullIndexRectList, GetAvatarSideIconRectFromIndexRect(notNullIndexRectList, systemInfo));
}
else
{
throw new Exception("校验角色列表识别结果失败,角色编号不是连续的!");
}
}
}
else
{
// 没有出战角色标识的情况下,直接抛出错误走写死逻辑
throw new Exception("找不到出战角色编号块与当前出战角色标识!");
}
}
}
private static (int, Rect) GetKnownIndexAndRect(List indexRectList)
{
for (int i = 0; i < indexRectList.Count; i++)
{
if (indexRectList[i] != default)
{
return (i + 1, indexRectList[i]);
}
}
return (-1, default);
}
public static Rect GetAvatarSideIconRectFromIndexRect(Rect indexRect, ISystemInfo systemInfo)
{
var s = systemInfo.AssetScale;
return new Rect(indexRect.X - (int)(91 * s), indexRect.Y - (int)(47 * s), (int)(82 * s), (int)(82 * s));
}
public static List GetAvatarSideIconRectFromIndexRect(List indexRect, ISystemInfo systemInfo)
{
return indexRect.Select(r => GetAvatarSideIconRectFromIndexRect(r, systemInfo)).ToList();
}
public static bool IsIntersecting(double y1, double h1, double y2, double h2)
{
// 计算第一个区域的结束位置
double end1 = y1 + h1;
// 计算第二个区域的结束位置
double end2 = y2 + h2;
return y1 < end2 && y2 < end1;
}
public static bool AreNullsAtEnd(List list)
{
int firstNullIndex = list.FindIndex(x => x == default); // 找到第一个 null 的索引
return firstNullIndex == -1 || list.Skip(firstNullIndex).All(x => x == default); // 检查从第一个 null 开始到末尾是否都是 null
}
///
/// 6.0 版本 队伍下的 草露 进度条 导致位置偏移
///
///
///
///
///
public static bool AvatarSideFixOffset(ImageRegion imageRegion, List avatarSideIconRectList, List avatarIndexRectList)
{
// 角色序号 左上角 坐标偏移(+2, -5)后存在3个白色点,则认为存在 草露 进度条
// 存在 草露 进度条时候整体上移 14 个像素
var whitePointCount = 0;
foreach (var rectIndex in avatarIndexRectList)
{
int x = rectIndex.X + 2;
int y = rectIndex.Y - 5;
var color = imageRegion.SrcMat.At(y, x);
if (color is { Item0: 255, Item1: 255, Item2: 255 })
{
whitePointCount++;
}
}
if (whitePointCount < 3)
{
return false;
}
TaskControl.Logger.LogInformation("检测到右侧队伍上偏移,进行位置偏移");
for (var i = 0; i < avatarSideIconRectList.Count; i++)
{
var rect = avatarSideIconRectList[i];
rect.Y -= 14;
avatarSideIconRectList[i] = rect;
}
for (var i = 0; i < avatarIndexRectList.Count; i++)
{
var rect = avatarIndexRectList[i];
rect.Y -= 14;
avatarIndexRectList[i] = rect;
}
return true;
}
///
/// 识别当前出战角色编号
/// 1. 颜色识别只要成功一次就认为成功并返回(优先级最高)
/// 2. 出战标识识别成功,颜色识别失败,认为结果不确定,需要重试一次。2次后结果相同认为成功
///
///
///
///
///
public static int GetAvatarIndexIsActiveWithContext(ImageRegion imageRegion, Rect[] rectArray, AvatarActiveCheckContext context)
{
var indexByColor = FindActiveIndexRectByColor(imageRegion, rectArray);
if (indexByColor > 0)
{
context.TotalCheckFailedCount = 0;
return indexByColor;
}
var indexByArrow = FindActiveIndexRectByArrow(imageRegion, rectArray);
if (indexByArrow > 0)
{
// 累计识别次数
context.ActiveIndexByArrowCount[indexByArrow - 1]++;
if (context.ActiveIndexByArrowCount[indexByArrow - 1] >= 2)
{
context.TotalCheckFailedCount = 0;
return indexByArrow;
}
return -2; // 重试
}
context.TotalCheckFailedCount++;
return -1; // 两种方式都失败
}
// public static int FindDifferentRect(Mat greyMat, Rect[] rectArray)
// {
// // 取其中一个矩形和另外三个矩形进行比较
// var one = new Mat(greyMat, rectArray[0]);
// for (int i = 1; i < rectArray.Length; i++)
// {
// Mat diff = new Mat();
// Cv2.Absdiff(one, new Mat(greyMat, rectArray[i]), diff);
// Scalar diffSum = Cv2.Sum(diff);
// double totalDiff = diffSum.Val0 + diffSum.Val1 + diffSum.Val2;
// totalDiff = totalDiff / (one.Width * one.Height);
// }
//
// return 1;
// }
public static int FindActiveIndexRectByColor(ImageRegion imageRegion, Rect[] rectArray)
{
if (rectArray.Length == 1)
{
return 1;
}
Mat[] mats = new Mat[rectArray.Length];
try
{
int whiteCount = 0, notWhiteRectNum = 0;
var mat = imageRegion.CacheGreyMat;
for (int i = 0; i < rectArray.Length; i++)
{
var indexMat = new Mat(mat, rectArray[i]);
mats[i] = indexMat;
if (IsWhiteRect(indexMat))
{
whiteCount++;
}
else
{
notWhiteRectNum = i + 1;
}
}
if (whiteCount == rectArray.Length - 1)
{
return notWhiteRectNum;
}
else
{
// 方法2:边缘像素白色比例
int m2 = FindActiveIndexRectByEdgeColor(mats);
if (m2 > 0)
{
return m2;
}
// 方法3:使用更加靠谱的差值识别(-1是未识别),但是不支持非满队
if (mats.Length == 4)
{
return ImageDifferenceDetector.FindMostDifferentImage(mats);
}
else
{
return -1;
}
}
}
finally
{
foreach (var mat in mats)
{
mat?.Dispose();
}
}
}
public static bool IsWhiteRect(Mat indexMat)
{
var count1 = OpenCvCommonHelper.CountGrayMatColor(indexMat, 251, 255); // 白
var count2 = OpenCvCommonHelper.CountGrayMatColor(indexMat, 50, 54); // 黑色文字
if ((count1 + count2) * 1.0 / (indexMat.Width * indexMat.Height) > 0.35)
{
// Debug.WriteLine($"白色矩形占比{(count1 + count2) * 1.0 / (indexMat.Width * indexMat.Height)}");
return true;
}
return false;
}
///
/// 使用出战标识识别出战
///
///
///
///
public static int FindActiveIndexRectByArrow(ImageRegion imageRegion, Rect[] rectArray)
{
if (rectArray.Length == 1)
{
return 1;
}
var curr = imageRegion.Find(ElementAssets.Instance.CurrentAvatarThreshold); // 当前出战角色标识
if (curr.IsEmpty())
{
return -1;
}
for (int i = 0; i < rectArray.Length; i++)
{
if (IsIntersecting(curr.Y, curr.Height, rectArray[i].Y, rectArray[i].Height))
{
return i + 1;
}
}
return -1;
}
///
/// 通过边缘像素颜色识别出战角色编号
///
///
///
public static int FindActiveIndexRectByEdgeColor(Mat[] mats)
{
try
{
int whiteCount = 0, notWhiteRectNum = 0;
for (int i = 0; i < mats.Length; i++)
{
if (CalculateWhiteEdgePixelsRatio(mats[i]) > 0.5)
{
whiteCount++;
}
else
{
notWhiteRectNum = i + 1;
}
}
if (whiteCount == mats.Length - 1)
{
return notWhiteRectNum;
}
else if (whiteCount == mats.Length)
{
// 如果四个都是白色,那就找内部有没有黑色
int blackCount = 0, notBlackRectNum = -1;
for (int i = 0; i < mats.Length; i++)
{
var count = OpenCvCommonHelper.CountGrayMatColorC1(mats[i], 50, 50); // 黑字
if (count > 0)
{
blackCount++;
}
else
{
notBlackRectNum = i + 1;
}
}
if (notBlackRectNum >= 1)
{
TaskControl.Logger.LogDebug("当前所有编号边缘均为白色(背景过白),通过内部黑色像素识别出战编号为{Index},存在黑色数字的角色编号有{C1}个,总角色数量{C2}", notBlackRectNum, blackCount, mats.Length);
return notBlackRectNum;
}
}
else
{
return -1;
}
}
catch (Exception e)
{
Debug.WriteLine(e);
}
return -1;
}
///
/// 计算灰度图最边缘一圈中纯白色(255)像素的占比
///
/// 返回纯白像素占比 (0.0 到 1.0)
public static double CalculateWhiteEdgePixelsRatio(Mat image)
{
int whiteCount = 0;
int height = image.Height;
int width = image.Width;
// 如果图片太小,无法获取边缘
if (height < 1 || width < 1)
{
return 0.0;
}
// 计算总边缘像素数
int totalCount = 2 * (width + height - 2);
// 顶边和底边
for (int x = 0; x < width; x++)
{
// 顶边
if (image.At(0, x) == 255)
{
whiteCount++;
}
// 底边(避免只有一行时重复计数)
if (height > 1 && image.At(height - 1, x) == 255)
{
whiteCount++;
}
}
// 左边和右边(不包括四个角,因为已经在顶边和底边中计算过)
for (int y = 1; y < height - 1; y++)
{
// 左边
if (image.At(y, 0) == 255)
{
whiteCount++;
}
// 右边(避免只有一列时重复计数)
if (width > 1 && image.At(y, width - 1) == 255)
{
whiteCount++;
}
}
// 计算并返回占比
return totalCount > 0 ? (double)whiteCount / totalCount : 0.0;
}
}