using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
using BetterGenshinImpact.GameTask.AutoWood.Assets;
using BetterGenshinImpact.GameTask.AutoWood.Utils;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.Genshin.Settings;
using BetterGenshinImpact.View.Drawable;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using static Vanara.PInvoke.User32;
using GC = System.GC;
namespace BetterGenshinImpact.GameTask.AutoWood;
///
/// 自动伐木
///
public partial class AutoWoodTask : ISoloTask
{
public string Name => "自动伐木";
private readonly AutoWoodAssets _assets;
private bool _first = true;
private readonly WoodStatisticsPrinter _printer;
private readonly Login3rdParty _login3rdParty;
private VK _zKey = VK.VK_Z;
private readonly WoodTaskParam _taskParam;
private CancellationToken _ct;
public AutoWoodTask(WoodTaskParam taskParam)
{
this._taskParam = taskParam;
_login3rdParty = new();
AutoWoodAssets.DestroyInstance();
_assets = AutoWoodAssets.Instance;
_printer = new WoodStatisticsPrinter(_assets);
}
public Task Start(CancellationToken ct)
{
var runTimeWatch = new Stopwatch();
_ct = ct;
_printer.Ct = _ct;
try
{
Kernel32.SetThreadExecutionState(Kernel32.EXECUTION_STATE.ES_CONTINUOUS | Kernel32.EXECUTION_STATE.ES_SYSTEM_REQUIRED | Kernel32.EXECUTION_STATE.ES_DISPLAY_REQUIRED);
Logger.LogInformation("→ {Text} 设置伐木总次数:{Cnt},设置木材数量上限:{MaxCnt}", "自动伐木,启动!", _taskParam.WoodRoundNum, _taskParam.WoodDailyMaxCount);
_login3rdParty.RefreshAvailabled();
if (_login3rdParty.Type == Login3rdParty.The3rdPartyType.Bilibili)
{
Logger.LogInformation("自动伐木启用B服模式");
}
SettingsContainer settingsContainer = new();
if (settingsContainer.OverrideController?.KeyboardMap?.ActionElementMap.Where(item => item.ActionId == ActionId.Gadget).FirstOrDefault()?.ElementIdentifierId is ElementIdentifierId key)
{
if (key != ElementIdentifierId.Z)
{
_zKey = key.ToVK();
Logger.LogInformation($"自动伐木检测到用户改键 {ElementIdentifierId.Z.ToName()} 改为 {key.ToName()}");
if (key == ElementIdentifierId.LeftShift || key == ElementIdentifierId.RightShift)
{
Logger.LogInformation($"用户改键 {key.ToName()} 可能不受模拟支持,若使用正常则忽略");
}
}
}
SystemControl.ActivateWindow();
// 伐木开始计时
runTimeWatch.Start();
for (var i = 0; i < _taskParam.WoodRoundNum; i++)
{
if (TaskContext.Instance().Config.AutoWoodConfig.WoodCountOcrEnabled)
{
if (_printer.WoodStatisticsAlwaysEmpty())
{
Logger.LogInformation("连续{Cnt}次获取木材数量为0。判定附近没有能响应「王树瑞佑」的树木!或者已达每日数量上限", _printer.NothingCount);
break;
}
if (_printer.ReachedWoodMaxCount)
{
Logger.LogInformation("{Names}已达到设置的上限:{MaxCnt}", _printer.WoodTotalDict.Keys, _taskParam.WoodDailyMaxCount);
break;
}
}
Logger.LogInformation("第{Cnt}次伐木", i + 1);
if (_ct.IsCancellationRequested)
{
break;
}
Felling(_taskParam, i + 1 == _taskParam.WoodRoundNum);
VisionContext.Instance().DrawContent.ClearAll();
Sleep(500, _ct);
}
return Task.CompletedTask;
}
finally
{
// 伐木结束计时
runTimeWatch.Stop();
Kernel32.SetThreadExecutionState(Kernel32.EXECUTION_STATE.ES_CONTINUOUS);
var elapsedTime = runTimeWatch.Elapsed;
Logger.LogInformation(@"本次伐木总耗时:{Time:hh\:mm\:ss}", elapsedTime);
}
}
private partial class WoodStatisticsPrinter(AutoWoodAssets assert)
{
public bool ReachedWoodMaxCount;
public int NothingCount;
public readonly ConcurrentDictionary WoodTotalDict = new();
private bool _firstWoodOcr = true;
private string _firstWoodOcrText = "";
private readonly Dictionary _woodMetricsDict = [];
private readonly Dictionary _woodNotPrintDict = [];
// from:https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/home/content/list?app_sn=ys_obc&channel_id=13
private static readonly List ExistWoods =
[
"悬铃木", "白梣木", "炬木", "椴木", "香柏木", "刺葵木", "柽木", "辉木", "业果木", "证悟木", "枫木", "垂香木",
"杉木", "竹节", "却砂木", "松木", "萃华木", "桦木", "孔雀木", "梦见木", "御伽木"
];
public CancellationToken Ct { get; set; }
[GeneratedRegex("([^\\d\\n]+)[×x](\\d+)")]
private static partial Regex _parseWoodStatisticsRegex();
public bool WoodStatisticsAlwaysEmpty()
{
return NothingCount >= 3;
}
public void PrintWoodStatistics(WoodTaskParam taskParam)
{
var woodStatisticsText = GetWoodStatisticsText(taskParam);
if (string.IsNullOrEmpty(woodStatisticsText))
{
NothingCount++;
Logger.LogWarning("未能识别到伐木的统计数据");
if (_woodMetricsDict.Count == 0)
{
TaskContext.Instance().Config.AutoWoodConfig.WoodCountOcrEnabled = false;
throw new NormalEndException("首次伐木就未识别到木材数据,已经自动关闭【OCR识别并累计木材数】的功能,请重新启动【自动伐木】功能!");
}
return;
}
ParseWoodStatisticsText(taskParam, woodStatisticsText);
CheckAndPrintWoodQuantities(taskParam);
}
private string GetWoodStatisticsText(WoodTaskParam taskParam)
{
var firstOcrResultList = new List();
// 创建一个计时器,循环识别文本,直到超时
var stopwatch = Stopwatch.StartNew();
while (stopwatch.ElapsedMilliseconds < 3500)
{
// OCR识别木材文本
var recognizedText = WoodTextAreaOcr();
if (_firstWoodOcr)
{
// 首次时会重复OCR识别,然后找到最好的OCR结果(即最长的那个)
var isFound = HasDetectedWoodText(recognizedText);
if (isFound) firstOcrResultList.Add(recognizedText);
if (firstOcrResultList.Count != 0 && !isFound) break;
SleepDurationBetweenOcrs(taskParam);
}
else
{
var isFound = HasDetectedWoodText(recognizedText);
if (!isFound)
{
SleepDurationBetweenOcrs(taskParam);
continue;
}
NothingCount = 0;
// 等待伐木的木材数量显示全,再次OCR识别。
// SleepDurationBetweenOcrs(taskParam);
// return WoodTextAreaOcr();
// 直接返回首次的识别结果
return _firstWoodOcrText;
}
}
stopwatch.Stop(); // 停止计时
_firstWoodOcrText = FindBestOcrResult(firstOcrResultList);
return _firstWoodOcrText;
}
private void SleepDurationBetweenOcrs(WoodTaskParam taskParam)
{
Sleep(_firstWoodOcr ? 300 : 100, Ct);
}
private string WoodTextAreaOcr()
{
// OCR识别文本区域
var woodCountRect = CaptureToRectArea().DeriveCrop(assert.WoodCountUpperRect);
return OcrFactory.Paddle.Ocr(woodCountRect.SrcGreyMat);
}
private bool HasDetectedWoodText(string recognizedText)
{
if (!_firstWoodOcr)
{
return !string.IsNullOrEmpty(recognizedText) &&
recognizedText.Contains("获得");
}
return !string.IsNullOrEmpty(recognizedText) &&
recognizedText.Contains("获得") &&
(recognizedText.Contains('×') || recognizedText.Contains('x'));
}
private void ParseWoodStatisticsText(WoodTaskParam taskParam, string text)
{
// 从识别的文本中提取木材名称和数量
// 格式示例:"获得\n竹节×30\n杉木×20"
if (!text.Contains('×') && !text.Contains('X'))
{
Logger.LogWarning("未能正确解析木材信息格式:{woodText}", text);
return;
}
// 匹配模式 "名称×数量"
var matches = _parseWoodStatisticsRegex().Matches(text);
// 如果OCR识别木材的种类小于等于首次保存的一样时,直接使用首次的木材数量。
if (!_firstWoodOcr && 1 <= matches.Count && matches.Count <= _woodMetricsDict.Count)
{
foreach (var entry in _woodMetricsDict.Where(entry => entry.Value <= taskParam.WoodDailyMaxCount))
{
UpdateWoodCount(entry.Key, entry.Value);
}
}
else
{
foreach (Match match in matches)
{
if (match.Success)
{
var materialName = match.Groups[1].Value.Trim();
var quantityStr = match.Groups[2].Value.Trim();
var quantity = int.Parse(quantityStr);
Debug.WriteLine($"首次获取木材的名称:{materialName}, 数量:{quantity}");
UpdateWoodCount(materialName, quantity);
}
else
{
Logger.LogWarning("识别到的数量不是有效的整数:{woodText}", text);
}
}
// 所有数据都保存一遍后,首次OCR识别结束
_firstWoodOcr = false;
}
}
private void UpdateWoodCount(string materialName, int quantity)
{
// 检查字典中是否已包含这种木材名称
if (!ExistWoods.Contains(materialName))
{
Logger.LogWarning("未知的木材名:{woodName},数量{Cnt}", materialName, quantity);
return;
}
WoodTotalDict.AddOrUpdate(
key: materialName,
addValue: quantity,
updateValueFactory: (_, existingValue) => existingValue + quantity
);
if (_firstWoodOcr)
{
// 记录木材单次获取的值
_woodMetricsDict.TryAdd(materialName, quantity);
}
}
private static string FindBestOcrResult(List firstOcrResultList)
{
// return firstOcrResultList.Count == 0 ? "" : firstOcrResultList.OrderByDescending(s => s.Length).First();
if (firstOcrResultList.Count == 0) return "";
// 先排序再查找
var sortedOcrResults = firstOcrResultList.OrderByDescending(s => s.Length).ToList();
int? targetLength = null;
foreach (var ocrResult in sortedOcrResults)
{
if (targetLength == null)
{
targetLength = ocrResult.Length;
}
else if (ocrResult.Length != targetLength)
{
// 如果当前结果长度与第一个匹配项的长度不同,则跳过
continue;
}
// 分解 OCR 结果中的多个条目
var matches = _parseWoodStatisticsRegex().Matches(ocrResult);
var isFound = true;
foreach (Match match in matches)
{
if (!match.Success)
{
isFound = false;
continue;
}
var materialName = match.Groups[1].Value.Trim();
Debug.WriteLine($"第一次获取的木材名称:{materialName}");
if (!ExistWoods.Contains(materialName))
{
isFound = false;
}
}
if (isFound) return ocrResult;
}
// 如果没有找到匹配的结果
return "";
}
private void CheckAndPrintWoodQuantities(WoodTaskParam taskParam)
{
if (WoodTotalDict.IsEmpty)
{
ReachedWoodMaxCount = false;
NothingCount++;
return;
}
foreach (var entry in WoodTotalDict)
{
if (_woodNotPrintDict.GetValueOrDefault(entry.Key)) continue;
// 打印每个条目的键(木材名称)和值(数量)
Logger.LogInformation("木材{woodName}累积获取数量:{Cnt}", entry.Key, entry.Value);
// 检查木材是否超过上限
if (entry.Value < taskParam.WoodDailyMaxCount) continue;
Logger.LogInformation("木材{Name}已达到数量设置的上限:{Count}", entry.Key, taskParam.WoodDailyMaxCount);
_woodNotPrintDict.TryAdd(entry.Key, true);
}
// 如果木材统计的最小值都大于设置的上限,则停止伐木
ReachedWoodMaxCount = WoodTotalDict.Values.Min() >= taskParam.WoodDailyMaxCount;
}
}
private void Felling(WoodTaskParam taskParam, bool isLast = false)
{
// 1. 按 z 触发「王树瑞佑」
PressZ(taskParam);
if (isLast)
{
return;
}
// 打印伐木的统计数据(可选)
if (TaskContext.Instance().Config.AutoWoodConfig.WoodCountOcrEnabled)
{
_printer.PrintWoodStatistics(taskParam);
if (_printer.WoodStatisticsAlwaysEmpty() || _printer.ReachedWoodMaxCount) return;
}
// 2. 按下 ESC 打开菜单 并退出游戏
PressEsc(taskParam);
// 3. 等待进入游戏
EnterGame(taskParam);
// 手动 GC
GC.Collect();
}
private void PressZ(WoodTaskParam taskParam)
{
// IMPORTANT: MUST try focus before press Z
SystemControl.FocusWindow(TaskContext.Instance().GameHandle);
if (_first)
{
using var contentRegion = CaptureToRectArea();
using var ra = contentRegion.Find(_assets.TheBoonOfTheElderTreeRo);
if (ra.IsEmpty())
{
#if !TEST_WITHOUT_Z_ITEM
throw new NormalEndException("请先装备小道具「王树瑞佑」!");
#else
System.Threading.Thread.Sleep(2000);
Simulation.SendInput.Keyboard.KeyPress(_zKey);
Debug.WriteLine("[AutoWood] Z");
_first = false;
#endif
}
else
{
Simulation.SendInput.Keyboard.KeyPress(_zKey);
Debug.WriteLine("[AutoWood] Z");
_first = false;
}
}
else
{
NewRetry.Do(() =>
{
Sleep(1, _ct);
using var contentRegion = CaptureToRectArea();
using var ra = contentRegion.Find(_assets.TheBoonOfTheElderTreeRo);
if (ra.IsEmpty())
{
#if !TEST_WITHOUT_Z_ITEM
throw new RetryException("未找到「王树瑞佑」");
#else
System.Threading.Thread.Sleep(15000);
#endif
}
Simulation.SendInput.Keyboard.KeyPress(_zKey);
Debug.WriteLine("[AutoWood] Z");
Sleep(500, _ct);
}, TimeSpan.FromSeconds(1), 120);
}
Sleep(300, _ct);
Sleep(TaskContext.Instance().Config.AutoWoodConfig.AfterZSleepDelay, _ct);
}
private void PressEsc(WoodTaskParam taskParam)
{
SystemControl.FocusWindow(TaskContext.Instance().GameHandle);
Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE);
// if (TaskContext.Instance().Config.AutoWoodConfig.PressTwoEscEnabled)
// {
// Sleep(1500, _cts);
// Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE);
// }
Debug.WriteLine("[AutoWood] Esc");
Sleep(800, _ct);
// 确认在菜单界面
try
{
NewRetry.Do(() =>
{
Sleep(1, _ct);
using var contentRegion = CaptureToRectArea();
using var ra = contentRegion.Find(_assets.MenuBagRo);
if (ra.IsEmpty())
{
Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE);
throw new RetryException("未检测到弹出菜单");
}
}, TimeSpan.FromSeconds(1.2), 5);
}
catch (Exception e)
{
Logger.LogInformation(e.Message);
Logger.LogInformation("仍旧点击退出按钮");
}
// 点击退出
GameCaptureRegion.GameRegionClick((size, scale) => (50 * scale, size.Height - 50 * scale));
Debug.WriteLine("[AutoWood] Click exit button");
Sleep(500, _ct);
// 点击确认
using var contentRegion = CaptureToRectArea();
contentRegion.Find(_assets.ConfirmRo, ra =>
{
ra.Click();
Debug.WriteLine("[AutoWood] Click confirm button");
ra.Dispose();
});
}
private void EnterGame(WoodTaskParam taskParam)
{
if (_login3rdParty.IsAvailabled)
{
Sleep(1, _ct);
_login3rdParty.Login(_ct);
}
var clickCnt = 0;
for (var i = 0; i < 50; i++)
{
Sleep(1, _ct);
using var contentRegion = CaptureToRectArea();
using var ra = contentRegion.Find(_assets.EnterGameRo);
if (!ra.IsEmpty())
{
clickCnt++;
GameCaptureRegion.GameRegion1080PPosClick(955, 666);
Debug.WriteLine("[AutoWood] Click entry");
}
else
{
if (clickCnt > 2)
{
Sleep(5000, _ct);
break;
}
}
Sleep(1000, _ct);
}
if (clickCnt == 0)
{
throw new RetryException("未检测进入游戏界面");
}
}
}