Files
MaaAssistantArknights/src/MaaWpfGui/Main/AsstProxy.cs

3188 lines
124 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.
// <copyright file="AsstProxy.cs" company="MaaAssistantArknights">
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License v3.0 only as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
#nullable enable
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using HandyControl.Data;
using MaaWpfGui.Configuration.Factory;
using MaaWpfGui.Configuration.Single.MaaTask;
using MaaWpfGui.Constants;
using MaaWpfGui.Constants.Enums;
using MaaWpfGui.Extensions;
using MaaWpfGui.Helper;
using MaaWpfGui.Models;
using MaaWpfGui.Models.AsstTasks;
using MaaWpfGui.Services;
using MaaWpfGui.Services.Notification;
using MaaWpfGui.Services.Web;
using MaaWpfGui.States;
using MaaWpfGui.Utilities;
using MaaWpfGui.ViewModels.UI;
using MaaWpfGui.ViewModels.UserControl.TaskQueue;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ObservableCollections;
using Serilog;
using Stylet;
using Windows.Win32;
using static MaaWpfGui.Helper.Instances.Data;
using AsstHandle = nint;
using AsstInstanceOptionKey = System.Int32;
using AsstTaskId = System.Int32;
using FightTask = MaaWpfGui.ViewModels.UserControl.TaskQueue.FightSettingsUserControlModel;
using ToastNotification = MaaWpfGui.Helper.ToastNotification;
namespace MaaWpfGui.Main;
/// <summary>
/// MaaCore 代理类。
/// </summary>
public class AsstProxy
{
private readonly RunningState _runningState;
private static readonly ILogger _logger = Log.ForContext<AsstProxy>();
public DateTimeOffset StartTaskTime { get; set; }
private static unsafe byte[] EncodeNullTerminatedUtf8(string s)
{
var enc = Encoding.UTF8.GetEncoder();
fixed (char* c = s)
{
var len = enc.GetByteCount(c, s.Length, true);
var buf = new byte[len + 1];
fixed (byte* ptr = buf)
{
enc.Convert(c, s.Length, ptr, len, true, out _, out _, out _);
}
return buf;
}
}
private static unsafe bool AsstSetUserDir(string dirname)
{
fixed (byte* ptr = EncodeNullTerminatedUtf8(dirname))
{
_logger.Information("AsstSetUserDir dirame: {Dirname}", dirname);
var ret = MaaService.AsstSetUserDir(ptr);
_logger.Information("AsstSetUserDir ret: {Ret}", ret);
return ret;
}
}
private static unsafe bool AsstLoadResource(string dirname)
{
fixed (byte* ptr = EncodeNullTerminatedUtf8(dirname))
{
_logger.Information("AsstLoadResource dirname: {Dirname}", dirname);
var ret = MaaService.AsstLoadResource(ptr);
_logger.Information("AsstLoadResource ret: {Ret}", ret);
return ret;
}
}
private static unsafe bool AsstSetInstanceOption(AsstHandle handle, AsstInstanceOptionKey key, string value)
{
fixed (byte* ptr1 = EncodeNullTerminatedUtf8(value))
{
return MaaService.AsstSetInstanceOption(handle, key, ptr1);
}
}
private static unsafe bool AsstConnect(AsstHandle handle, string adbPath, string address, string config)
{
_logger.Information("handle: {Handle}, adbPath: {AdbPath}, address: {Address}, config: {Config}", (long)handle, adbPath, address, config);
fixed (byte* ptr1 = EncodeNullTerminatedUtf8(adbPath),
ptr2 = EncodeNullTerminatedUtf8(address),
ptr3 = EncodeNullTerminatedUtf8(config))
{
bool ret = MaaService.AsstConnect(handle, ptr1, ptr2, ptr3);
_logger.Information("handle: {Handle}, adbPath: {AdbPath}, address: {Address}, config: {Config}, return: {Ret}", (long)handle, adbPath, address, config, ret);
return ret;
}
}
private static unsafe void AsstSetConnectionExtras(string name, string extras)
{
_logger.Information("name: {Name}, extras: {Extras}", name, extras);
fixed (byte* ptr1 = EncodeNullTerminatedUtf8(name),
ptr2 = EncodeNullTerminatedUtf8(extras))
{
MaaService.AsstSetConnectionExtras(ptr1, ptr2);
}
}
private static bool AsstAttachWindow(AsstHandle handle, IntPtr hwnd, ulong screencapMethod, ulong mouseMethod, ulong keyboardMethod)
{
_logger.Information("handle: {Handle}, hwnd: {Hwnd}, screencapMethod: {ScreencapMethod}, mouseMethod: {MouseMethod}, keyboardMethod: {KeyboardMethod}",
(long)handle, hwnd, screencapMethod, mouseMethod, keyboardMethod);
bool ret = MaaService.AsstAttachWindow(handle, hwnd, screencapMethod, mouseMethod, keyboardMethod);
_logger.Information("handle: {Handle}, hwnd: {Hwnd}, return: {Ret}", (long)handle, hwnd, ret);
return ret;
}
private static void AsstSetConnectionExtrasMuMu12(string extras)
{
AsstSetConnectionExtras("MuMuEmulator12", extras);
}
private static void AsstSetConnectionExtrasLdPlayer(string extras)
{
AsstSetConnectionExtras("LDPlayer", extras);
}
private static unsafe AsstTaskId AsstAppendTask(AsstHandle handle, string type, string taskParams)
{
fixed (byte* ptr1 = EncodeNullTerminatedUtf8(type),
ptr2 = EncodeNullTerminatedUtf8(taskParams))
{
return MaaService.AsstAppendTask(handle, ptr1, ptr2);
}
}
private static unsafe bool AsstSetTaskParams(AsstHandle handle, AsstTaskId id, string taskParams)
{
fixed (byte* ptr1 = EncodeNullTerminatedUtf8(taskParams))
{
return MaaService.AsstSetTaskParams(handle, id, ptr1);
}
}
// 现在拆分了 core 和 UI 的日志,所以这个函数暂时没用到
/*
[DllImport("MaaCore.dll")]
private static extern unsafe void AsstLog(byte* level, byte* message);
/// <summary>
/// 记录日志。
/// </summary>
/// <param name="message">日志内容。</param>
public static unsafe void AsstLog(string message)
{
var level = new ReadOnlySpan<byte>(new byte[] { (byte)'G', (byte)'U', (byte)'I', 0 });
fixed (byte* ptr1 = level, ptr2 = EncodeNullTerminatedUtf8(message))
{
AsstLog(ptr1, ptr2);
}
}
*/
public static unsafe BitmapImage? AsstGetImage(AsstHandle handle)
{
var buffer = ArrayPool<byte>.Shared.Rent(1280 * 720 * 3);
try
{
ulong readSize;
fixed (byte* ptr = buffer)
{
readSize = MaaService.AsstGetImage(handle, ptr, (ulong)buffer.Length);
}
if (readSize == MaaService.AsstGetNullSize())
{
return null;
}
// buff is a png data
var image = new BitmapImage();
image.BeginInit();
using var stream = new MemoryStream(buffer, 0, (int)readSize, false);
image.StreamSource = stream;
image.CacheOption = BitmapCacheOption.OnLoad;
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
image.EndInit();
image.Freeze();
return image;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public BitmapImage? AsstGetImage()
{
return AsstGetImage(_handle);
}
public BitmapImage? AsstGetImage(bool forceScreencap)
{
// UI 端有两类取图场景:
// - 首页预览/缩略图:直接取 core 的缓存帧即可(避免频繁主动截图)
// - 监控/诊断:需要强制触发一次截图以拿到“此刻”的帧
if (forceScreencap)
{
MaaService.AsstAsyncScreencap(_handle, true);
}
return AsstGetImage(_handle);
}
public BitmapImage? AsstGetFreshImage()
{
return AsstGetImage(forceScreencap: true);
}
public static async Task<BitmapImage?> AsstGetImageAsync(AsstHandle handle)
{
return await Task.Run(() => AsstGetImage(handle));
}
public async Task<BitmapImage?> AsstGetImageAsync()
{
return await AsstGetImageAsync(_handle);
}
public async Task<BitmapImage?> AsstGetImageAsync(bool forceScreencap)
{
if (forceScreencap)
{
MaaService.AsstAsyncScreencap(_handle, true);
}
return await AsstGetImageAsync(_handle);
}
public async Task<BitmapImage?> AsstGetFreshImageAsync()
{
return await AsstGetImageAsync(forceScreencap: true);
}
// 需要外部调用 ArrayPool<byte>.Shared.Return(buffer)
public static unsafe byte[]? AsstGetImageBgrData(AsstHandle handle)
{
const int Width = 1280, Height = 720, Channels = 3;
const int TotalSize = Width * Height * Channels;
var buffer = ArrayPool<byte>.Shared.Rent(TotalSize);
ulong readSize;
fixed (byte* ptr = buffer)
{
readSize = MaaService.AsstGetImageBgr(handle, ptr, TotalSize);
}
if (readSize == MaaService.AsstGetNullSize())
{
ArrayPool<byte>.Shared.Return(buffer);
return null;
}
// **不拷贝,直接返回池内存**
// 外层代码用完必须调用 ArrayPool<byte>.Shared.Return(buffer)
return buffer;
}
// 需要外部调用 ArrayPool<byte>.Shared.Return(buffer)
public byte[]? AsstGetImageBgrData()
{
return AsstGetImageBgrData(_handle);
}
public byte[]? AsstGetImageBgrData(bool forceScreencap)
{
if (forceScreencap)
{
MaaService.AsstAsyncScreencap(_handle, true);
}
return AsstGetImageBgrData(_handle);
}
// 需要外部调用 ArrayPool<byte>.Shared.Return(buffer)
public byte[]? AsstGetFreshImageBgrData()
{
return AsstGetImageBgrData(forceScreencap: true);
}
// 需要外部调用 ArrayPool<byte>.Shared.Return(buffer)
public static async Task<byte[]?> AsstGetImageBgrDataAsync(AsstHandle handle)
{
return await Task.Run(() => AsstGetImageBgrData(handle));
}
// 需要外部调用 ArrayPool<byte>.Shared.Return(buffer)
public async Task<byte[]?> AsstGetImageBgrDataAsync()
{
return await AsstGetImageBgrDataAsync(_handle);
}
public async Task<byte[]?> AsstGetImageBgrDataAsync(bool forceScreencap)
{
if (forceScreencap)
{
MaaService.AsstAsyncScreencap(_handle, true);
}
return await AsstGetImageBgrDataAsync(_handle);
}
// 需要外部调用 ArrayPool<byte>.Shared.Return(buffer)
public async Task<byte[]?> AsstGetFreshImageBgrDataAsync()
{
return await AsstGetImageBgrDataAsync(forceScreencap: true);
}
public static WriteableBitmap WriteBgrToBitmap(byte[] bgrData, WriteableBitmap? targetBitmap)
{
const int Width = 1280, Height = 720;
const int Stride = Width * 3;
targetBitmap ??= new(Width, Height, 96, 96, PixelFormats.Bgr24, null);
targetBitmap.Lock();
targetBitmap.WritePixels(
new(0, 0, Width, Height),
bgrData,
Stride,
0);
targetBitmap.Unlock();
return targetBitmap;
}
public const int ScreencapWidth = 1280;
public const int ScreencapHeight = 720;
public const int ScreencapChannels = 3;
public static BitmapSource? CreateBgrBitmapSourceScaled(byte[] bgrData, int targetWidth, int targetHeight)
{
if (targetWidth <= 0 || targetHeight <= 0)
{
_logger.Warning("Invalid target size: {Width}x{Height}", targetWidth, targetHeight);
return null;
}
const int SrcWidth = ScreencapWidth;
const int SrcHeight = ScreencapHeight;
const int SrcStride = SrcWidth * ScreencapChannels;
if (bgrData.Length < SrcHeight * SrcStride)
{
_logger.Warning("Invalid bgrData size: {Size}, expected at least {ExpectedSize}", bgrData.Length, SrcHeight * SrcStride);
return null;
}
int dstStride = targetWidth * ScreencapChannels;
// 直接生成小图,避免先构建大图再缩放导致不必要的内存峰值。
var dst = new byte[targetHeight * dstStride];
for (int y = 0; y < targetHeight; y++)
{
int srcY = y * SrcHeight / targetHeight;
int srcRow = srcY * SrcStride;
int dstRow = y * dstStride;
for (int x = 0; x < targetWidth; x++)
{
int srcX = x * SrcWidth / targetWidth;
int srcIndex = srcRow + (srcX * ScreencapChannels);
int dstIndex = dstRow + (x * ScreencapChannels);
dst[dstIndex] = bgrData[srcIndex];
dst[dstIndex + 1] = bgrData[srcIndex + 1];
dst[dstIndex + 2] = bgrData[srcIndex + 2];
}
}
var bitmap = BitmapSource.Create(
targetWidth,
targetHeight,
96,
96,
PixelFormats.Bgr24,
null,
dst,
dstStride);
bitmap.Freeze();
return bitmap;
}
private readonly MaaService.CallbackDelegate _callback;
/// <summary>
/// Initializes a new instance of the <see cref="AsstProxy"/> class.
/// </summary>
public AsstProxy()
{
_callback = CallbackFunction;
_runningState = RunningState.Instance;
_tasksStatus.CollectionChanged += (in NotifyCollectionChangedEventArgs<KeyValuePair<AsstTaskId, (TaskType, TaskStatus)>> args) => {
if (args.Action == NotifyCollectionChangedAction.Reset)
{
TaskSettingVisibilityInfo.Instance.NotifyOfTaskStatus();
}
};
AsstSetUserDir(PathsHelper.BaseDir);
}
/// <summary>
/// Finalizes an instance of the <see cref="AsstProxy"/> class.
/// </summary>
~AsstProxy()
{
if (_handle != AsstHandle.Zero)
{
AsstDestroy();
}
}
/// <summary>
/// 加载全局资源。新版 core 全惰性加载资源,所以可以无脑调用
/// </summary>
/// <returns>是否成功。</returns>
public bool LoadResource()
{
using var log = new LogScope(_logger);
string clientType = SettingsViewModel.GameSettings.ClientType;
string mainRes = PathsHelper.ResourceDir;
string globalRes = Path.Combine(mainRes, "global", clientType, "resource");
string mainCacheRes = PathsHelper.CacheResourceDir;
string globalCacheRes = Path.Combine(mainCacheRes, "global", clientType, "resource");
bool loaded;
if (clientType is ClientType.Official or ClientType.Bilibili)
{
// Read resources first, then read cache
CopyTasksJson(mainCacheRes);
loaded = LoadResIfExists(mainRes);
loaded &= LoadResIfExists(mainCacheRes);
}
else
{
// Read resources first, then read cache
CopyTasksJson(mainCacheRes);
CopyTasksJson(globalCacheRes);
loaded = LoadResIfExists(mainRes) && LoadResIfExists(mainCacheRes);
loaded &= LoadResIfExists(globalRes) && LoadResIfExists(globalCacheRes);
}
// 使用窗口绑定模式时,额外加载 PC 平台差异资源
if (SettingsViewModel.ConnectSettings.UseAttachWindow)
{
string pcPlatformRes = Path.Combine(mainRes, "platform_diff", "PC", "resource");
loaded &= LoadResIfExists(pcPlatformRes);
}
return loaded;
static bool LoadResIfExists(string path)
{
if (!Directory.Exists(path))
{
_logger.Warning("Resource not found: {Path}", path);
return true;
}
_logger.Information("Load resource: {Path}", path);
// AsstLoadResource 需要的是 resource 的上级目录
var parent = Directory.GetParent(path)?.FullName ?? string.Empty;
if (string.IsNullOrEmpty(parent))
{
_logger.Warning("Resource path invalid: {Path}", path);
return false;
}
return AsstLoadResource(parent);
}
// 新的目录结构为 tasks/tasks.jsonapi 为了兼容,仍然存在 resource/tasks.json
static void CopyTasksJson(string oldResPath)
{
try
{
string tasksJsonPath = Path.Combine(oldResPath, @"tasks.json");
string tasksFolderPath = Path.Combine(oldResPath, @"tasks");
string newTasksJsonPath = Path.Combine(tasksFolderPath, "tasks.json");
if (!File.Exists(tasksJsonPath))
{
return;
}
if (!Directory.Exists(tasksFolderPath))
{
Directory.CreateDirectory(tasksFolderPath);
_logger.Information("Created directory: {TasksFolderPath}", tasksFolderPath);
}
File.Copy(tasksJsonPath, newTasksJsonPath, true);
_logger.Information("Moved {TasksJsonPath} to {NewTasksJsonPath}", tasksJsonPath, newTasksJsonPath);
}
catch (Exception ex)
{
_logger.Error("Failed to move tasks.json: {ExMessage}", ex.Message);
}
}
}
/// <summary>
/// 异步加载资源
/// </summary>
/// <returns>是否成功。</returns>
public async Task<bool> LoadResourceAsync()
{
return await Task.Run(() => LoadResource());
}
/// <summary>
/// 等待系统空闲时异步加载资源,并返回操作是否成功的值。
/// Asynchronously loads the resource when the system is idle and returns a value indicating whether the operation succeeded.
/// </summary>
/// <returns>
/// 表示在系统空闲时异步加载操作的任务。如果资源加载成功,任务结果为 <see langword="true"/>;否则为 <see langword="false"/>。
/// A task that represents the asynchronous load operation when the system is idle. The task result is <see langword="true"/> if the
/// resource was loaded successfully; otherwise, <see langword="false"/>.
/// </returns>
public async Task<bool> LoadResourceWhenIdleAsync()
{
await _runningState.UntilIdleAsync();
return await LoadResourceAsync();
}
/// <summary>
/// 初始化。
/// </summary>
public void Init()
{
if (GpuOption.GetCurrent() is GpuOption.EnableOption x)
{
var info = x.GpuInfo;
var description = info?.Description;
var version = info?.DriverVersion;
var date = info?.DriverDate?.ToString("yyyy-MM-dd");
if (x.IsDeprecated)
{
Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("GpuDeprecatedMessage"), description), UiLogColor.Warning);
_logger.Warning("Using deprecated GPU {0} (Driver {1} {2})", description, version, date);
}
else
{
_logger.Information("Using GPU {0} (Driver {1} {2})", description, version, date);
}
// Check if driver date is over two years old
if (info is { DriverDate: { } driverDate })
{
var twoYearsAgo = DateTime.Now.AddYears(-2);
if (driverDate < twoYearsAgo)
{
var dateStr = driverDate.ToString("yyyy-MM-dd");
var message = string.Format(LocalizationHelper.GetString("GpuDriverOutdatedMessage"), description, version ?? "Unknown", dateStr);
Instances.TaskQueueViewModel.AddLog(message, UiLogColor.Warning);
_logger.Warning("Using GPU {0} with outdated driver {1} (release date: {2}, over 2 years old)", description, version, dateStr);
}
}
AsstSetStaticOption(AsstStaticOptionKey.GpuOCR, x.Index.ToString());
}
bool loaded = LoadResource();
_handle = MaaService.AsstCreateEx(_callback, AsstHandle.Zero);
if (loaded == false || _handle == AsstHandle.Zero)
{
Execute.OnUIThreadAsync(
() => {
MessageBoxHelper.Show(LocalizationHelper.GetString("ResourceBroken"), LocalizationHelper.GetString("Error"), iconKey: ResourceToken.FatalGeometry, iconBrushKey: ResourceToken.DangerBrush);
Bootstrapper.Shutdown();
});
}
_runningState.SetInit(true);
AsstSetInstanceOption(InstanceOptionKey.TouchMode, SettingsViewModel.ConnectSettings.TouchMode);
AsstSetInstanceOption(InstanceOptionKey.DeploymentWithPause, SettingsViewModel.GameSettings.DeploymentWithPause ? "1" : "0");
AsstSetInstanceOption(InstanceOptionKey.AdbLiteEnabled, SettingsViewModel.ConnectSettings.AdbLiteEnabled ? "1" : "0");
// TODO: 之后把这个 OnUIThread 拆出来
// ReSharper disable once AsyncVoidLambda
Execute.OnUIThread(
async () => {
if (SettingsViewModel.StartSettings.RunDirectly)
{
// 如果是直接运行模式,就先让按钮显示为运行
_runningState.SetIdle(false);
}
await Task.Run(() => SettingsViewModel.StartSettings.TryToStartEmulator(true));
// 一般是点了“停止”按钮了
if (_runningState.GetStopping())
{
Instances.TaskQueueViewModel.SetStopped();
return;
}
// ReSharper disable once InvertIf
if (SettingsViewModel.StartSettings.RunDirectly)
{
// 重置按钮状态不影响LinkStart判断
_runningState.SetIdle(true);
await Instances.TaskQueueViewModel.LinkStart();
}
});
}
/// <summary>
/// Determines the length of the specified string (not including the terminating null character).
/// </summary>
/// <param name="ptr">The null-terminated string to be checked.</param>
/// <returns>
/// The function returns the length of the string, in characters.
/// If <paramref name="ptr"/> is <see cref="AsstHandle.Zero"/>, the function returns <c>0</c>.
/// </returns>
[DllImport("ucrtbase.dll", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern int strlen(AsstHandle ptr);
private static string? PtrToStringCustom(AsstHandle ptr, Encoding enc)
{
if (ptr == AsstHandle.Zero)
{
return null;
}
int len = strlen(ptr);
if (len == 0)
{
return string.Empty;
}
byte[] bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
return enc.GetString(bytes);
}
private void CallbackFunction(int msg, AsstHandle jsonBuffer, AsstHandle customArg)
{
var jsonStr = PtrToStringCustom(jsonBuffer, Encoding.UTF8);
// Console.WriteLine(json_str);
var json = (JObject?)JsonConvert.DeserializeObject(jsonStr ?? string.Empty);
MaaService.ProcCallbackMsg dlg = ProcMsg;
Execute.OnUIThread(
() => {
dlg((AsstMsg)msg, json);
});
}
private AsstHandle _handle;
private void ProcMsg(AsstMsg msg, JObject details)
{
switch (msg)
{
case AsstMsg.InternalError:
break;
case AsstMsg.InitFailed:
MessageBoxHelper.Show(LocalizationHelper.GetString("InitializationError"), LocalizationHelper.GetString("Error"), iconKey: ResourceToken.FatalGeometry, iconBrushKey: ResourceToken.DangerBrush);
Bootstrapper.Shutdown();
break;
case AsstMsg.ConnectionInfo:
ProcConnectInfo(details);
break;
case AsstMsg.TaskChainStart:
Instances.TaskQueueViewModel.Running = true;
goto case AsstMsg.TaskChainExtraInfo; // fallthrough
case AsstMsg.AllTasksCompleted:
case AsstMsg.AsyncCallInfo:
case AsstMsg.Destroyed:
case AsstMsg.TaskChainError:
case AsstMsg.TaskChainCompleted:
case AsstMsg.TaskChainStopped:
Instances.TaskQueueViewModel.Running = false;
goto case AsstMsg.TaskChainExtraInfo; // fallthrough
case AsstMsg.TaskChainExtraInfo:
ProcTaskChainMsg(msg, details);
break;
case AsstMsg.SubTaskError:
case AsstMsg.SubTaskStart:
case AsstMsg.SubTaskCompleted:
case AsstMsg.SubTaskExtraInfo:
ProcSubTaskMsg(msg, details);
TaskQueueViewModel.InvokeProcSubTaskMsg(msg, details);
break;
case AsstMsg.SubTaskStopped:
break;
case AsstMsg.ReportRequest:
_ = ProcReportRequest(details);
break;
default:
throw new ArgumentOutOfRangeException(nameof(msg), msg, null);
}
}
public bool Connected { get; set; }
private string _connectedAdb = string.Empty;
private string _connectedAddress = string.Empty;
private string _lastConnectionError = string.Empty;
private void ProcConnectInfo(JObject details)
{
var what = details["what"]?.ToString() ?? string.Empty;
switch (what)
{
case "Connected":
Connected = true;
_connectedAdb = details["details"]!["adb"]!.ToString();
_connectedAddress = details["details"]!["address"]!.ToString();
SettingsViewModel.ConnectSettings.ConnectAddress = _connectedAddress;
_lastConnectionError = string.Empty;
break;
case "UnsupportedResolution":
Connected = false;
_lastConnectionError = LocalizationHelper.GetString("ResolutionNotSupported");
Instances.TaskQueueViewModel.AddLog(_lastConnectionError, UiLogColor.Error);
break;
case "ResolutionInfo":
{
int width = details["details"]?["width"]?.ToObject<int>() ?? 0;
int height = details["details"]?["height"]?.ToObject<int>() ?? 0;
if (SettingsViewModel.GameSettings.ClientType == ClientType.EN && (width != 1920 || height != 1080))
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ResolutionInfoYoStarEN"), UiLogColor.Error);
}
}
break;
case "ResolutionError":
Connected = false;
_lastConnectionError = LocalizationHelper.GetString("ResolutionAcquisitionFailure");
Instances.TaskQueueViewModel.AddLog(_lastConnectionError, UiLogColor.Error);
break;
case "Reconnecting":
Instances.TaskQueueViewModel.AddLog($"{LocalizationHelper.GetString("TryToReconnect")} ({Convert.ToUInt32(details!["details"]!["times"]) + 1})", UiLogColor.Error);
break;
case "Reconnected":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReconnectSuccess"));
break;
case "Disconnect":
Connected = false;
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReconnectFailed"), UiLogColor.Error);
if (_runningState.GetIdle())
{
break;
}
if (!AsstStop())
{
_logger.Warning("Failed to stop Asst");
}
break;
case "ScreencapFailed":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ScreencapFailed"), UiLogColor.Error);
break;
case "TouchModeNotAvailable":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TouchModeNotAvailable"), UiLogColor.Error);
Connected = false;
break;
case "FastestWayToScreencap":
{
string costString = details["details"]?["cost"]?.ToString() ?? "???";
string method = details["details"]?["method"]?.ToString() ?? "???";
SettingsViewModel.ConnectSettings.ScreencapMethod = method;
List<(string Method, string Cost)>? screencapAlternatives = null;
var alternativesToken = details["details"]?["alternatives"];
if (alternativesToken is JArray { Count: > 1 } arr)
{
screencapAlternatives = arr.Select(item => {
string method1 = item?["method"]?.ToString() ?? "???";
string cost1 = item?["cost"]?.ToString() ?? "???";
return (method1, cost1);
}).ToList();
}
StringBuilder fastestScreencapStringBuilder = new();
string color = UiLogColor.Trace;
if (int.TryParse(costString, out var timeCost))
{
switch (timeCost)
{
case > 800:
costString = timeCost.ToString("#,#");
color = UiLogColor.Warning;
break;
case > 400:
color = UiLogColor.Warning;
break;
}
}
else
{
color = UiLogColor.Error;
}
var needToStop = false;
switch (SettingsViewModel.ConnectSettings.ConnectConfig)
{
case "MuMuEmulator12":
if (!SettingsViewModel.ConnectSettings.MuMuEmulator12Extras.Enable)
{
break;
}
if (method != "MumuExtras")
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MuMuExtrasNotEnabledMessage"), UiLogColor.Error);
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MuMuExtrasNotEnabledMessage"), UiLogColor.Error, showTime: false);
needToStop = true;
}
else if (timeCost < 100)
{
color = UiLogColor.MuMuSpecialScreenshot;
}
break;
case "LDPlayer":
if (!SettingsViewModel.ConnectSettings.LdPlayerExtras.Enable)
{
break;
}
if (method != "LDExtras")
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("LdExtrasNotEnabledMessage"), UiLogColor.Error);
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("LdExtrasNotEnabledMessage"), UiLogColor.Error, showTime: false);
needToStop = true;
}
else if (timeCost < 100)
{
color = UiLogColor.LdSpecialScreenshot;
}
break;
}
fastestScreencapStringBuilder.Insert(0, string.Format(LocalizationHelper.GetString("FastestWayToScreencap"), costString, method));
var fastestScreencapString = fastestScreencapStringBuilder.ToString();
SettingsViewModel.ConnectSettings.ScreencapTestCost = fastestScreencapString;
Instances.TaskQueueViewModel.AddLog(fastestScreencapString, color, toolTip: screencapAlternatives.CreateScreencapTooltip());
Instances.CopilotViewModel.AddLog(fastestScreencapString, color, showTime: false);
// 截图增强未生效禁止启动
if (needToStop)
{
Execute.OnUIThreadAsync(async () => {
Connected = false;
await Instances.TaskQueueViewModel.Stop();
Instances.TaskQueueViewModel.SetStopped();
});
}
}
break;
case "ScreencapCost":
var screencapCostMin = details["details"]?["min"]?.ToString() ?? "???";
var screencapCostAvg = details["details"]?["avg"]?.ToString() ?? "???";
var screencapCostMax = details["details"]?["max"]?.ToString() ?? "???";
var currentTime = DateTimeOffset.Now.ToString("HH:mm:ss");
SettingsViewModel.ConnectSettings.ScreencapCost = string.Format(LocalizationHelper.GetString("ScreencapCost"), screencapCostMin, screencapCostAvg, screencapCostMax, currentTime);
if (!HasPrintedScreencapWarning && int.TryParse(screencapCostAvg, out var screencapCostAvgInt))
{
static void AddLog(string message, string color)
{
Instances.TaskQueueViewModel.AddLog(message, color);
Instances.CopilotViewModel.AddLog(message, color, showTime: false);
HasPrintedScreencapWarning = true;
}
switch (screencapCostAvgInt)
{
// 日志提示
case >= 800:
AddLog(string.Format(LocalizationHelper.GetString("FastestWayToScreencapErrorTip"), screencapCostAvgInt), UiLogColor.Warning);
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge1);
break;
case >= 400:
AddLog(string.Format(LocalizationHelper.GetString("FastestWayToScreencapWarningTip"), screencapCostAvgInt), UiLogColor.Warning);
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge2);
break;
default:
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge3);
if (screencapCostAvgInt < 100)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge4);
}
if (screencapCostAvgInt < 10)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge5);
}
if (screencapCostAvgInt < 5)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge6);
}
break;
}
}
}
break;
}
}
private DispatcherTimer? _toastNotificationTimer;
private void OnToastNotificationTimerTick(object? sender, EventArgs e)
{
if (FightTask.SanityReport is not null)
{
var sanityReport = LocalizationHelper.GetString("SanityReport");
var recoveryTime = FightTask.SanityReport.ReportTime.AddMinutes(FightTask.SanityReport.SanityCurrent < FightTask.SanityReport.SanityMax ? (FightTask.SanityReport.SanityMax - FightTask.SanityReport.SanityCurrent) * 6 : 0);
sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m"));
ToastNotification.ShowDirect(sanityReport);
}
DisposeTimer();
}
public void DisposeTimer()
{
if (_toastNotificationTimer is null)
{
return;
}
_toastNotificationTimer.Stop();
_toastNotificationTimer.Tick -= OnToastNotificationTimerTick;
_toastNotificationTimer = null;
}
private void ProcTaskChainMsg(AsstMsg msg, JObject details)
{
string taskChain = details["taskchain"]?.ToString() ?? string.Empty;
AsstTaskId taskId = details["taskid"]?.ToObject<AsstTaskId>() ?? 0;
switch (taskChain)
{
case "CloseDown":
return;
case "Recruit":
{
if (msg == AsstMsg.TaskChainError)
{
Instances.ToolboxViewModel.RecruitInfo = LocalizationHelper.GetString("IdentifyTheMistakes");
ToastNotification.ShowDirect(LocalizationHelper.GetString("IdentifyTheMistakes"));
}
break;
}
}
bool isCopilotTaskChain = taskChain is "Copilot" or "SSSCopilot"; /* or "VideoRecognition"; */
switch (msg)
{
case AsstMsg.TaskChainStopped:
Instances.TaskQueueViewModel.SetStopped();
UpdateTaskStatus(taskId, TaskStatus.Completed);
foreach (var i in Instances.TaskQueueViewModel.TaskItemViewModels)
{
i.TaskId = 0;
i.Status = (int)TaskStatus.Idle;
}
_tasksStatus.Clear();
break;
case AsstMsg.TaskChainError:
{
UpdateTaskStatus(taskId, TaskStatus.Error);
_tasksStatus.TryGetValue(taskId, out var value);
var log = LocalizationHelper.GetString("TaskError") + LocalizationHelper.GetString(taskChain);
Instances.TaskQueueViewModel.AddLog(log, UiLogColor.Error, updateCardImage: true, fetchLatestImage: true, useCardImageAsToolTip: true);
ToastNotification.ShowDirect(log);
if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenError)
{
ExternalNotificationService.Send(log, log);
}
if (value is { Type: TaskType.Copilot })
{
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("CombatError"), UiLogColor.Error);
AchievementTrackerHelper.Instance.Unlock(AchievementIds.CopilotError);
}
break;
}
case AsstMsg.TaskChainStart:
{
var taskIndex = Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(t => t.TaskId == taskId)?.Index ?? -1;
var task = taskIndex >= 0 && taskIndex < ConfigFactory.CurrentConfig.TaskQueue.Count
? ConfigFactory.CurrentConfig.TaskQueue[taskIndex]
: null;
var taskName = task?.Name ?? $"({LocalizationHelper.GetString(taskChain)})";
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartTask") + taskName, splitMode: TaskQueueViewModel.LogCardSplitMode.Before);
_logger.Information("Start Task Chain: {TaskChain}, Task ID: {TaskId}", taskChain, taskId);
UpdateTaskStatus(taskId, TaskStatus.InProgress);
// LinkStart 按钮也会修改,但小工具中的日志源需要在这里修改
Instances.OverlayViewModel.LogItemsSource = (taskChain is "Copilot" or "SSSCopilot") /* or "VideoRecognition") */
? Instances.CopilotViewModel.LogItemViewModels
: Instances.TaskQueueViewModel.LogItemViewModels;
break;
}
case AsstMsg.TaskChainCompleted:
{
// 判断 _latestTaskId 中是否有元素的值和 details["taskid"] 相等,如果有再判断这个 id 对应的任务是否在 _mainTaskTypes 中
UpdateTaskStatus(taskId, TaskStatus.Completed);
if (_tasksStatus.TryGetValue(taskId, out var taskInfo))
{
if (_mainTaskTypes.Contains(taskInfo.Type))
{
Instances.TaskQueueViewModel.UpdateMainTasksProgress();
}
}
var taskIndex = Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(t => t.TaskId == taskId)?.Index ?? -1;
var task = taskIndex >= 0 && taskIndex < ConfigFactory.CurrentConfig.TaskQueue.Count
? ConfigFactory.CurrentConfig.TaskQueue[taskIndex]
: null;
switch (taskChain)
{
case "Infrast":
{
if (task is InfrastTask infrastTask)
{
InfrastSettingsUserControlModel.IncreaseCustomInfrastPlanIndex(infrastTask);
}
break;
}
}
var taskName = task?.Name ?? $"({LocalizationHelper.GetString(taskChain)})";
if (taskChain == "Fight" && FightTask.SanityReport is not null)
{
var sanityLog = "\n" + string.Format(LocalizationHelper.GetString("CurrentSanity"), FightTask.SanityReport.SanityCurrent, FightTask.SanityReport.SanityMax);
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + taskName + sanityLog);
if (FightTask.SanityReport.SanityCurrent == 0)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.SanityPlanner);
}
}
else
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + taskName);
}
_logger.Information("Completed Task Chain: {TaskChain}, Task ID: {TaskId}", taskChain, taskId);
if (isCopilotTaskChain)
{
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.UseCopilotGroup);
}
break;
}
case AsstMsg.TaskChainExtraInfo:
{
var what = details["what"]?.ToString();
var why = details["why"]?.ToString();
switch (what)
{
case "RoutingRestart":
string msgText = string.Empty;
switch (why)
{
case "TooManyBattlesAhead":
var cost = details["node_cost"]?.ToString() ?? "?";
msgText = string.Format(LocalizationHelper.GetString("RoutingRestartTooManyBattles"), cost);
break;
}
Instances.TaskQueueViewModel.AddLog(msgText, UiLogColor.Warning);
break;
}
break;
}
case AsstMsg.AllTasksCompleted:
bool isMainTaskQueueAllCompleted = false;
var taskList = details["finished_tasks"]?.ToObject<AsstTaskId[]>();
if (taskList?.Length > 0)
{
var latestMainTaskIds = _tasksStatus.Where(i => _mainTaskTypes.Contains(i.Value.Type)).Select(i => i.Key);
isMainTaskQueueAllCompleted = taskList.Any(i => latestMainTaskIds.Contains(i));
}
if (_tasksStatus.Any(t => t.Value.Type == TaskType.Copilot))
{
if (SettingsViewModel.GameSettings.CopilotWithScript)
{
Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript", showLog: false));
if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.EndsWithScript))
{
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("EndsWithScript"));
}
}
}
bool buyWine = _tasksStatus.Any(t => t.Value.Type == TaskType.Mall) && Instances.SettingsViewModel.DidYouBuyWine();
foreach (var i in Instances.TaskQueueViewModel.TaskItemViewModels)
{
i.TaskId = 0;
i.Status = (int)TaskStatus.Idle;
}
_tasksStatus.Clear();
Instances.TaskQueueViewModel.ResetAllTemporaryVariable();
_runningState.SetIdle(true);
if (isMainTaskQueueAllCompleted)
{
var dateTimeNow = DateTimeOffset.Now;
var diffTaskTime = (dateTimeNow - StartTaskTime).ToString(@"h\h\ m\m\ s\s");
var allTaskCompleteTitle = string.Format(LocalizationHelper.GetString("AllTasksComplete"), diffTaskTime);
var allTaskCompleteMessage = LocalizationHelper.GetString("AllTaskCompleteContent");
var sanityReport = LocalizationHelper.GetString("SanityReport");
var configurationPreset = ConfigurationHelper.GetCurrentConfiguration();
allTaskCompleteMessage = allTaskCompleteMessage
.Replace("{DateTime}", dateTimeNow.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{Preset}", configurationPreset)
.Replace("{TimeDiff}", diffTaskTime);
var allTaskCompleteLog = string.Format(LocalizationHelper.GetString("AllTasksComplete"), diffTaskTime);
if (FightTask.SanityReport is not null)
{
var recoveryTime = FightTask.SanityReport.ReportTime.AddMinutes(FightTask.SanityReport.SanityCurrent < FightTask.SanityReport.SanityMax ? (FightTask.SanityReport.SanityMax - FightTask.SanityReport.SanityCurrent) * 6 : 0);
sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m"));
allTaskCompleteLog = allTaskCompleteLog + Environment.NewLine + sanityReport;
Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog, splitMode: TaskQueueViewModel.LogCardSplitMode.Both);
if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete)
{
var logs = SettingsViewModel.ExternalNotificationSettings.ExternalNotificationEnableDetails
? Instances.TaskQueueViewModel.LogItemViewModels.Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n")
: string.Empty;
logs += allTaskCompleteMessage;
ExternalNotificationService.Send(allTaskCompleteTitle, logs + Environment.NewLine + sanityReport);
}
if (_toastNotificationTimer is not null)
{
DisposeTimer();
}
var interval = recoveryTime - DateTimeOffset.Now.AddMinutes(6);
if (interval > TimeSpan.Zero)
{
_toastNotificationTimer = new DispatcherTimer {
Interval = interval,
};
_toastNotificationTimer.Tick += OnToastNotificationTimerTick;
_toastNotificationTimer.Start();
}
}
else
{
Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog, splitMode: TaskQueueViewModel.LogCardSplitMode.Both);
if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete)
{
var logs = SettingsViewModel.ExternalNotificationSettings.ExternalNotificationEnableDetails
? Instances.TaskQueueViewModel.LogItemViewModels.Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n")
: string.Empty;
logs += allTaskCompleteMessage;
ExternalNotificationService.Send(allTaskCompleteTitle, logs);
}
}
using (var toast = new ToastNotification(allTaskCompleteTitle))
{
if (FightTask.SanityReport is not null)
{
toast.AppendContentText(sanityReport);
}
toast.Show();
}
if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay())
{
if (Application.Current.MainWindow?.DataContext is RootViewModel rvm)
{
rvm.GifVisibility = true;
rvm.ChangeGif();
}
}
// Instances.TaskQueueViewModel.CheckAndShutdown();
_ = Instances.TaskQueueViewModel.CheckAfterCompleted();
if (Instances.OverlayViewModel.IsCreated)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.LogSupervisor);
}
}
else if (isCopilotTaskChain)
{
ToastNotification.ShowDirect(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain));
}
if (buyWine)
{
Instances.SettingsViewModel.LastBuyWineTime = DateTime.UtcNow.ToYjDate().ToFormattedString();
var result = MessageBoxHelper.Show(
LocalizationHelper.GetString("DrunkAndStaggering"),
LocalizationHelper.GetString("Burping"),
iconKey: "DrunkAndStaggeringGeometry",
iconBrushKey: "PallasBrush");
if (result == MessageBoxResult.OK)
{
Instances.SettingsViewModel.Cheers = true;
Bootstrapper.ShutdownAndRestartWithoutArgs();
}
}
break;
case AsstMsg.InternalError:
break;
case AsstMsg.InitFailed:
break;
case AsstMsg.ConnectionInfo:
break;
case AsstMsg.AsyncCallInfo:
break;
case AsstMsg.Destroyed:
break;
case AsstMsg.SubTaskError:
break;
case AsstMsg.SubTaskStart:
break;
case AsstMsg.SubTaskCompleted:
break;
case AsstMsg.SubTaskExtraInfo:
break;
case AsstMsg.SubTaskStopped:
break;
default:
throw new ArgumentOutOfRangeException(nameof(msg), msg, null);
}
}
private static void ProcSubTaskMsg(AsstMsg msg, JObject details)
{
// 下面几行注释暂时没用到,先注释起来...
// string taskChain = details["taskchain"].ToString();
// string classType = details["class"].ToString();
switch (msg)
{
case AsstMsg.SubTaskError:
ProcSubTaskError(details);
break;
case AsstMsg.SubTaskStart:
ProcSubTaskStart(details);
break;
case AsstMsg.SubTaskCompleted:
ProcSubTaskCompleted(details);
break;
case AsstMsg.SubTaskExtraInfo:
ProcSubTaskExtraInfo(details);
break;
case AsstMsg.InternalError:
break;
case AsstMsg.InitFailed:
break;
case AsstMsg.ConnectionInfo:
break;
case AsstMsg.AllTasksCompleted:
break;
case AsstMsg.TaskChainError:
break;
case AsstMsg.TaskChainStart:
break;
case AsstMsg.TaskChainCompleted:
break;
case AsstMsg.TaskChainExtraInfo:
break;
case AsstMsg.TaskChainStopped:
break;
case AsstMsg.SubTaskStopped:
break;
default:
throw new ArgumentOutOfRangeException(nameof(msg), msg, null);
}
}
private static void ProcSubTaskError(JObject details)
{
string subTask = details["subtask"]?.ToString() ?? string.Empty;
AsstTaskId taskId = details["taskid"]?.ToObject<AsstTaskId>() ?? 0;
switch (subTask)
{
case "StartGameTask":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FailedToOpenClient"), UiLogColor.Error);
break;
case "StopGameTask":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error);
break;
case "AutoRecruitTask":
{
var whyStr = details.TryGetValue("why", out var why) ? why.ToString() : LocalizationHelper.GetString("ErrorOccurred");
Instances.TaskQueueViewModel.AddLog(whyStr + ", " + LocalizationHelper.GetString("HasReturned"), UiLogColor.Error);
break;
}
case "RecognizeDrops":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DropRecognitionError"), UiLogColor.Error);
break;
case "ReportToPenguinStats":
{
var why = details["why"]!.ToString();
// 剿灭放弃上传企鹅物流的特殊处理
Instances.AsstProxy.TasksStatus.TryGetValue(taskId, out var value);
if (value is { Type: TaskType.Fight }
&& (Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(t => t.TaskId == taskId)?.Index ?? -1) is int index and > -1
&& index <= ConfigFactory.CurrentConfig.TaskQueue.Count
&& ConfigFactory.CurrentConfig.TaskQueue[index] is Configuration.Single.MaaTask.FightTask fight
&& FightSettingsUserControlModel.GetFightStage(fight.StagePlan) == FightSettingsUserControlModel.AnnihilationName)
{
Instances.TaskQueueViewModel.AddLog("AnnihilationStage, " + LocalizationHelper.GetString("GiveUpUploadingPenguins"));
break;
}
Instances.TaskQueueViewModel.AddLog(why + ", " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning);
break;
}
case "CheckStageValid":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TheEx"), UiLogColor.Error);
break;
case "BattleFormationTask":
{
var why = details.TryGetValue("why", out var whyObj) ? whyObj.ToString() : string.Empty;
if (why == "OperatorMissing")
{
var missingOpers = details["details"]?["opers"]?.ToObject<Dictionary<string, JArray>>();
if (missingOpers is not null && missingOpers.Count > 0)
{
var str = new StringBuilder();
str.AppendLine();
foreach (var (groupName, opers) in missingOpers)
{
if (opers.Count == 1)
{
str.AppendLine($"{groupName}");
continue;
}
else
{
var operList = opers.Cast<dynamic>().ToList(); // 确保 opers 是动态类型
str.AppendLine($"{groupName}=> {string.Join(" / ", operList.Select(i => i.name).ToList())}");
}
}
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissingOperators") + str.ToString().TrimEnd(), UiLogColor.Error);
}
else
{
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissingOperators"), UiLogColor.Error);
}
if (missingOpers is not null && missingOpers.Count >= 2)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.Irreplaceable);
}
}
break;
}
case "CopilotTask":
{
var what = details["what"]?.ToString() ?? string.Empty;
if (what == "UserAdditionalOperInvalid")
{
var operName = details["details"]?["name"]?.ToString();
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetStringFormat("CopilotUserAdditionalNameInvalid", operName ?? string.Empty), UiLogColor.Error);
}
break;
}
}
}
private static void ProcSubTaskStart(JObject details)
{
string subTask = details["subtask"]?.ToString() ?? string.Empty;
switch (subTask)
{
case "ProcessTask":
{
string taskName = details!["details"]!["task"]!.ToString();
int execTimes = (int)details!["details"]!["exec_times"]!;
switch (taskName)
{
case "StartButton2":
case "AnnihilationConfirm":
StringBuilder missionStartLogBuilder = new();
if (FightTask.FightReport is null)
{
missionStartLogBuilder.AppendLine(string.Format(LocalizationHelper.GetString("MissionStart.FightTask"), "???", "???"));
}
else
{
var times = FightTask.FightReport.Series == 1 ? $"{FightTask.FightReport.TimesFinished + 1}" : $"{FightTask.FightReport.TimesFinished + 1}~{FightTask.FightReport.TimesFinished + FightTask.FightReport.Series}";
missionStartLogBuilder.AppendLine(string.Format(LocalizationHelper.GetString("MissionStart.FightTask"), times, FightTask.FightReport.SanityCost));
}
if (FightTask.SanityReport is not null)
{
missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("CurrentSanity"), FightTask.SanityReport.SanityCurrent, FightTask.SanityReport.SanityMax);
}
if (ExpiringMedicineUsedTimes > 0)
{
missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("MedicineUsedTimesWithExpiring"), MedicineUsedTimes, ExpiringMedicineUsedTimes);
}
else if (MedicineUsedTimes > 0)
{
missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("MedicineUsedTimes"), MedicineUsedTimes);
}
if (StoneUsedTimes > 0)
{
missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("StoneUsedTimes"), StoneUsedTimes);
}
Instances.TaskQueueViewModel.AddLog(missionStartLogBuilder.ToString().TrimEnd(), UiLogColor.Info, splitMode: TaskQueueViewModel.LogCardSplitMode.Before);
break;
case "StoneConfirm":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StoneUsed") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info);
StoneUsedTimes++;
break;
case "AbandonAction":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ActingCommandError"), UiLogColor.Error);
break;
case "FightMissionFailedAndStop":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightMissionFailedAndStop"), UiLogColor.Error);
ToastNotification.ShowDirect(LocalizationHelper.GetString("FightMissionFailedAndStop"));
break;
case "RecruitRefreshConfirm":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("LabelsRefreshed"), UiLogColor.Info);
break;
case "RecruitConfirm":
RecruitConfirmTime++;
if (RecruitConfirmTime > AchievementTrackerHelper.Instance.GetProgressToGroup(AchievementIds.HrManager))
{
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.HrManager);
}
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitConfirm"), UiLogColor.Info);
break;
case "InfrastDormDoubleConfirmButton":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("InfrastDormDoubleConfirmed"), UiLogColor.Error);
break;
/* 肉鸽相关 */
case "ExitThenAbandon":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ExplorationAbandoned"), UiLogColor.ExplorationAbandonedIS);
AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RoguelikeRetreat);
break;
// case "StartAction":
// Instances.TaskQueueViewModel.AddLog("开始战斗");
// break;
case "MissionCompletedFlag":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightCompleted"), UiLogColor.SuccessIS, updateCardImage: true);
break;
case "MissionFailedFlag":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightFailed"), UiLogColor.Error, updateCardImage: true);
break;
case "StageTrader":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Trader"), UiLogColor.TraderIS);
break;
case "StageSafeHouse":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("SafeHouse"), UiLogColor.SafehouseIS);
break;
case "StageFilterTruth":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FilterTruth"), UiLogColor.TruthIS);
break;
// case "StageBoonsEnter":
// Instances.TaskQueueViewModel.AddLog("古堡馈赠");
// break;
case "StageCombatOps":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CombatOps"), UiLogColor.CombatIS);
break;
case "StageEmergencyOps":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("EmergencyOps"), UiLogColor.EmergencyIS);
break;
case "StageDreadfulFoe":
case "StageDreadfulFoe-5":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DreadfulFoe"), UiLogColor.BossIS);
break;
case "StageTraderInvestSystemFull":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UpperLimit"), UiLogColor.Info);
break;
case "OfflineConfirm":
if (TaskQueueViewModel.FightTask.AutoRestartOnDrop)
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("GameDrop"), UiLogColor.Warning);
}
else
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("GameDropNoRestart"), UiLogColor.Warning);
ToastNotification.ShowDirect(LocalizationHelper.GetString("GameDropNoRestart"));
_ = Instances.TaskQueueViewModel.Stop();
}
break;
case "GamePass":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeGamePass"), UiLogColor.RareOperator);
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.RoguelikeGamePassGroup);
break;
case "BattleStartAll":
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissionStart"), UiLogColor.Info);
break;
case "StageDrops-Stars-3":
case "StageDrops-Stars-Adverse":
{
Instances.CopilotViewModel.CopilotTaskSuccess();
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("CompleteCombat"), UiLogColor.Info);
break;
}
case "StageTraderSpecialShoppingAfterRefresh":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeSpecialItemBought"), UiLogColor.RareOperator);
break;
case "DeepExplorationNotUnlockedComplain":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DeepExplorationNotUnlockedComplain"), UiLogColor.Warning);
break;
case "PNS-Resume":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReclamationPnsModeError"), UiLogColor.Error);
break;
case "PIS-Commence":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReclamationPisModeError"), UiLogColor.Error);
break;
}
break;
}
case "CombatRecordRecognitionTask":
{
var what = details["what"]?.ToString();
if (!string.IsNullOrEmpty(what))
{
Instances.CopilotViewModel.AddLog(what);
}
break;
}
}
}
private static void ProcSubTaskCompleted(JObject details)
{
string subTask = details["subtask"]?.ToString() ?? string.Empty;
switch (subTask)
{
case "ProcessTask":
var taskName = details["details"]?["task"]?.ToString();
var taskChain = details["taskchain"]?.ToString();
AsstTaskId taskId = details["taskid"]?.ToObject<AsstTaskId>() ?? 0;
switch (taskChain)
{
case "Infrast":
{
switch (taskName)
{
case "UnlockClues":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ClueExchangeUnlocked"));
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ClueUseGroup);
AchievementTrackerHelper.Instance.ClueObsessionAdd();
break;
case "SendClues":
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ClueSendGroup);
break;
}
break;
}
case "Roguelike":
{
int execTimes = (int)details!["details"]!["exec_times"]!;
if (taskName == "StartExplore")
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("BegunToExplore") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info, splitMode: TaskQueueViewModel.LogCardSplitMode.Before);
}
break;
}
case "Mall":
{
switch (taskName)
{
case "EndOfActionThenStop":
{
var index = Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(t => t.TaskId == taskId)?.Index ?? -1;
if (index >= 0 && index < ConfigFactory.CurrentConfig.TaskQueue.Count && ConfigFactory.CurrentConfig.TaskQueue[index] is MallTask mall)
{
mall.CreditFightLastTime = DateTime.UtcNow.ToYjDate().ToFormattedString();
}
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString("CreditFight"));
AchievementTrackerHelper.Instance.AddProgress(AchievementIds.MosquitoLeg);
break;
}
case "VisitLimited" or "VisitNextBlack":
{
var index = Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(t => t.TaskId == taskId)?.Index ?? -1;
if (index >= 0 && index < ConfigFactory.CurrentConfig.TaskQueue.Count && ConfigFactory.CurrentConfig.TaskQueue[index] is MallTask mall)
{
mall.VisitFriendsLastTime = DateTime.UtcNow.ToYjDate().ToFormattedString();
}
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString("Visiting"));
break;
}
}
break;
}
}
break;
}
}
private static void ProcSubTaskExtraInfo(JObject details)
{
string taskChain = details["taskchain"]?.ToString() ?? string.Empty;
switch (taskChain)
{
case "Recruit":
ProcRecruitCalcMsg(details);
break;
/*
case "VideoRecognition":
ProcVideoRecMsg(details);
break;
*/
}
var subTaskDetails = details["details"];
switch (taskChain)
{
case "Depot":
Instances.ToolboxViewModel.DepotParse((JObject?)subTaskDetails, updateSyncTime: true);
break;
case "OperBox":
Instances.ToolboxViewModel.OperBoxParse((JObject?)subTaskDetails);
break;
}
string what = details["what"]?.ToString() ?? string.Empty;
switch (what)
{
case "StageDrops":
{
string allDrops = string.Empty;
var statistics = subTaskDetails!["stats"] ?? new JArray();
var stageInfo = subTaskDetails!["stage"] ?? new JObject();
int curTimes = (int)(subTaskDetails["cur_times"] ?? -1);
var drops = new List<(string ItemId, string ItemName, int Total, int Add)>();
foreach (var item in statistics)
{
var itemId = item["itemId"]?.ToString() ?? string.Empty;
var itemName = item["itemName"]?.ToString() ?? string.Empty;
if (itemName == "furni")
{
itemName = LocalizationHelper.GetString("FurnitureDrop");
itemId = "3401";
}
int totalQuantity = (int)(item["quantity"] ?? -1);
int addQuantity = (int)(item["addQuantity"] ?? -1);
drops.Add((itemId, itemName, totalQuantity, addQuantity));
}
// 先按新增数量降序,再按总数量降序
drops = [.. drops.OrderByDescending(x => x.Add).ThenByDescending(x => x.Total)];
foreach (var (_, itemName, totalQuantity, addQuantity) in drops)
{
allDrops += $"{itemName} : {totalQuantity.FormatNumber(false)}";
if (addQuantity > 0)
{
allDrops += $" (+{addQuantity.FormatNumber(false)})";
}
allDrops += "\n";
}
var stageCode = stageInfo["stageCode"]?.ToString();
allDrops = allDrops.EndsWith('\n') ? allDrops.TrimEnd('\n') : LocalizationHelper.GetString("NoDrop");
var dropsForTooltip = drops.Where(x => !string.IsNullOrEmpty(x.ItemId)).ToList();
Instances.TaskQueueViewModel.AddLog(
$"{stageCode} {LocalizationHelper.GetString("TotalDrop")}\n" +
$"{allDrops}{(curTimes >= 0
? $"\n{LocalizationHelper.GetString("CurTimes")} : {curTimes}"
: string.Empty)}",
toolTip: dropsForTooltip.CreateMaterialDropTooltip(),
updateCardImage: true);
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySpenderGroup, curTimes > 0 ? curTimes : 1);
// 联动更新 Depot 数据
Instances.ToolboxViewModel.UpdateDepotFromDrops(drops);
break;
}
case "EnterFacility":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ThisFacility") +
LocalizationHelper.GetString($"{subTaskDetails?["facility"]}") + " " +
((int)(subTaskDetails?["index"] ?? -2) + 1).ToString("D2"),
splitMode: TaskQueueViewModel.LogCardSplitMode.Before);
break;
case "ProductIncorrect":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductIncorrect"), UiLogColor.Error);
break;
case "ProductUnknown":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductUnknown"), UiLogColor.Error);
break;
case "ProductChanged":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductChanged"), UiLogColor.Info);
break;
case "InfrastConfirmButton":
Instances.TaskQueueViewModel.AddLog(string.Empty, updateCardImage: true, fetchLatestImage: true);
break;
case "RecruitTagsDetected":
{
var tags = subTaskDetails!["tags"] ?? new JArray();
string logContent = tags.Select(tagName => tagName.ToString())
.Aggregate(string.Empty, (current, tagStr) => current + (tagStr + "\n"));
logContent = logContent.EndsWith('\n') ? logContent.TrimEnd('\n') : LocalizationHelper.GetString("Error");
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitingResults") + "\n" + logContent, splitMode: TaskQueueViewModel.LogCardSplitMode.Before, updateCardImage: true);
break;
}
case "RecruitSpecialTag":
{
string special = subTaskDetails!["tag"]!.ToString();
if (special == "支援机械" && TaskQueueViewModel.RecruitTask.NotChooseLevel1 == false)
{
break;
}
using var toast = new ToastNotification(LocalizationHelper.GetString("RecruitingTips"));
toast.AppendContentText(special).ShowRecruit();
break;
}
case "RecruitRobotTag":
{
string special = subTaskDetails!["tag"]!.ToString();
using var toast = new ToastNotification(LocalizationHelper.GetString("RecruitingTips"));
toast.AppendContentText(special).ShowRecruitRobot();
break;
}
case "RecruitResult":
{
int level = (int)subTaskDetails!["level"]!;
var tooltip = Instances.ToolboxViewModel.RecruitResultInlines.CreateTooltip(PlacementMode.Center);
if (level >= 5)
{
using (var toast = new ToastNotification(string.Format(LocalizationHelper.GetString("RecruitmentOfStar"), level)))
{
toast.AppendContentText(new string('★', level)).ShowRecruit(row: 2);
}
Instances.TaskQueueViewModel.AddLog(level + " ★ Tags", UiLogColor.RareOperator, "Bold", toolTip: tooltip);
}
else
{
Instances.TaskQueueViewModel.AddLog(level + " ★ Tags", UiLogColor.Info, toolTip: tooltip);
}
if (level == 6)
{
AchievementTrackerHelper.Instance.SetProgress(AchievementIds.RecruitNoSixStarStreak, 1);
}
else
{
AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitNoSixStar); // 累计
AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitNoSixStarStreak); // 连续
}
/*
bool robot = (bool)subTaskDetails["robot"];
if (robot)
{
using (var toast = new ToastNotification(Localization.GetString("RecruitmentOfBot")))
{
toast.AppendContentText(new string('★', 1)).ShowRecruitRobot(row: 2);
}
Instances.TaskQueueViewModel.AddLog(1 + " ★ Tag", LogColor.RobotOperator, "Bold");
}
*/
break;
}
case "RecruitSupportOperator":
{
var name = subTaskDetails!["name"]!.ToString();
Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RecruitSupportOperator"), name), UiLogColor.Info);
break;
}
case "RecruitTagsSelected":
{
var selected = subTaskDetails!["tags"] ?? new JArray();
string selectedLog = selected.Aggregate(string.Empty, (current, tag) => current + (tag + "\n"));
selectedLog = selectedLog.EndsWith('\n') ? selectedLog.TrimEnd('\n') : LocalizationHelper.GetString("NoDrop");
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Choose") + " Tags\n" + selectedLog);
break;
}
case "RecruitTagsRefreshed":
{
int refreshCount = (int)subTaskDetails!["count"]!;
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Refreshed") + refreshCount + LocalizationHelper.GetString("UnitTime"));
AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitGambler);
break;
}
case "RecruitNoPermit":
{
bool continueRefresh = (bool)subTaskDetails!["continue"]!;
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString(continueRefresh ? "ContinueRefresh" : "NoRecruitmentPermit"));
break;
}
case "NotEnoughStaff":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NotEnoughStaff"), UiLogColor.Error);
break;
case "CreditFullOnlyBuyDiscount":
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CreditFullOnlyBuyDiscount") + subTaskDetails!["credit"], UiLogColor.Message);
break;
}
/* Roguelike */
case "StageInfo":
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartCombat") + subTaskDetails!["name"]);
if (TaskQueueViewModel.RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete)
{
Instances.TaskQueueViewModel.RoguelikeInCombatAndShowWait = true;
}
break;
}
case "StageInfoError":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageInfoError"), UiLogColor.Error, splitMode: TaskQueueViewModel.LogCardSplitMode.Both, updateCardImage: true);
break;
case "BattleFormation":
Instances.CopilotViewModel.AddLog(
LocalizationHelper.GetString("BattleFormation") +
"\n[" +
string.Join(
", ",
(subTaskDetails!["formation"]?.ToObject<List<string?>>() ?? [])
.Select(oper => DataHelper.GetLocalizedCharacterName(oper) ?? oper)
.Where(oper => !string.IsNullOrEmpty(oper))) + "]");
break;
case "BattleFormationParseFailed":
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("BattleFormationParseFailed"));
break;
case "BattleFormationSelected":
{
var oper_name = DataHelper.GetLocalizedCharacterName(subTaskDetails!["selected"]?.ToString());
var group_name = subTaskDetails!["group_name"]?.ToString();
if (group_name is not null && oper_name != group_name)
{
oper_name = $"{group_name} => {oper_name}";
}
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("BattleFormationSelected") + oper_name);
break;
}
case "BattleFormationOperUnavailable":
{
Instances.CopilotViewModel.HasRequirementIgnored = true;
var oper_name = DataHelper.GetLocalizedCharacterName(subTaskDetails!["oper_name"]?.ToString());
var type = subTaskDetails["requirement_type"]?.ToString() ?? "Unknown Type";
bool isError = !Instances.CopilotViewModel.IgnoreRequirements;
switch (type)
{
case "elite":
type = LocalizationHelper.GetString("BattleFormationOperUnavailable.Elite");
isError = true;
break;
case "level":
type = LocalizationHelper.GetString("BattleFormationOperUnavailable.Level");
break;
case "skill_level":
type = LocalizationHelper.GetString("BattleFormationOperUnavailable.SkillLevel");
break;
case "module":
type = LocalizationHelper.GetString("BattleFormationOperUnavailable.Module");
break;
default:
break;
}
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetStringFormat("BattleFormationOperUnavailable", oper_name ?? string.Empty, type), isError ? UiLogColor.Error : UiLogColor.Warning);
break;
}
case "CopilotAction":
{
var doc = subTaskDetails!["doc"]?.ToString();
if (!string.IsNullOrEmpty(doc))
{
var color = subTaskDetails["doc_color"]?.ToString();
Instances.CopilotViewModel.AddLog(doc, string.IsNullOrEmpty(color) ? UiLogColor.Message : color);
}
var target = subTaskDetails["target"]?.ToString();
var actionToken = subTaskDetails?["action"];
var actionString = actionToken?.ToString() ?? "UnknownAction";
Instances.CopilotViewModel.AddLog(
string.Format(
LocalizationHelper.GetString("CurrentSteps"),
LocalizationHelper.GetString(actionString),
DataHelper.GetLocalizedCharacterName(target) ?? target));
var elapsed_time_str = subTaskDetails!["elapsed_time"]?.ToString();
if (int.TryParse(elapsed_time_str, out int elapsed_time_int) && elapsed_time_int >= 0)
{
Instances.CopilotViewModel.AddLog(string.Format(LocalizationHelper.GetString("ElapsedTime"), elapsed_time_int), UiLogColor.Message);
}
break;
}
case "CopilotListLoadTaskFileSuccess":
Instances.CopilotViewModel.AddLog($"Parse {subTaskDetails!["file_name"]}[{subTaskDetails["stage_name"]}] Success");
Instances.CopilotViewModel.HasRequirementIgnored = false;
break;
case "SSSStage":
Instances.CopilotViewModel.AddLog(string.Format(LocalizationHelper.GetString("CurrentStage"), subTaskDetails!["stage"]), UiLogColor.Info);
break;
case "SSSSettlement":
Instances.CopilotViewModel.AddLog($"{details["why"]}", UiLogColor.Info);
break;
case "SSSGamePass":
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("SSSGamePass"), UiLogColor.RareOperator);
break;
case "UnsupportedLevel":
Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("UnsupportedLevel") + subTaskDetails!["level"], UiLogColor.Error);
_ = ResourceUpdater.ResourceUpdateAndReloadAsync();
break;
case "CustomInfrastRoomGroupsMatch":
// 选用xxx组编组
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomGroupsMatch") + subTaskDetails!["group"]);
break;
case "CustomInfrastRoomGroupsMatchFailed":
// 干员编组匹配失败
var groups = (JArray?)subTaskDetails!["groups"];
if (groups != null)
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomGroupsMatchFailed") + string.Join(", ", groups));
}
break;
case "CustomInfrastRoomOperators":
string nameStr = (subTaskDetails!["names"] ?? new JArray())
.Aggregate(string.Empty, (current, name) => current + DataHelper.GetLocalizedCharacterName(name.ToString()) + ", ");
if (nameStr != string.Empty)
{
nameStr = nameStr.Remove(nameStr.Length - 2);
}
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomOperators") + nameStr);
break;
case "InfrastTrainingIdle":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TrainingIdle"));
break;
case "InfrastTrainingCompleted":
{
var operatorName = DataHelper.GetLocalizedCharacterName(subTaskDetails!["operator"]?.ToString()) ?? "UnKnown";
var skillName = subTaskDetails["skill"]?.ToString() ?? "UnKnown";
Instances.TaskQueueViewModel.AddLog(
$"[{operatorName}] {skillName}\n" +
$"{LocalizationHelper.GetString("TrainingLevel")}: {(int)(subTaskDetails["level"] ?? -1)} {LocalizationHelper.GetString("TrainingCompleted")}",
UiLogColor.Info);
break;
}
case "InfrastTrainingTimeLeft":
{
var operatorName = DataHelper.GetLocalizedCharacterName(subTaskDetails!["operator"]?.ToString()) ?? "UnKnown";
var skillName = subTaskDetails["skill"]?.ToString() ?? "UnKnown";
Instances.TaskQueueViewModel.AddLog(
$"[{operatorName}] {skillName}\n" +
$"{LocalizationHelper.GetString("TrainingLevel")}: {(int)(subTaskDetails["level"] ?? -1)}\n" +
$"{LocalizationHelper.GetString("TrainingTimeLeft")}: {subTaskDetails["time"]}",
UiLogColor.Info);
break;
}
/* 生息演算 */
case "ReclamationReport":
Instances.TaskQueueViewModel.AddLog(
LocalizationHelper.GetString("AlgorithmFinish") + "\n" +
LocalizationHelper.GetString("AlgorithmBadge") + ": " + $"{(int)(subTaskDetails!["total_badges"] ?? -1)}(+{(int)(subTaskDetails["badges"] ?? -1)})" + "\n" +
LocalizationHelper.GetString("AlgorithmConstructionPoint") + ": " + $"{(int)(subTaskDetails["total_construction_points"] ?? -1)}(+{(int)(subTaskDetails["construction_points"] ?? -1)})");
break;
case "ReclamationProcedureStart":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MissionStart") + $" {(int)(subTaskDetails!["times"] ?? -1)} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info);
break;
case "ReclamationSmeltGold":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AlgorithmDoneSmeltGold") + $" {(int)(subTaskDetails!["times"] ?? -1)} " + LocalizationHelper.GetString("UnitTime"));
break;
case "SanityBeforeStage":
{
FightTask.SanityReport = null;
if (subTaskDetails?.ToObject<FightSettingsUserControlModel.SanityInfo>() is { SanityMax: > 0 } report)
{
FightTask.SanityReport = report;
}
break;
}
case "FightTimes":
{
FightTask.FightReport = null;
if ((subTaskDetails?.Children())?.Any() is true)
{
FightTask.FightReport = subTaskDetails.ToObject<FightTask.FightTimes>()!;
if (FightTask.FightReport.TimesFinished > 0)
{
AchievementTrackerHelper.Instance.SetProgress(AchievementIds.OverLimitAgent, FightTask.FightReport.TimesFinished);
}
if (FightTask.Instance.HasTimesLimited != false && FightTask.FightReport.IsFinished && FightTask.FightReport.TimesFinished < FightTask.Instance.MaxTimes)
{
Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("FightTimesUnused"), FightTask.FightReport.TimesFinished, FightTask.FightReport.Series, FightTask.FightReport.TimesFinished + FightTask.FightReport.Series, FightTask.Instance.MaxTimes), UiLogColor.Warning);
}
}
break;
}
case "UseMedicine":
var medicineReport = (JObject?)subTaskDetails;
if (medicineReport is null || !medicineReport.ContainsKey("is_expiring") || !medicineReport.ContainsKey("count"))
{
break;
}
var isExpiringMedicine = medicineReport.TryGetValue("is_expiring", out var isExpiringMedicineToken) && (bool)isExpiringMedicineToken;
int medicineCount = medicineReport.TryGetValue("count", out var medicineCountToken) ? (int)medicineCountToken : -1;
if (medicineCount == -1)
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MedicineUsed") + " Unknown times", UiLogColor.Error);
break;
}
string medicineLog;
if (!isExpiringMedicine)
{
MedicineUsedTimes += medicineCount;
medicineLog = LocalizationHelper.GetString("MedicineUsed") + $" {MedicineUsedTimes}(+{medicineCount})";
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySaverGroup, medicineCount);
}
else
{
ExpiringMedicineUsedTimes += medicineCount;
medicineLog = LocalizationHelper.GetString("ExpiringMedicineUsed") + $" {ExpiringMedicineUsedTimes}(+{medicineCount})";
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySaverGroup, medicineCount);
AchievementTrackerHelper.Instance.SetProgress(AchievementIds.SanityExpire, ExpiringMedicineUsedTimes);
}
Instances.TaskQueueViewModel.AddLog(medicineLog, UiLogColor.Info);
break;
case "StageQueueUnableToAgent":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails!["stage_code"]} " + LocalizationHelper.GetString("UnableToAgent"), UiLogColor.Info);
break;
case "StageQueueMissionCompleted":
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails!["stage_code"]} - {subTaskDetails["stars"]} ★", UiLogColor.Info);
break;
}
}
private static void ProcRecruitCalcMsg(JObject details)
{
Instances.ToolboxViewModel.ProcRecruitMsg(details);
}
private static void ProcVideoRecMsg(JObject details)
{
string what = details["what"]?.ToString() ?? string.Empty;
switch (what)
{
case "Finished":
var filename = details["details"]?["filename"];
Instances.CopilotViewModel.AddLog("Save to: " + filename, UiLogColor.Info, showTime: false);
// string p = @"C:\tmp\this path contains spaces, and,commas\target.txt";
string args = $"/e, /select, \"{filename}\"";
ProcessStartInfo info = new() {
FileName = "explorer",
Arguments = args,
};
Process.Start(info);
break;
}
}
private static async Task ProcReportRequest(JObject details)
{
string? url = (string?)details["url"];
if (string.IsNullOrEmpty(url))
{
_logger.Error("Report request received with empty URL.");
return;
}
string subTask = details["subtask"]?.ToString() ?? string.Empty;
// 企鹅物流不收集 txwy 客户端数据
if (SettingsViewModel.GameSettings.ClientType == ClientType.Txwy && (subTask == "ReportToPenguinStats"))
{
_logger.Information("PenguinStats report skipped for txwy client type.");
return;
}
var headersToken = details["headers"];
Dictionary<string, string> headers = [];
if (headersToken is JObject headersObj)
{
foreach (var prop in headersObj.Properties())
{
headers[prop.Name] = prop.Value.ToString();
}
}
string? body = (string?)details["body"];
if (string.IsNullOrEmpty(body))
{
_logger.Error("Report request received with empty body.");
return;
}
var content = new StringContent(body, Encoding.UTF8, "application/json");
bool success = false;
try
{
success = await GameDataReportService.PostWithRetryAsync(url, content, headers, subTask, penguinId => {
if (string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.PenguinId))
{
SettingsViewModel.GameSettings.PenguinId = penguinId;
}
_logger.Information("New PenguinId got: {PenguinId}", penguinId);
});
}
catch (Exception ex)
{
_logger.Warning(ex, "Failed to report: {Url}", url);
}
if (!success)
{
Instances.TaskQueueViewModel.AddLog("Failed to report, " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning);
}
}
public bool AsstSetInstanceOption(InstanceOptionKey key, string value)
{
return AsstSetInstanceOption(_handle, (AsstInstanceOptionKey)key, value);
}
public bool AsstSetStaticOption(AsstStaticOptionKey key, string value)
{
return MaaService.AsstSetStaticOption(key, value);
}
/// <summary>
/// 使用 TCP 或 adb devices 命令检查连接。TCP 检测相比 adb devices 更快,但不支持实体机。
/// </summary>
/// <param name="adbPath">adb path用于实体机检测</param>
/// <param name="address">连接地址</param>
/// <returns>设备是否在线</returns>
private static bool CheckConnection(string adbPath, string? address)
{
if (string.IsNullOrEmpty(address))
{
return false;
}
// 实体机可能设备名 -> [host]
if (!address.Contains(':') && !address.Contains('-'))
{
return WinAdapter.GetAdbAddresses(adbPath).Contains(address);
}
// normal -> [host]:[port]
// LdPlayer -> emulator-[port]
string[] hostAndPort = address.Split([':', '-'], StringSplitOptions.RemoveEmptyEntries);
if (hostAndPort.Length != 2 || !int.TryParse(hostAndPort[1], out var port))
{
return false;
}
string host = hostAndPort[0];
if (host.StartsWith("emulator"))
{
host = "127.0.0.1";
port += 1;
}
using var client = new TcpClient();
try
{
IAsyncResult result = client.BeginConnect(host, port, null, null);
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(0.5));
if (success)
{
client.EndConnect(result);
return true;
}
client.Close();
return false;
}
catch
{
return false;
}
}
/// <summary>
/// 连接模拟器。
/// </summary>
/// <param name="error">具体的连接错误。</param>
/// <returns>是否成功。</returns>
public bool AsstConnect(ref string error)
{
// 如果启用了 AttachWindow 模式,则使用窗口绑定而非 ADB 连接
if (SettingsViewModel.ConnectSettings.UseAttachWindow)
{
return AsstAttachWindowConnect(ref error);
}
return AsstAdbConnect(ref error);
}
/// <summary>
/// 搜索指定窗口标题的窗口句柄。
/// </summary>
/// <param name="windowName">窗口标题(完全匹配)。</param>
/// <returns>找到的窗口句柄列表。</returns>
private static List<IntPtr> FindWindowsByName(string windowName)
{
var results = new List<IntPtr>();
PInvoke.EnumWindows((hWnd, lParam) => {
if (!PInvoke.IsWindowVisible(hWnd))
{
return true;
}
var len = PInvoke.GetWindowTextLength(hWnd);
if (len <= 0)
{
return true;
}
Span<char> buffer = len <= 1024 ? stackalloc char[len + 1] : new char[len + 1];
int written = PInvoke.GetWindowText(hWnd, buffer);
var title = new string(buffer[..Math.Max(0, Math.Min(written, buffer.Length))]);
if (title == windowName)
{
results.Add((IntPtr)hWnd);
}
return true;
}, IntPtr.Zero);
return results;
}
/// <summary>
/// 通过 AttachWindow 绑定 Win32 窗口。
/// 自动搜索 "明日方舟" 窗口。
/// </summary>
/// <param name="error">具体的连接错误。</param>
/// <returns>是否成功。</returns>
private bool AsstAttachWindowConnect(ref string error)
{
_lastConnectionError = string.Empty;
if (!Bootstrapper.IsUserAdministrator())
{
var result = Application.Current.Dispatcher.Invoke(() =>
MessageBoxHelper.Show(
LocalizationHelper.GetString("AttachWindowNeedAdmin"),
"MAA",
MessageBoxButton.YesNo,
MessageBoxImage.Question));
if (result == MessageBoxResult.No)
{
error = LocalizationHelper.GetString("RestartAsAdminFailed");
return false;
}
Bootstrapper.RestartAsAdmin();
return false;
}
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UseAttachWindowWarning"), UiLogColor.Warning);
const string TargetWindowName = "明日方舟";
var foundWindows = FindWindowsByName(TargetWindowName);
if (foundWindows.Count == 0)
{
error = string.Format(LocalizationHelper.GetString("AttachWindowNotFound"), TargetWindowName);
Instances.TaskQueueViewModel.AddLog(error, UiLogColor.Error);
_logger.Warning("AttachWindow: No window found with name {WindowName}", TargetWindowName);
return false;
}
var hwnd = foundWindows[0];
if (foundWindows.Count > 1)
{
// 找到多个窗口,使用第一个并记录日志
var multipleMsg = string.Format(LocalizationHelper.GetString("AttachWindowMultipleFound"), foundWindows.Count, TargetWindowName);
Instances.TaskQueueViewModel.AddLog(multipleMsg, UiLogColor.Info);
_logger.Warning("AttachWindow: Multiple windows found with name {WindowName}, count: {Count}, using first one: {Hwnd}", TargetWindowName, foundWindows.Count, hwnd);
}
else
{
var foundMsg = string.Format(LocalizationHelper.GetString("AttachWindowFound"), TargetWindowName);
Instances.TaskQueueViewModel.AddLog(foundMsg, UiLogColor.Info);
_logger.Information("AttachWindow: Found window \"{WindowName}\" with HWND: {Hwnd}", TargetWindowName, hwnd);
}
if (!ulong.TryParse(SettingsViewModel.ConnectSettings.AttachWindowScreencapMethod, out var screencapMethod))
{
screencapMethod = 2; // 默认 FramePool
}
if (!ulong.TryParse(SettingsViewModel.ConnectSettings.AttachWindowMouseMethod, out var mouseMethod))
{
mouseMethod = 32; // 默认 SendMessageWithCursorPos
}
if (!ulong.TryParse(SettingsViewModel.ConnectSettings.AttachWindowKeyboardMethod, out var keyboardMethod))
{
keyboardMethod = 2; // 默认 SendMessage
}
bool ret = AsstAttachWindow(_handle, hwnd, screencapMethod, mouseMethod, keyboardMethod);
if (!ret)
{
// 等待回调完成以获取详细错误信息
System.Threading.Thread.Sleep(100);
if (!string.IsNullOrEmpty(_lastConnectionError))
{
error = _lastConnectionError;
}
else
{
error = LocalizationHelper.GetString("AttachWindowFailed");
}
Instances.TaskQueueViewModel.AddLog(error, UiLogColor.Error);
}
return ret;
}
/// <summary>
/// 通过 ADB 连接模拟器。
/// </summary>
/// <param name="error">具体的连接错误。</param>
/// <returns>是否成功。</returns>
private bool AsstAdbConnect(ref string error)
{
_lastConnectionError = string.Empty;
switch (SettingsViewModel.ConnectSettings.ConnectConfig)
{
case "MuMuEmulator12":
AsstSetConnectionExtrasMuMu12(SettingsViewModel.ConnectSettings.MuMuEmulator12Extras.Config);
break;
case "LDPlayer":
AsstSetConnectionExtrasLdPlayer(SettingsViewModel.ConnectSettings.LdPlayerExtras.Config);
break;
}
if (SettingsViewModel.ConnectSettings.AutoDetectConnection)
{
if (!AutoDetectConnection(ref error))
{
return false;
}
}
if (Connected && _connectedAdb == SettingsViewModel.ConnectSettings.AdbPath &&
_connectedAddress == SettingsViewModel.ConnectSettings.ConnectAddress)
{
var actualConnectionStatus = CheckConnection(_connectedAdb, _connectedAddress);
if (!actualConnectionStatus)
{
Connected = false;
_logger.Information("Connection lost to {ConnectedAdb} {ConnectedAddress}", _connectedAdb, _connectedAddress);
error = "Connection lost";
}
else
{
_logger.Information("Already connected to {ConnectedAdb} {ConnectedAddress}", _connectedAdb, _connectedAddress);
if (!Instances.TaskQueueViewModel.EnableAutoReload)
{
return true;
}
_logger.Information("Forced reload resource");
if (!LoadResource())
{
error = "Load Resource Failed";
return false;
}
ToastNotification.ShowDirect("Auto Reload");
return true;
}
}
bool ret = AsstConnect(_handle, SettingsViewModel.ConnectSettings.AdbPath, SettingsViewModel.ConnectSettings.ConnectAddress, SettingsViewModel.ConnectSettings.ConnectConfig);
// 如果连接失败,等待回调完成以获取详细错误信息
if (!ret)
{
System.Threading.Thread.Sleep(100);
}
// 尝试默认的备选端口
if (!ret && SettingsViewModel.ConnectSettings.AutoDetectConnection)
{
if (SettingsViewModel.ConnectSettings.DefaultAddress.TryGetValue(SettingsViewModel.ConnectSettings.ConnectConfig, out var value))
{
foreach (var address in value
.TakeWhile(_ => !_runningState.GetIdle()))
{
ret = AsstConnect(_handle, SettingsViewModel.ConnectSettings.AdbPath, address, SettingsViewModel.ConnectSettings.ConnectConfig);
if (!ret)
{
continue;
}
SettingsViewModel.ConnectSettings.ConnectAddress = address;
break;
}
}
else
{
Execute.OnUIThreadAsync(
() => {
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AutoDetectConnectionNotSupported"), UiLogColor.Error);
});
}
}
if (!ret)
{
// 等待回调完成以获取详细错误信息
System.Threading.Thread.Sleep(1000);
if (!string.IsNullOrEmpty(_lastConnectionError))
{
error = _lastConnectionError;
}
else
{
error = LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("CheckSettings");
}
}
else if (SettingsViewModel.ConnectSettings.AutoDetectConnection && !SettingsViewModel.ConnectSettings.AlwaysAutoDetectConnection)
{
SettingsViewModel.ConnectSettings.AutoDetectConnection = false;
}
return ret;
}
private static bool AutoDetectConnection(ref string error)
{
var adbPath = SettingsViewModel.ConnectSettings.AdbPath;
bool adbResult = !string.IsNullOrEmpty(adbPath) &&
File.Exists(adbPath) &&
Path.GetFileName(adbPath).Contains("adb", StringComparison.InvariantCultureIgnoreCase) &&
CheckConnection(adbPath, SettingsViewModel.ConnectSettings.ConnectAddress);
if (adbResult)
{
error = string.Empty;
return true;
}
// 蓝叠的特殊处理
{
string bsHvAddress = SettingsViewModel.ConnectSettings.TryToSetBlueStacksHyperVAddress() ?? string.Empty;
bool bsResult = CheckConnection(adbPath, bsHvAddress);
if (bsResult)
{
error = string.Empty;
if (string.IsNullOrEmpty(SettingsViewModel.ConnectSettings.AdbPath) && SettingsViewModel.ConnectSettings.DetectAdbConfig(ref error))
{
return string.IsNullOrEmpty(error);
}
SettingsViewModel.ConnectSettings.ConnectAddress = bsHvAddress;
return true;
}
}
if (SettingsViewModel.ConnectSettings.DetectAdbConfig(ref error))
{
// https://github.com/MaaAssistantArknights/MaaAssistantArknights/issues/8547
// DetectAdbConfig 会把 ConnectAddress 变成第一个不是 emulator 开头的地址,可能会存在多开问题
error = string.Empty;
return true;
}
return false;
}
private AsstTaskId AsstAppendTaskWithEncoding(AsstTaskType type, JObject? taskParams = null)
{
taskParams ??= [];
return AsstAppendTask(_handle, type.ToString(), JsonConvert.SerializeObject(taskParams));
}
private bool AsstSetTaskParamsWithEncoding(AsstTaskId id, JObject? taskParams = null)
{
if (id == 0)
{
return false;
}
taskParams ??= [];
return AsstSetTaskParams(_handle, id, JsonConvert.SerializeObject(taskParams));
}
/// <summary>
/// WPF 区分任务的类型
/// </summary>
public enum TaskType
{
/// <summary>开始唤醒</summary>
StartUp,
/// <summary>关闭游戏</summary>
CloseDown,
/// <summary>理智作战</summary>
Fight,
/// <summary>自动公招</summary>
Recruit,
/// <summary>基建</summary>
Infrast,
/// <summary>获取信用点/访问好友/信用商店</summary>
Mall,
/// <summary>领奖励/邮箱/幸运墙等</summary>
Award,
/// <summary>自动肉鸽</summary>
Roguelike,
/// <summary>公招识别</summary>
RecruitCalc,
/// <summary>自动战斗</summary>
Copilot,
/*
/// <summary>视频识别(真有人用吗)</summary>
VideoRec,
*/
/// <summary>仓库识别</summary>
Depot,
/// <summary>干员识别</summary>
OperBox,
/// <summary>抽卡</summary>
Gacha,
/// <summary>生息演算</summary>
Reclamation,
/// <summary>小游戏</summary>
MiniGame,
/// <summary>自定义任务s</summary>
Custom,
}
private readonly HashSet<TaskType> _mainTaskTypes =
[
TaskType.StartUp,
TaskType.Fight,
TaskType.Recruit,
TaskType.Infrast,
TaskType.Mall,
TaskType.Award,
TaskType.Roguelike,
TaskType.Reclamation,
];
private readonly ObservableDictionary<AsstTaskId, (TaskType Type, TaskStatus Status)> _tasksStatus = [];
public IReadOnlyDictionary<AsstTaskId, (TaskType Type, TaskStatus Status)> TasksStatus => new Dictionary<AsstTaskId, (TaskType, TaskStatus)>(_tasksStatus);
private bool UpdateTaskStatus(AsstTaskId id, TaskStatus status)
{
if (id <= 0)
{
return false;
}
if (!_tasksStatus.TryGetValue(id, out var value))
{
_logger.Error("Task ID {TaskId} not found in _tasksStatus", id);
return false;
}
if (value.Status == TaskStatus.Idle && status == TaskStatus.InProgress)
{
RunningState.Instance.ResetTimeout(); // 进入新任务时重置超时计时
}
_tasksStatus[id] = (value.Type, status);
Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(item => item.TaskId == id)?.Status = (int)status;
if (status == TaskStatus.InProgress)
{
TaskSettingVisibilityInfo.Instance.NotifyOfTaskStatus();
}
return true;
}
public bool AsstAppendCloseDown(string clientType)
{
if (!AsstStop())
{
_logger.Warning("Failed to stop Asst");
}
var (type, param) = new AsstCloseDownTask() { ClientType = clientType }.Serialize();
return AsstAppendTaskWithEncoding(TaskType.CloseDown, type, param);
}
/// <summary>
/// <c>CloseDown</c> 任务。
/// </summary>
/// <param name="clientType">客户端版本。</param>
/// <returns>是否成功。</returns>
public bool AsstStartCloseDown(string clientType)
{
return AsstAppendCloseDown(clientType) && AsstStart();
}
public bool AsstBackToHome()
{
return MaaService.AsstBackToHome(_handle);
}
/// <summary>
/// 仓库识别。
/// </summary>
/// <returns>是否成功。</returns>
public bool AsstStartDepot()
{
return AsstAppendTaskWithEncoding(TaskType.Depot, AsstTaskType.Depot) && AsstStart();
}
/// <summary>
/// 干员识别。
/// </summary>
/// <returns>是否成功。</returns>
public bool AsstStartOperBox()
{
return AsstAppendTaskWithEncoding(TaskType.OperBox, AsstTaskType.OperBox) && AsstStart();
}
/// <summary>
/// 牛牛抽卡。
/// </summary>
/// <param name="once">是否为单抽,默认为 true</param>
/// <returns>是否成功。</returns>
public bool AsstStartGacha(bool once = true)
{
var task = new AsstCustomTask() {
CustomTasks = [once ? "GachaOnce" : "GachaTenTimes"],
};
var (type, param) = task.Serialize();
return AsstAppendTaskWithEncoding(TaskType.Gacha, type, param) && AsstStart();
}
/// <summary>
/// 小游戏。
/// </summary>
/// <param name="taskName">任务名tasks.json 中的 key</param>
/// <returns>是否成功。</returns>
public bool AsstMiniGame(string taskName)
{
var task = new AsstCustomTask() {
CustomTasks = [taskName],
};
var (type, param) = task.Serialize();
return AsstAppendTaskWithEncoding(TaskType.MiniGame, type, param) && AsstStart();
}
/*
/// <summary>
/// 视频识别。
/// </summary>
/// <param name="filename">文件路径</param>
/// <returns>是否成功。</returns>
public bool AsstStartVideoRec(string filename)
{
var taskParams = new JObject {
["filename"] = filename,
};
AsstTaskId id = AsstAppendTaskWithEncoding(AsstTaskType.VideoRecognition, taskParams);
_tasksStatus.Add(id, (TaskType.Copilot, TaskStatus.Idle));
return id != 0 && AsstStart();
}
*/
public bool AsstAppendTaskWithEncoding(TaskType wpfTasktype, AsstBaseTask task)
{
return AsstAppendTaskWithEncoding(wpfTasktype, task.Serialize());
}
public bool AsstAppendTaskWithEncoding(TaskType wpfTaskType, (AsstTaskType Type, JObject? TaskParams) task)
{
return AsstAppendTaskWithEncoding(wpfTaskType, task.Type, task.TaskParams);
}
public bool AsstAppendTaskWithEncoding(TaskType wpfTaskType, AsstTaskType type, JObject? taskParams = null)
{
taskParams ??= [];
AsstTaskId id = AsstAppendTask(_handle, type.ToString(), JsonConvert.SerializeObject(taskParams));
if (id == 0)
{
return false;
}
_tasksStatus.Add(id, (wpfTaskType, TaskStatus.Idle));
return true;
}
public bool AsstSetTaskParamsEncoded(AsstTaskId id, AsstBaseTask task)
{
return AsstSetTaskParamsEncoded(id, task.Serialize().Params);
}
public bool AsstSetTaskParamsEncoded(AsstTaskId id, JObject? taskParams = null)
{
if (id == 0)
{
return false;
}
taskParams ??= [];
return AsstSetTaskParams(_handle, id, JsonConvert.SerializeObject(taskParams));
}
/// <summary>
/// 启动。
/// </summary>
/// <returns>是否成功。</returns>
public bool AsstStart()
{
return MaaService.AsstStart(_handle);
}
/// <summary>
/// 运行中。
/// </summary>
/// <returns>是否正在运行。</returns>
public bool AsstRunning()
{
return MaaService.AsstRunning(_handle);
}
/// <summary>
/// 停止。
/// </summary>
/// <returns>是否成功。</returns>
public bool AsstStop()
{
return MaaService.AsstStop(_handle);
}
/// <summary>
/// 销毁。
/// </summary>
public void AsstDestroy()
{
MaaService.AsstDestroy(_handle);
}
}
/// <summary>
/// MaaCore 消息。
/// </summary>
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public enum AsstMsg
{
/* Global Info */
/// <summary>
/// 内部错误。
/// </summary>
InternalError = 0,
/// <summary>
/// 初始化失败。
/// </summary>
InitFailed,
/// <summary>
/// 连接相关错误。
/// </summary>
ConnectionInfo,
/// <summary>
/// 全部任务完成。
/// </summary>
AllTasksCompleted,
/// <summary>
/// 外部异步调用信息
/// </summary>
AsyncCallInfo,
/// <summary>
/// 实例已销毁
/// </summary>
Destroyed,
/* TaskChain Info */
/// <summary>
/// 任务链执行/识别错误。
/// </summary>
TaskChainError = 10000,
/// <summary>
/// 任务链开始。
/// </summary>
TaskChainStart,
/// <summary>
/// 任务链完成。
/// </summary>
TaskChainCompleted,
/// <summary>
/// 任务链额外信息。
/// </summary>
TaskChainExtraInfo,
/// <summary>
/// 任务链手动停止
/// </summary>
TaskChainStopped,
/* SubTask Info */
/// <summary>
/// 原子任务执行/识别错误。
/// </summary>
SubTaskError = 20000,
/// <summary>
/// 原子任务开始。
/// </summary>
SubTaskStart,
/// <summary>
/// 原子任务完成。
/// </summary>
SubTaskCompleted,
/// <summary>
/// 原子任务额外信息。
/// </summary>
SubTaskExtraInfo,
/// <summary>
/// 原子任务手动停止
/// </summary>
SubTaskStopped,
/// <summary>
/// 上报请求
/// </summary>
ReportRequest = 30000,
}
/// <summary>
/// 任务状态
/// </summary>
public enum TaskStatus
{
/// <summary>
/// 未开始
/// </summary>
Idle = 0,
/// <summary>
/// 进行中
/// </summary>
InProgress = 1,
/// <summary>
/// 已完成
/// </summary>
Completed = 2,
/// <summary>
/// 错误终止
/// </summary>
Error = 3,
}
public enum AsstStaticOptionKey
{
/// <summary>
/// 无效
/// </summary>
Invalid,
/// <summary>
/// 用CPU进行OCR
/// </summary>
CpuOCR,
/// <summary>
/// 用GPU进行OCR
/// </summary>
GpuOCR,
}
public enum InstanceOptionKey
{
/* Deprecated */ // MinitouchEnabled = 1,
/// <summary>
/// Indicates the touch mode.
/// </summary>
TouchMode = 2,
/// <summary>
/// Indicates whether the deployment should be paused.
/// </summary>
DeploymentWithPause = 3,
/// <summary>
/// Indicates whether AdbLite is used.
/// </summary>
AdbLiteEnabled = 4,
/// <summary>
/// Indicates whether the ADB server process should be killed when the instance is exited.
/// </summary>
KillAdbOnExit = 5,
}