diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index 83b47329..2e3bed8a 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -158,6 +158,9 @@ Always + + Always + Always @@ -171,6 +174,7 @@ + diff --git a/BetterGenshinImpact/Core/BgiVision/BvImage.cs b/BetterGenshinImpact/Core/BgiVision/BvImage.cs new file mode 100644 index 00000000..c01a05b0 --- /dev/null +++ b/BetterGenshinImpact/Core/BgiVision/BvImage.cs @@ -0,0 +1,31 @@ +using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.GameTask; +using OpenCvSharp; + +namespace BetterGenshinImpact.Core.BgiVision; + +public class BvImage +{ + public RecognitionObject RecognitionObject { get; } + + public BvImage(string templateAssert, Rect roi = default, double threshold = 0.8) + { + var args = templateAssert.Split(':'); + var featureName = args[0]; + var assertName = args[1]; + + RecognitionObject = new RecognitionObject + { + Name = templateAssert, + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(featureName, assertName), + RegionOfInterest = roi, + Threshold = threshold + }.InitTemplate(); + } + + public RecognitionObject ToRecognitionObject() + { + return RecognitionObject; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/BgiVision/BvLocator.cs b/BetterGenshinImpact/Core/BgiVision/BvLocator.cs new file mode 100644 index 00000000..d17f2cd2 --- /dev/null +++ b/BetterGenshinImpact/Core/BgiVision/BvLocator.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.GameTask.Common; +using BetterGenshinImpact.GameTask.Model.Area; +using Microsoft.Extensions.Logging; +using OpenCvSharp; +using static BetterGenshinImpact.GameTask.Common.TaskControl; + + +namespace BetterGenshinImpact.Core.BgiVision; + +/// +/// 针对 Region 体系的包装 +/// +public class BvLocator +{ + private static readonly ILogger Logger = App.GetLogger(); + private readonly CancellationToken _cancellationToken; + + public RecognitionObject RecognitionObject { get; } + + public Action? RetryAction { get; set; } + + public static int DefaultTimeout { get; set; } = 10000; + + public static int DefaultRetryInterval { get; set; } = 250; + + public BvLocator(RecognitionObject recognitionObject, CancellationToken cancellationToken) + { + RecognitionObject = recognitionObject; + _cancellationToken = cancellationToken; + } + + /// + /// 根据传入的位置信息定位元素 + /// 不建议外部调用使用 + /// + /// + /// + public List FindAll() + { + using var screen = CaptureToRectArea(); + + if (RecognitionObject.RecognitionType == RecognitionTypes.TemplateMatch) + { + var region = screen.Find(RecognitionObject); + if (region.IsExist()) + { + return [region]; + } + + return []; + } + else if (RecognitionObject.RecognitionType == RecognitionTypes.Ocr) + { + var results = screen.FindMulti(RecognitionObject); + if (!string.IsNullOrEmpty(RecognitionObject.Text)) + { + return results.FindAll(r => r.Text.Contains(RecognitionObject.Text)); + } + + return results; + } + else + { + throw new NotSupportedException($"不被 Locator 支持的识别类型: {RecognitionObject.RecognitionType}"); + } + } + + public bool IsExist() + { + return FindAll().Count > 0; + } + + public async Task Click(int? timeout = null) + { + return (await WaitFor(timeout)).First().Click(); + } + + public async Task DoubleClick(int? timeout = null) + { + var list = await WaitFor(timeout); + return list.First().DoubleClick(); + } + + public async Task> WaitFor(int? timeout = null) + { + var actualTimeout = timeout ?? DefaultTimeout; + var retryCount = actualTimeout / DefaultRetryInterval; + + List results = []; + var retryRes = await NewRetry.WaitForAction(() => + { + RetryAction?.Invoke(); + results = FindAll(); + return results.Count > 0; + }, _cancellationToken, retryCount, DefaultRetryInterval); + + if (retryRes) + { + return results; + } + else + { + throw new TimeoutException($"识别元素在 {actualTimeout}ms 后超时未出现!"); + } + } + + public async Task> TryWaitFor(int? timeout = null) + { + try + { + return await WaitFor(timeout); + } + catch + { + return []; + } + } + + public async Task WaitForDisappear(int? timeout = null) + { + var actualTimeout = timeout ?? DefaultTimeout; + var retryCount = actualTimeout / DefaultRetryInterval; + + var retryRes = await NewRetry.WaitForAction(() => + { + RetryAction?.Invoke(); + var results = FindAll(); + return results.Count == 0; + }, _cancellationToken, retryCount, DefaultRetryInterval); + + if (!retryRes) + { + throw new TimeoutException($"识别元素在 {actualTimeout}ms 后超时未消失!"); + } + } + + public async Task TryWaitForDisappear(int? timeout = null) + { + try + { + await WaitForDisappear(timeout); + } + catch + { + // ignored + } + } + + /// + /// 方便优雅的设置感兴趣区域 (ROI) + /// 该方法会覆盖 RecognitionObject.RegionOfInterest 的值。 + /// + /// + /// + public BvLocator WithRoi(Rect rect) + { + RecognitionObject.RegionOfInterest = rect; + return this; + } + + public BvLocator WithRetryAction(Action? action) + { + RetryAction = action; + return this; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/BgiVision/BvPage.cs b/BetterGenshinImpact/Core/BgiVision/BvPage.cs new file mode 100644 index 00000000..950de111 --- /dev/null +++ b/BetterGenshinImpact/Core/BgiVision/BvPage.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.Core.Simulator; +using BetterGenshinImpact.GameTask.Common; +using BetterGenshinImpact.GameTask.Model.Area; +using Fischless.WindowsInput; +using Microsoft.Extensions.Logging; +using OpenCvSharp; + +namespace BetterGenshinImpact.Core.BgiVision; + +public class BvPage +{ + private static readonly ILogger Logger = App.GetLogger(); + private readonly CancellationToken _cancellationToken; + + public IKeyboardSimulator Keyboard => Simulation.SendInput.Keyboard; + + public IMouseSimulator Mouse => Simulation.SendInput.Mouse; + + /// + /// Default timeout for operations in milliseconds + /// + public int DefaultTimeout { get; set; } = 10000; + + /// + /// Default retry interval in milliseconds + /// + public int DefaultRetryInterval { get; set; } = 1000; + + public BvPage(CancellationToken cancellationToken = default) + { + _cancellationToken = cancellationToken; + } + + /// + /// 截图 + /// + /// + public ImageRegion Screenshot() + { + return TaskControl.CaptureToRectArea(); + } + + /// + /// 等待 + /// + /// + /// + public async Task Wait(int milliseconds) + { + await TaskControl.Delay(milliseconds, _cancellationToken); + return this; + } + + /// + /// 定位图片位置 + /// + /// + /// + public BvLocator Locator(BvImage image) + { + return new BvLocator(image.ToRecognitionObject(), _cancellationToken); + } + + /// + /// 定位文本位置 + /// + /// + /// + /// + public BvLocator Locator(string text, Rect rect = default) + { + return Locator(new RecognitionObject + { + RecognitionType = RecognitionTypes.Ocr, + RegionOfInterest = rect, + Text = text + }); + } + + + /// + /// 定位 RecognitionObject 代表的位置 + /// + /// + /// + public BvLocator Locator(RecognitionObject ro) + { + return new BvLocator(ro, _cancellationToken); + } + + public BvLocator GetByText(string text = "", Rect rect = default) + { + return Locator(text, rect); + } + + public BvLocator GetByImage(BvImage image) + { + return Locator(image); + } + + + public List Ocr(Rect rect = default) + { + return Locator(string.Empty, rect).FindAll(); + } + + + /// + /// 1080P 分辨率下点击坐标 + /// + /// + /// + public void Click(double x, double y) + { + GameCaptureRegion.GameRegion1080PPosClick(x, y); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs b/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs index 7edb6b85..4bed5363 100644 --- a/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs +++ b/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs @@ -167,24 +167,29 @@ public class RecognitionObject public Dictionary ReplaceDictionary { get; set; } = []; /// - /// 包含匹配 + /// 包含匹配 (用于单个确认是否存在) /// 多个值全匹配的情况下才算成功 /// 复杂情况请用下面的正则匹配 /// public List AllContainMatchText { get; set; } = []; /// - /// 包含匹配 + /// 包含匹配(用于单个确认是否存在) /// 一个值匹配就算成功 /// public List OneContainMatchText { get; set; } = []; /// - /// 正则匹配 + /// 正则匹配(用于单个确认是否存在) /// 多个值全匹配的情况下才算成功 /// public List RegexMatchText { get; set; } = []; + /// + /// 用于多个OCR结果的匹配 + /// + public string Text { get; set; } = string.Empty; + public static RecognitionObject Ocr(double x, double y, double w, double h) { return new RecognitionObject diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index e9dad71b..5fb9747b 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -407,16 +407,12 @@ public class ScriptRepoUpdater : Singleton ZipFile.ExtractToDirectory(zipPath, ReposPath, true); } - public async Task ImportScriptFromClipboard() + public async Task ImportScriptFromClipboard( string clipboardText ) { // 获取剪切板内容 try { - if (Clipboard.ContainsText()) - { - string clipboardText = Clipboard.GetText(); - await ImportScriptFromUri(clipboardText, true); - } + await ImportScriptFromUri(clipboardText, true); } catch (Exception e) { diff --git a/BetterGenshinImpact/GameTask/Model/Area/Region.cs b/BetterGenshinImpact/GameTask/Model/Area/Region.cs index 036d0d46..c0fb6fbd 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/Region.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/Region.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Drawing; using System.Threading; +using BetterGenshinImpact.GameTask.Common; using Vanara.PInvoke; namespace BetterGenshinImpact.GameTask.Model.Area; @@ -100,10 +101,20 @@ public class Region : IDisposable /// 点击【自己】的中心 /// region.Derive(x,y).Click() 等效于 region.ClickTo(x,y) /// - public void Click() + public Region Click() { // 相对自己是 0, 0 坐标 ClickTo(0, 0, Width, Height); + return this; + } + + public Region DoubleClick() + { + // 相对自己是 0, 0 坐标 + ClickTo(0, 0, Width, Height); + TaskControl.Sleep(60); + ClickTo(0, 0, Width, Height); + return this; } /// diff --git a/BetterGenshinImpact/GameTask/UseActiveCode/UseActiveCodeTask.cs b/BetterGenshinImpact/GameTask/UseActiveCode/UseActiveCodeTask.cs deleted file mode 100644 index 9f813fa9..00000000 --- a/BetterGenshinImpact/GameTask/UseActiveCode/UseActiveCodeTask.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BetterGenshinImpact.GameTask.UseActiveCode -{ - internal class UseActiveCodeTask - { - } -} diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/Assets/1920x1080/esc_return_button.png b/BetterGenshinImpact/GameTask/UseRedeemCode/Assets/1920x1080/esc_return_button.png new file mode 100644 index 00000000..d9d732b8 Binary files /dev/null and b/BetterGenshinImpact/GameTask/UseRedeemCode/Assets/1920x1080/esc_return_button.png differ diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/GetLiveRedeemCode.cs b/BetterGenshinImpact/GameTask/UseRedeemCode/GetLiveRedeemCode.cs new file mode 100644 index 00000000..32facc77 --- /dev/null +++ b/BetterGenshinImpact/GameTask/UseRedeemCode/GetLiveRedeemCode.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using BetterGenshinImpact.GameTask.UseRedeemCode.Model; +using BetterGenshinImpact.Helpers.Http; +using Newtonsoft.Json.Linq; + +namespace BetterGenshinImpact.GameTask.UseRedeemCode; + +/// +/// 获取直播的前瞻兑换码 +/// +public class GetLiveRedeemCode +{ + private static readonly string BBS_URL = "https://bbs-api.mihoyo.com"; + private readonly HttpClient _httpClient = HttpClientFactory.GetCommonSendClient(); + + private readonly Dictionary _url = new() + { + { "act_id_1", $"{BBS_URL}/painter/api/user_instant/list?offset=0&size=20&uid=75276539" }, + { "act_id_2", $"{BBS_URL}/painter/api/user_instant/list?offset=0&size=20&uid=75276550" }, + { "index", "https://api-takumi.mihoyo.com/event/miyolive/index" }, + { "code", "https://api-takumi-static.mihoyo.com/event/miyolive/refreshCode" } + }; + + private async Task GetDataAsync(string type, Dictionary? data = null) + { + try + { + HttpResponseMessage res; + var request = new HttpRequestMessage(HttpMethod.Get, _url[type]); + + // 为所有需要的请求添加 act_id header + if ((type == "index" || type == "code") && data != null && data.TryGetValue("actId", out var actId)) + { + request.Headers.Add("x-rpc-act_id", actId); + } + + // 为code类型添加查询参数 + if (type == "code" && data != null) + { + var uriBuilder = new UriBuilder(_url[type]); + var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query); + query["version"] = data.GetValueOrDefault("version", ""); + query["time"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); + uriBuilder.Query = query.ToString(); + request.RequestUri = uriBuilder.Uri; + } + + res = await _httpClient.SendAsync(request); + var content = await res.Content.ReadAsStringAsync(); + return JObject.Parse(content); + } + catch (Exception e) + { + return JObject.Parse($"{{\"error\":\"[{e.GetType().Name}] {type} 接口请求错误\",\"retcode\":1}}"); + } + } + + private async Task GetActIdAsync(string id) + { + var ret = await GetDataAsync($"act_id_{id}"); + if (ret == null) return ""; + + // 检查error或retcode != 0 + if (ret["error"] != null || ret["retcode"]?.Value() != 0) + return ""; + + string actId = ""; + var keywords = new List { "前瞻特别节目" }; + + var list = ret["data"]?["list"] as JArray; + if (list == null) return ""; + + foreach (var p in list) + { + var post = p["post"]?["post"]; + if (post == null) continue; + + var subject = post["subject"]?.Value(); + if (string.IsNullOrEmpty(subject)) continue; + + bool containsAll = keywords.All(word => subject.Contains(word)); + if (!containsAll) continue; + + var structContent = post["structured_content"]?.Value(); + if (string.IsNullOrEmpty(structContent)) continue; + + var segments = JArray.Parse(structContent); + foreach (var segment in segments) + { + var link = segment["attributes"]?["link"]?.Value() ?? ""; + // 优化:安全获取insert字段值 + string insert = ""; + var insertToken = segment["insert"]; + if (insertToken != null) + { + if (insertToken.Type == JTokenType.String) + { + insert = insertToken.Value() ?? ""; + } + else if (insertToken.Type == JTokenType.Object) + { + // 如果是对象,尝试转换为字符串 + insert = insertToken.ToString(); + } + } + if ((insert.Contains("观看") || insert.Contains("米游社直播间")) && !string.IsNullOrEmpty(link)) + { + var match = Regex.Match(link, @"act_id=(.*?)\&"); + if (match.Success) + actId = match.Groups[1].Value; + } + } + + if (!string.IsNullOrEmpty(actId)) break; + } + + return actId; + } + + private async Task<(string codeVer, string title)> GetLiveDataAsync(string actId) + { + var ret = await GetDataAsync("index", new Dictionary { { "actId", actId } }); + if (ret == null || ret["error"] != null || ret["retcode"]?.Value() != 0) + return (null, null); + + var liveRaw = ret["data"]?["live"]; + if (liveRaw == null) return (null, null); + + string codeVer = liveRaw["code_ver"]?.Value(); + string title = liveRaw["title"]?.Value(); + return (codeVer, title); + } + + private async Task> GetCodeAsync(string version, string actId) + { + var ret = await GetDataAsync("code", new Dictionary { { "version", version }, { "actId", actId } }); + var result = new List(); + if (ret == null || ret["error"] != null || ret["retcode"]?.Value() != 0) + return result; + + var removeTag = new Regex("<.*?>", RegexOptions.Compiled); + var codeList = ret["data"]?["code_list"] as JArray; + if (codeList == null) return result; + + foreach (var codeInfo in codeList) + { + string items = removeTag.Replace(codeInfo["title"]?.Value() ?? "", ""); + string code = codeInfo["code"]?.Value(); + if (!string.IsNullOrEmpty(code)) + result.Add(new RedeemCode(code, items)); + } + + return result; + } + + /// + /// 获取前瞻直播兑换码信息。返回格式:List[("奖励内容", "兑换码")] + /// + public async Task> GetCodeMsgAsync() + { + string actId = await GetActIdAsync("1"); + if (string.IsNullOrEmpty(actId)) + { + actId = await GetActIdAsync("2"); + if (string.IsNullOrEmpty(actId)) + throw new Exception("暂无前瞻直播资讯!"); + } + + var (codeVer, title) = await GetLiveDataAsync(actId); + if (string.IsNullOrEmpty(codeVer)) + throw new Exception("前瞻直播数据异常"); + var codeList = await GetCodeAsync(codeVer, actId); + return codeList; + } +} diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/Model/RedeemCode.cs b/BetterGenshinImpact/GameTask/UseRedeemCode/Model/RedeemCode.cs new file mode 100644 index 00000000..59387ec6 --- /dev/null +++ b/BetterGenshinImpact/GameTask/UseRedeemCode/Model/RedeemCode.cs @@ -0,0 +1,14 @@ +namespace BetterGenshinImpact.GameTask.UseRedeemCode.Model; + +public class RedeemCode +{ + public string Code { get; set; } + + public string? Items { get; set; } + + public RedeemCode(string code, string? items) + { + Code = code; + Items = items; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs b/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs new file mode 100644 index 00000000..6a43e593 --- /dev/null +++ b/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using BetterGenshinImpact.View.Windows; +using TextBox = Wpf.Ui.Controls.TextBox; + +namespace BetterGenshinImpact.GameTask.UseRedeemCode; + +public class RedeemCodeManager +{ + public static async Task ImportFromClipboard(string clipboardText) + { + var codes = ExtractAllCodes(clipboardText); + if (codes.Count == 0) + { + return; + } + + var multilineTextBox = new TextBox + { + TextWrapping = TextWrapping.Wrap, + Height = 340, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Text = codes.Aggregate("", (current, code) => current + $"{code}\n"), + }; + var p = new PromptDialog( + "从剪切版中获取到下面的兑换码,是否自动使用?", + "自动使用兑换码", + multilineTextBox, + null); + p.Height = 500; + p.ShowDialog(); + + if (p.DialogResult != true) + { + return; + } + + Clipboard.Clear(); + await new TaskRunner().RunSoloTaskAsync(new UseRedemptionCodeTask(codes)); + } + + public static List ExtractAllCodes(string clipboardText) + { + if (string.IsNullOrEmpty(clipboardText)) + { + return []; + } + + var regex = new Regex(@"(? m.Value) + .ToList(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/UseRedemptionCodeTask.cs b/BetterGenshinImpact/GameTask/UseRedeemCode/UseRedemptionCodeTask.cs new file mode 100644 index 00000000..9e35db8b --- /dev/null +++ b/BetterGenshinImpact/GameTask/UseRedeemCode/UseRedemptionCodeTask.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using BetterGenshinImpact.Core.BgiVision; +using BetterGenshinImpact.GameTask.Common.BgiVision; +using BetterGenshinImpact.GameTask.Common.Element.Assets; +using BetterGenshinImpact.GameTask.Common.Job; +using BetterGenshinImpact.GameTask.UseRedeemCode.Model; +using BetterGenshinImpact.Helpers; +using BetterGenshinImpact.Helpers.Extensions; +using Microsoft.Extensions.Logging; +using Vanara.PInvoke; +using Rect = OpenCvSharp.Rect; + +namespace BetterGenshinImpact.GameTask.UseRedeemCode; + +public class UseRedemptionCodeTask : ISoloTask +{ + private static readonly ILogger _logger = App.GetLogger(); + + + private readonly List _list; + + public UseRedemptionCodeTask(List list) + { + this._list = list; + } + + public UseRedemptionCodeTask(List strList) + { + _list = strList + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => new RedeemCode(code, null)) + .ToList(); + } + + public string Name => "使用兑换码"; + + public async Task Start(CancellationToken ct) + { + InitLog(_list); + + try + { + Rect captureRect = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; + + await new ReturnMainUiTask().Start(ct); + + var page = new BvPage(ct); + + _logger.LogInformation("使用兑换码: {Msg}", "打开设置"); + // 按ESC键打开菜单 + page.Keyboard.KeyPress(User32.VK.VK_ESCAPE); + // 等待ESC后菜单出现 + await page.Locator(new BvImage("UseRedeemCode:esc_return_button.png")).WaitFor(); + // 点击设置按钮 + page.Click(45, 825); + await page.Wait(1000); + + // 点击账户 + _logger.LogInformation("使用兑换码: {Msg}", "点击账户 —— 前往兑换"); + await page.GetByText("账户").WithRoi(captureRect.CutLeft(0.2)).Click(); + await page.Wait(300); + + // 点击前往兑换 + await page.GetByText("前往兑换").WithRoi(captureRect.CutRight(0.3)).Click(); + + // 等待兑换码输入框出现 + await page.GetByText("兑换奖励").WaitFor(); + + + foreach (var redeemCode in _list) + { + if (string.IsNullOrEmpty(redeemCode.Code)) + { + continue; + } + + await UseRedeemCode(redeemCode, page); + } + } + catch (Exception ex) + { + _logger.LogError("使用兑换码时发生错误: {Message}", ex.Message); + _logger.LogDebug(ex, "使用兑换码时发生错误"); + } + finally + { + // 清空剪贴板 + UIDispatcherHelper.Invoke(Clipboard.Clear); + // 返回主界面 + await new ReturnMainUiTask().Start(ct); + + } + } + + private async Task UseRedeemCode(RedeemCode redeemCode, BvPage page) + { + Rect captureRect = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; + + _logger.LogInformation("输入兑换码: {Code}", redeemCode.Code); + // 将要输入的文本复制到剪贴板 + UIDispatcherHelper.Invoke(() => Clipboard.SetDataObject(redeemCode.Code!)); + // 粘贴兑换码 + await page.GetByText("粘贴").WithRoi(captureRect.CutRight(0.5)).Click(); + // 点击兑换 + await page.Locator(ElementAssets.Instance.BtnWhiteConfirm).Click(); + + // 兑换成功 + var list = await page.GetByText("兑换成功").TryWaitFor(1000); + if (list.Count > 0) + { + _logger.LogInformation("兑换码 {Code} 兑换成功", redeemCode.Code); + // 点击确认 + await page.Locator(ElementAssets.Instance.BtnBlackConfirm).Click(); + await page.Wait(5100); + } + else + { + _logger.LogWarning("兑换码 {Code} 兑换失败,可能是过期、错误或已被使用", redeemCode.Code); + // 点击清除 + await page.GetByText("清除").WithRoi(captureRect.CutRight(0.5)).Click(); + } + } + + + private static void InitLog(List list) + { + _logger.LogInformation("开始使用兑换码:"); + foreach (var redeemCode in list) + { + if (string.IsNullOrEmpty(redeemCode.Items)) + { + _logger.LogInformation("{Code}", redeemCode.Code); + } + else + { + _logger.LogInformation("{Code} - {Msg}", redeemCode.Code, redeemCode.Items); + } + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Helpers/Http/HttpClientFactory.cs b/BetterGenshinImpact/Helpers/Http/HttpClientFactory.cs new file mode 100644 index 00000000..f8dee59c --- /dev/null +++ b/BetterGenshinImpact/Helpers/Http/HttpClientFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Concurrent; +using System.Net.Http; + +namespace BetterGenshinImpact.Helpers.Http; + +public class HttpClientFactory +{ + private static readonly ConcurrentDictionary Clients = new(); + + public static HttpClient GetClient(string key, Func factory) + { + return Clients.GetOrAdd(key, _ => factory()); + } + + public static HttpClient GetCommonSendClient() + { + return Clients.GetOrAdd("common", _ => new HttpClient()); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml b/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml index c869a701..e5ec95fa 100644 --- a/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml +++ b/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml @@ -677,7 +677,7 @@ + BorderThickness="0,1,0,0" /> - - + + - - + + + TextWrapping="Wrap" /> + TextWrapping="Wrap" /> + Text="{Binding Config.AutoFightConfig.PickDropsAfterFightSeconds}" /> @@ -963,10 +963,10 @@ Text="优先使用浓缩树脂,然后使用原粹树脂,其余树脂不使用" TextWrapping="Wrap" /> + Grid.RowSpan="2" + Grid.Column="1" + Margin="0,0,36,0" + IsChecked="{Binding Config.AutoDomainConfig.SpecifyResinUse,Converter={StaticResource InverseBooleanConverter}, Mode=TwoWay}" /> @@ -1011,12 +1011,13 @@ - + @@ -1025,12 +1026,13 @@ - + @@ -1039,12 +1041,13 @@ - + @@ -1053,12 +1056,13 @@ - + @@ -1367,10 +1371,10 @@ Text="优先使用浓缩树脂,然后使用原粹树脂,其余树脂不使用" TextWrapping="Wrap" /> + Grid.RowSpan="2" + Grid.Column="1" + Margin="0,0,36,0" + IsChecked="{Binding Config.AutoStygianOnslaughtConfig.SpecifyResinUse,Converter={StaticResource InverseBooleanConverter}, Mode=TwoWay}" /> @@ -1415,12 +1419,13 @@ - + @@ -1429,12 +1434,13 @@ - + @@ -1443,12 +1449,13 @@ - + @@ -1457,12 +1464,13 @@ - + @@ -1580,45 +1588,9 @@ --> - - - - - - - - - - - 进入演奏界面使用,下落模式必须选择垂落模式 - - - 点击查看使用教程 - - - - - - - - - - - + @@ -1632,27 +1604,18 @@ - 进入专辑界面使用,自动演奏未完成乐曲 - + 可以自动演奏单个,也可以全自动完成整个专辑 - 点击查看使用教程 - @@ -1669,7 +1632,75 @@ + + 进入演奏界面使用,下落模式必须选择垂落模式 - + + 点击查看使用教程 + + + + + + + + + + + + + + + + 进入专辑界面使用,自动演奏未完成乐曲 - + + 点击查看使用教程 + + + + + + + + + + + + + + + TextWrapping="Wrap"> 请 + Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}"> 下载 - 到本地后填入torch_cpu.dll或torch_cuda.dll的完整地址。如未生效可尝试重启BGI。 + + 到本地后填入torch_cpu.dll或torch_cuda.dll的完整地址。如未生效可尝试重启BGI。 + + + + + + + + + + + + + + + + + + + + + + 自动使用输入的兑换码 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 请 + + 下载 + + 到本地后填入torch_cpu.dll或torch_cuda.dll的完整地址。如未生效可尝试重启BGI。 + Grid.RowSpan="2" + Grid.Column="1" + MinWidth="90" + Margin="0,0,36,0" + Value="{Binding Config.AutoArtifactSalvageConfig.MaxNumToCheck, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" + Text="{Binding Config.AutoArtifactSalvageConfig.MaxNumToCheck, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> - @@ -2097,7 +2315,7 @@ - + log/gridIcons - + 以下过长的内容在pr时会搬到教程里去 - + 需要漆黑的背景以降低干扰,比如渊下宫-蛇肠之路的一个锚点,将视角竖直向上看向洞顶 - + 诸如提纳里的耳朵太长了,他装备的物品角标目前无法正确地和正上方的物品图标进行轮廓分割,请手动规避 + Grid.RowSpan="2" + Grid.Column="1" + MinWidth="90" + Margin="0,0,36,0" + Value="{Binding Config.GetGridIconsConfig.MaxNumToGet, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" + Text="{Binding Config.GetGridIconsConfig.MaxNumToGet, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> diff --git a/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs b/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs index 5fb97c8a..2a922a1e 100644 --- a/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs +++ b/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs @@ -28,7 +28,7 @@ public partial class PromptDialog { private readonly PromptDialogConfig _config; - public PromptDialog(string question, string title, UIElement uiElement, string defaultValue, PromptDialogConfig? config = null) + public PromptDialog(string question, string title, UIElement uiElement, string? defaultValue, PromptDialogConfig? config = null) { InitializeComponent(); MyTitleBar.Title = title; @@ -36,11 +36,11 @@ public partial class PromptDialog _config = config ?? new PromptDialogConfig(); DynamicContent.Content = uiElement; - if (DynamicContent.Content is TextBox textBox) + if (DynamicContent.Content is TextBox textBox && defaultValue != null) { textBox.Text = defaultValue; } - else if (DynamicContent.Content is ComboBox comboBox) + else if (DynamicContent.Content is ComboBox comboBox && defaultValue != null) { comboBox.Text = defaultValue; } @@ -126,4 +126,4 @@ public partial class PromptDialog { Close(); } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs index 97ab617d..63b08c47 100644 --- a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs @@ -23,6 +23,7 @@ using System.Net.Http.Json; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; +using BetterGenshinImpact.GameTask.UseRedeemCode; using BetterGenshinImpact.View.Windows; using BetterGenshinImpact.ViewModel.Pages; using DeviceId; @@ -37,17 +38,15 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel private readonly IConfigService _configService; public string Title => $"BetterGI · 更好的原神 · {Global.Version}{(RuntimeHelper.IsDebug ? " · Dev" : string.Empty)}"; - [ObservableProperty] - private bool _isVisible = true; + [ObservableProperty] private bool _isVisible = true; - [ObservableProperty] - private WindowState _windowState = WindowState.Normal; + [ObservableProperty] private WindowState _windowState = WindowState.Normal; - [ObservableProperty] - private WindowBackdropType _currentBackdropType = WindowBackdropType.Auto; + [ObservableProperty] private WindowBackdropType _currentBackdropType = WindowBackdropType.Auto; - [ObservableProperty] - private bool _isWin11Later = OsVersionHelper.IsWindows11_OrGreater; + [ObservableProperty] private bool _isWin11Later = OsVersionHelper.IsWindows11_OrGreater; + + private bool _firstActivated = true; public AllConfig Config { get; set; } @@ -61,7 +60,37 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel [RelayCommand] private async Task OnActivated() { - await ScriptRepoUpdater.Instance.ImportScriptFromClipboard(); + // 首次激活时不处理 + if (_firstActivated) + { + _firstActivated = false; + return; + } + + // 激活时候获取剪切板内容 用于脚本导入、兑换码自动兑换等 + try + { + if (Clipboard.ContainsText()) + { + string clipboardText = Clipboard.GetText(); + + if (string.IsNullOrEmpty(clipboardText) + || clipboardText.Length > 1000) + { + return; + } + + + // 1. 导入脚本 + await ScriptRepoUpdater.Instance.ImportScriptFromClipboard(clipboardText); + // 2. 自动兑换码 + await RedeemCodeManager.ImportFromClipboard(clipboardText); + } + } + catch + { + // 忽略异常,可能是因为没有权限访问剪切板 + } } [RelayCommand] diff --git a/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs index bf97357c..d229ece3 100644 --- a/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs @@ -34,6 +34,7 @@ using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.QuickTeleport.Assets; +using BetterGenshinImpact.GameTask.UseRedeemCode; using BetterGenshinImpact.View; using OpenCvSharp; using Vanara.PInvoke; @@ -581,8 +582,6 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel Config.HotKeyConfig.Test1HotkeyType, (_, _) => { - LowerHeadThenWalkToTask _lowerHeadThenWalkToTask = new("chest_tip.png", 20000); - _lowerHeadThenWalkToTask.Start(CancellationToken.None); } )); debugDirectory.Children.Add(new HotKeySettingModel( diff --git a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs index 8df36076..34057000 100644 --- a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs @@ -25,18 +25,21 @@ using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.Helpers; using Wpf.Ui; -using Wpf.Ui.Controls; using Wpf.Ui.Violeta.Controls; using BetterGenshinImpact.ViewModel.Pages.View; using System.Linq; using System.Reflection; using System.Collections.Frozen; using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.AutoStygianOnslaught; using BetterGenshinImpact.View.Windows; using BetterGenshinImpact.GameTask.GetGridIcons; using BetterGenshinImpact.GameTask.Model.GameUI; +using BetterGenshinImpact.GameTask.UseRedeemCode; +using TextBox = Wpf.Ui.Controls.TextBox; namespace BetterGenshinImpact.ViewModel.Pages; @@ -169,6 +172,13 @@ public partial class TaskSettingsPageViewModel : ViewModel .GetField(e.ToString())? .GetCustomAttribute()? .Description ?? e.ToString()); + + + [ObservableProperty] + private bool _switchAutoRedeemCodeEnabled; + + [ObservableProperty] + private string _switchAutoRedeemCodeButtonText = "启动"; public TaskSettingsPageViewModel(IConfigService configService, INavigationService navigationService, TaskTriggerDispatcher taskTriggerDispatcher) { @@ -547,4 +557,44 @@ public partial class TaskSettingsPageViewModel : ViewModel Process.Start("explorer.exe", path); } + + [RelayCommand] + private async Task OnSwitchAutoRedeemCode() + { + var multilineTextBox = new TextBox + { + TextWrapping = TextWrapping.Wrap, + AcceptsReturn = true, + Height = 340, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + PlaceholderText = "请在此输入兑换码,每行一条记录" + }; + var p = new PromptDialog( + "输入兑换码", + "自动使用兑换码", + multilineTextBox, + null); + p.Height = 500; + p.ShowDialog(); + if (p.DialogResult == true && !string.IsNullOrWhiteSpace(multilineTextBox.Text)) + { + var codes = multilineTextBox.Text.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(code => code.Trim()) + .Where(code => !string.IsNullOrEmpty(code)) + .ToList(); + + if (codes.Count == 0) + { + Toast.Warning("没有有效的兑换码"); + return; + } + + SwitchAutoRedeemCodeEnabled = true; + await new TaskRunner() + .RunSoloTaskAsync(new UseRedemptionCodeTask(codes)); + SwitchAutoRedeemCodeEnabled = false; + } + + + } } \ No newline at end of file