//
// 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
//
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
{
///
/// The view model of remote control.
///
// 通过 container.Get(); 实例化或获取实例
// ReSharper disable once ClassNeverInstantiated.Global
public class RemoteControlService
{
private Task _pollJobTask = Task.CompletedTask;
private readonly List _enqueueTaskIds = [];
private readonly ConcurrentQueue _sequentialTaskQueue = new();
private readonly ConcurrentQueue _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(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(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 InvokeInstanceAsyncFunction(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)
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new ArgumentException($"Method '{methodName}' is not asynchronous.");
}
return await (Task)methodInfo.Invoke(instance, null);
}
private static TResult InvokeStaticFunction(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(response);
// A list of task
if (jsonObject?.GetValue("tasks") is JArray tasks)
{
foreach (var task in tasks.OfType())
{
var type = task.GetValue("type")?.Value();
if (string.IsNullOrWhiteSpace(type))
{
continue;
}
// It is a valid task
var id = task.GetValue("id")?.Value();
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();
var id = task.GetValue("id")?.Value();
var data = task.GetValue("params")?.Value();
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();
var id = task.GetValue("id")?.Value();
// 当前没有任务需要读取params,未来需要时可以取消注释
// var data = task.GetValue("params")?.Value();
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
///
/// 根据"一键长草"功能进行修改的方法。
///
///
/// 注意以下特点:
/// - 可以在非UI线程执行。
/// - 不调用StartScript。
/// - 不使用Model里的列表。
/// - 在结尾添加对RunningStatus的等待。
/// 若"一键长草"功能在未来有更新,此方法也应进行相应的同步更新,但需确保上述特点保持不变。
///
/// 指定的任务列表。
/// 异步任务,无返回结果。
// 这个是不是可以直接做到 TaskQueueViewModel 里面去?
private async Task LinkStart(IEnumerable 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(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(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));
}
}
}
}