Files
MaaAssistantArknights/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs
2025-04-08 10:53:23 +08:00

815 lines
30 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="RemoteControlService.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and 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>
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using MaaWpfGui.Constants;
using MaaWpfGui.Helper;
using MaaWpfGui.States;
using MaaWpfGui.ViewModels.UI;
using MaaWpfGui.ViewModels.UserControl.Settings;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using Stylet;
namespace MaaWpfGui.Services.RemoteControl
{
/// <summary>
/// The view model of remote control.
/// </summary>
// 通过 container.Get<RemoteControlService>(); 实例化或获取实例
// ReSharper disable once ClassNeverInstantiated.Global
public class RemoteControlService
{
private Task _pollJobTask = Task.CompletedTask;
private readonly List<string> _enqueueTaskIds = [];
private readonly ConcurrentQueue<JObject> _sequentialTaskQueue = new();
private readonly ConcurrentQueue<JObject> _instantTaskQueue = new();
private Task _executeSequentialJobTask = Task.CompletedTask;
private Task _executeInstantJobTask = Task.CompletedTask;
private readonly RunningState _runningState;
private bool _inited = false;
private string _currentSequentialTaskId = string.Empty;
private static RemoteControlUserControlModel RemoteSettings => SettingsViewModel.RemoteControlSettings;
public RemoteControlService()
{
InitializePollJobTask();
_runningState = RunningState.Instance;
}
public void InitializePollJobTask()
{
if (_inited)
{
return;
}
if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri))
{
return;
}
_inited = true;
_pollJobTask = _pollJobTask.ContinueWith(async _ =>
{
while (true)
{
await Task.Delay(1000);
try
{
if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri))
{
Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return");
_inited = false;
return;
}
await PollJobTaskLoop();
}
catch (Exception ex)
{
Log.Logger.Error(ex, "RemoteControl service raises unknown error.");
}
}
// ReSharper disable once FunctionNeverReturns
});
_executeSequentialJobTask = _executeSequentialJobTask.ContinueWith(async _ =>
{
while (true)
{
await Task.Delay(1000);
try
{
if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri))
{
Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return");
return;
}
await ExecuteSequentialJobLoop();
}
catch (Exception ex)
{
Log.Logger.Error(ex, "RemoteControl service raises unknown error.");
}
}
// ReSharper disable once FunctionNeverReturns
});
_executeInstantJobTask = _executeInstantJobTask.ContinueWith(async _ =>
{
while (true)
{
await Task.Delay(1000);
try
{
if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri))
{
Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return");
return;
}
await ExecuteInstantJobLoop();
}
catch (Exception ex)
{
Log.Logger.Error(ex, "RemoteControl service raises unknown error.");
}
}
// ReSharper disable once FunctionNeverReturns
});
}
#region Private Method Invoker
private static T GetPrivateFieldValue<T>(object instance, string fieldName)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
if (string.IsNullOrEmpty(fieldName))
{
throw new ArgumentNullException(nameof(fieldName));
}
Type type = instance.GetType();
FieldInfo fieldInfo = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
if (fieldInfo == null)
{
throw new ArgumentException($"Field '{fieldName}' not found in type '{type.FullName}'.");
}
if (!typeof(T).IsAssignableFrom(fieldInfo.FieldType))
{
throw new ArgumentException($"Field '{fieldName}' is not of type {typeof(T)}.");
}
return (T)fieldInfo.GetValue(instance);
}
private static void InvokeInstanceMethod(object instance, string methodName)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentNullException(nameof(methodName));
}
Type type = instance.GetType();
MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (methodInfo == null)
{
throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'.");
}
methodInfo.Invoke(instance, null);
}
private static T InvokeInstanceFunction<T>(object instance, string methodName)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentNullException(nameof(methodName));
}
Type type = instance.GetType();
MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (methodInfo == null)
{
throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'.");
}
if (!typeof(T).IsAssignableFrom(methodInfo.ReturnType))
{
throw new ArgumentException($"Method '{methodName}' is not {typeof(T)}.");
}
return (T)methodInfo.Invoke(instance, null);
}
private static async Task<T> InvokeInstanceAsyncFunction<T>(object instance, string methodName)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentNullException(nameof(methodName));
}
Type type = instance.GetType();
MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (methodInfo == null)
{
throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'.");
}
// 检查方法是否是异步方法 (返回Task或Task<T>)
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new ArgumentException($"Method '{methodName}' is not asynchronous.");
}
return await (Task<T>)methodInfo.Invoke(instance, null);
}
private static TResult InvokeStaticFunction<TResult>(Type staticType, string methodName)
{
if (staticType == null)
{
throw new ArgumentNullException(nameof(staticType));
}
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentNullException(nameof(methodName));
}
MethodInfo methodInfo = staticType.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
if (methodInfo == null)
{
throw new ArgumentException($"Method '{methodName}' not found in type '{staticType.FullName}'.");
}
return (TResult)methodInfo.Invoke(null, null);
}
#endregion
#region JobLoops
private async Task PollJobTaskLoop()
{
var endpoint = RemoteSettings.RemoteControlGetTaskEndpointUri;
if (!IsEndpointValid(endpoint))
{
return;
}
var uid = RemoteSettings.RemoteControlUserIdentity;
var did = RemoteSettings.RemoteControlDeviceIdentity;
var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new { user = uid, device = did });
if (response == null)
{
Log.Logger.Error("RemoteControlService endpoint failed.");
return;
}
var jsonObject = JsonConvert.DeserializeObject<JObject>(response);
// A list of task
if (jsonObject?.GetValue("tasks") is JArray tasks)
{
foreach (var task in tasks.OfType<JObject>())
{
var type = task.GetValue("type")?.Value<string>();
if (string.IsNullOrWhiteSpace(type))
{
continue;
}
// It is a valid task
var id = task.GetValue("id")?.Value<string>();
if (_enqueueTaskIds.Contains(id))
{
continue;
}
_enqueueTaskIds.Add(id);
switch (type)
{
case "LinkStart":
case "LinkStart-Base":
case "LinkStart-WakeUp":
case "LinkStart-Combat":
case "LinkStart-Recruiting":
case "LinkStart-Mall":
case "LinkStart-Mission":
case "LinkStart-AutoRoguelike":
case "LinkStart-Reclamation":
case "Toolbox-GachaOnce":
case "Toolbox-GachaTenTimes":
case "CaptureImage":
case "Settings-ConnectAddress":
case "Settings-Stage1":
_sequentialTaskQueue.Enqueue(task);
break;
case "CaptureImageNow":
case "HeartBeat":
case "StopTask":
_instantTaskQueue.Enqueue(task);
break;
}
}
}
}
private async Task ExecuteSequentialJobLoop()
{
if (_sequentialTaskQueue.TryDequeue(out var task))
{
var type = task.GetValue("type")?.Value<string>();
var id = task.GetValue("id")?.Value<string>();
var data = task.GetValue("params")?.Value<string>();
var payload = string.Empty;
var status = "SUCCESS";
_currentSequentialTaskId = id;
switch (type)
{
case "LinkStart":
{
// 一键长草特殊任务
await _runningState.UntilIdleAsync();
var startLogStr = string.Format(LocalizationHelper.GetString("RemoteControlReceivedTask"), type, id);
Instances.TaskQueueViewModel.AddLog(startLogStr);
await Execute.OnUIThreadAsync(() =>
{
Instances.TaskQueueViewModel.LinkStart();
});
await _runningState.UntilIdleAsync();
var stopLogStr = string.Format(LocalizationHelper.GetString("RemoteControlCompletedTask"), type, id);
Instances.TaskQueueViewModel.AddLog(stopLogStr);
break;
}
case "LinkStart-Base":
case "LinkStart-WakeUp":
case "LinkStart-Combat":
case "LinkStart-Recruiting":
case "LinkStart-Mall":
case "LinkStart-Mission":
case "LinkStart-AutoRoguelike":
case "LinkStart-Reclamation":
{
await LinkStart(new[] { type.Split('-')[1] });
break;
}
case "Toolbox-GachaOnce":
{
await _runningState.UntilIdleAsync();
Instances.RecognizerViewModel.GachaOnce();
await _runningState.UntilIdleAsync();
break;
}
case "Toolbox-GachaTenTimes":
{
await _runningState.UntilIdleAsync();
Instances.RecognizerViewModel.GachaTenTimes();
await _runningState.UntilIdleAsync();
break;
}
case "CaptureImage":
{
string errMsg = string.Empty;
bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg));
if (connected)
{
var image = Instances.AsstProxy.AsstGetImage();
if (image == null)
{
status = "FAILED";
break;
}
byte[] bytes;
using (MemoryStream stream = new MemoryStream())
{
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(image));
encoder.Save(stream);
bytes = stream.ToArray();
}
payload = Convert.ToBase64String(bytes);
break;
}
status = "FAILED";
break;
}
case "Settings-ConnectAddress":
// ConfigurationHelper.SetValue(type.Split('-')[1], data);
await Execute.OnUIThreadAsync(() =>
{
SettingsViewModel.ConnectSettings.ConnectAddress = data;
});
break;
case "Settings-Stage1":
await Execute.OnUIThreadAsync(() =>
{
TaskQueueViewModel.FightTask.Stage1 = data;
});
break;
// ReSharper disable once RedundantEmptySwitchSection
default:
// 未知的Type统一直接发给MAACore
// No! 未知的任务一概不处理
break;
}
var endpoint = RemoteSettings.RemoteControlReportStatusUri;
if (IsEndpointValid(endpoint))
{
var uid = RemoteSettings.RemoteControlUserIdentity;
var did = RemoteSettings.RemoteControlDeviceIdentity;
var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new
{
user = uid,
device = did,
status,
task = id,
payload,
});
if (response == null)
{
Log.Logger.Error("RemoteControlService report task failed.");
}
}
_currentSequentialTaskId = string.Empty;
}
}
private async Task ExecuteInstantJobLoop()
{
if (_instantTaskQueue.TryDequeue(out var task))
{
var type = task.GetValue("type")?.Value<string>();
var id = task.GetValue("id")?.Value<string>();
// 当前没有任务需要读取params,未来需要时可以取消注释
// var data = task.GetValue("params")?.Value<string>();
var payload = string.Empty;
var status = "SUCCESS";
switch (type)
{
case "HeartBeat":
{
payload = _currentSequentialTaskId;
break;
}
case "StopTask":
{
await Task.Run(() =>
{
if (!Instances.AsstProxy.AsstStop())
{
// 无法确定当前的界面找不到借用的UI位置因此只能Log
Log.Logger.Error("Failed to stop Asst.");
}
});
// 无需等待,甩出任务即可返回,远端应该用心跳来确认界面卡死和取消是否成功。
break;
}
case "CaptureImageNow":
{
string errMsg = string.Empty;
bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg));
if (connected)
{
var image = Instances.AsstProxy.AsstGetImage();
if (image == null)
{
status = "FAILED";
break;
}
byte[] bytes;
using (MemoryStream stream = new MemoryStream())
{
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(image));
encoder.Save(stream);
bytes = stream.ToArray();
}
payload = Convert.ToBase64String(bytes);
break;
}
status = "FAILED";
break;
}
// ReSharper disable once RedundantEmptySwitchSection
default:
// 未知的Type统一直接发给MAACore
// No! 未知的任务一概不处理
break;
}
var endpoint = RemoteSettings.RemoteControlReportStatusUri;
if (IsEndpointValid(endpoint))
{
var uid = RemoteSettings.RemoteControlUserIdentity;
var did = RemoteSettings.RemoteControlDeviceIdentity;
var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new
{
user = uid,
device = did,
status,
task = id,
payload,
});
if (response == null)
{
Log.Logger.Error("RemoteControlService report task failed.");
}
}
}
}
#endregion
/// <summary>
/// 根据"一键长草"功能进行修改的方法。
/// </summary>
/// <remarks>
/// <para>注意以下特点:</para>
/// <para>- 可以在非UI线程执行。</para>
/// <para>- 不调用StartScript。</para>
/// <para>- 不使用Model里的列表。</para>
/// <para>- 在结尾添加对RunningStatus的等待。</para>
/// <para>若"一键长草"功能在未来有更新,此方法也应进行相应的同步更新,但需确保上述特点保持不变。</para>
/// </remarks>
/// <param name="originalNames">指定的任务列表。</param>
/// <returns>异步任务,无返回结果。</returns>
// 这个是不是可以直接做到 TaskQueueViewModel 里面去?
private async Task LinkStart(IEnumerable<string> originalNames)
{
await _runningState.UntilIdleAsync();
_runningState.SetIdle(false);
await Execute.OnUIThreadAsync(async () =>
{
// 虽然更改时已经保存过了,不过保险起见还是在点击开始之后再保存一次(任务及基建列表)
Instances.TaskQueueViewModel.TaskItemSelectionChanged();
TaskQueueViewModel.InfrastTask.InfrastOrderSelectionChanged();
InvokeInstanceMethod(Instances.TaskQueueViewModel, "ClearLog");
/*await Task.Run(() => Instances.SettingsViewModel.RunScript("StartsWithScript"));*/
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ConnectingToEmulator"));
// 一般是点了“停止”按钮了
if (Instances.TaskQueueViewModel.Stopping)
{
Instances.TaskQueueViewModel.SetStopped();
return;
}
if (!await InvokeInstanceAsyncFunction<bool>(Instances.TaskQueueViewModel, "ConnectToEmulator"))
{
return;
}
// 一般是点了“停止”按钮了
if (Instances.TaskQueueViewModel.Stopping)
{
Instances.TaskQueueViewModel.SetStopped();
return;
}
bool taskRet = true;
// 直接遍历TaskItemViewModels里面的内容是排序后的
int count = 0;
foreach (var item in originalNames)
{
++count;
switch (item)
{
case "Base":
taskRet &= Instances.TaskQueueViewModel.AppendInfrast();
break;
case "WakeUp":
taskRet &= TaskQueueViewModel.AppendStart();
break;
case "Combat":
taskRet &= Instances.TaskQueueViewModel.AppendFight();
break;
case "Recruiting":
taskRet &= TaskQueueViewModel.AppendRecruit();
break;
case "Mall":
taskRet &= TaskQueueViewModel.AppendMall();
break;
case "Mission":
taskRet &= TaskQueueViewModel.AppendAward();
break;
case "AutoRoguelike":
taskRet &= TaskQueueViewModel.AppendRoguelike();
break;
case "Reclamation":
taskRet &= TaskQueueViewModel.AppendReclamation();
break;
default:
--count;
// Instances.TaskQueueViewModel._logger.Error("Unknown task: " + item);
break;
}
if (taskRet)
{
continue;
}
Instances.TaskQueueViewModel.AddLog(item + "Error", UiLogColor.Error);
taskRet = true;
--count;
}
if (count == 0)
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UnselectedTask"));
_runningState.SetIdle(true);
Instances.TaskQueueViewModel.SetStopped();
return;
}
taskRet &= Instances.AsstProxy.AsstStart();
if (taskRet)
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Running"));
}
else
{
Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UnknownErrorOccurs"));
await Instances.TaskQueueViewModel.Stop();
Instances.TaskQueueViewModel.SetStopped();
}
});
await _runningState.UntilIdleAsync();
}
public static async Task ConnectionTest()
{
var endpoint = RemoteSettings.RemoteControlGetTaskEndpointUri;
if (!IsEndpointValid(endpoint, alarm: true))
{
return;
}
var uid = RemoteSettings.RemoteControlUserIdentity;
var did = RemoteSettings.RemoteControlDeviceIdentity;
try
{
var body = System.Text.Json.JsonSerializer.Serialize(new { user = uid, device = did });
var message = new HttpRequestMessage(HttpMethod.Post, endpoint);
message.Headers.Accept.ParseAdd("application/json");
message.Content = new StringContent(body, Encoding.UTF8, "application/json");
var client = GetPrivateFieldValue<HttpClient>(Instances.HttpService, "_client");
var response = await client.SendAsync(message);
if (response != null)
{
if (!response.IsSuccessStatusCode)
{
var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), response.StatusCode);
ToastNotification.ShowDirect(errorMsg);
return;
}
}
else
{
// 一般来说不会走到这里因为null response一定会报错
var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), "Unknown");
ToastNotification.ShowDirect(errorMsg);
return;
}
ToastNotification.ShowDirect(
LocalizationHelper.GetString("RemoteControlConnectionTestSuccess"));
}
catch (Exception e)
{
var error = e.Message;
if (e.InnerException != null)
{
error = e.InnerException.Message;
}
var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), error);
ToastNotification.ShowDirect(errorMsg);
}
}
public static void RegenerateDeviceIdentity()
{
RemoteSettings.RemoteControlDeviceIdentity = Guid.NewGuid().ToString("N");
}
public static bool IsEndpointValid(string endpoint, bool alarm = false)
{
if (string.IsNullOrWhiteSpace(endpoint))
{
ShowToast("RemoteControlConnectionTestFailEmpty", alarm);
return false;
}
string lowerEndpoint = endpoint.ToLower();
if (lowerEndpoint.StartsWith("https://"))
{
return true;
}
else if (lowerEndpoint.StartsWith("http://"))
{
ShowToast("RemoteControlConnectionTestWarningHttpUnsafe", alarm);
return true;
}
else
{
ShowToast("RemoteControlConnectionTestFailNotHttpOrHttps", alarm);
return false;
}
}
public static void ShowToast(string message, bool alarm)
{
if (alarm)
{
ToastNotification.ShowDirect(LocalizationHelper.GetString(message));
}
}
}
}