diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d19be8df..03bf0527a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,9 +15,9 @@ option(WITH_THRIFT_CONTROLLER "build with thrift controller" ON) option(WITH_DBG_CONTROLLER "build with debugging controller" ON) option(WITH_WIN32_CONTROLLER "build with win32 controller" ON) -option(WITH_GRPC "build with protobuf and grpc" ON) +option(WITH_GRPC "build with protobuf and grpc" OFF) -option(BUILD_GRPC_CLI "build grpc CLI exec" ON) +option(BUILD_GRPC_CLI "build grpc CLI exec" OFF) option(BUILD_SAMPLE "build a demo" OFF) diff --git a/docs/en_us/1.1-QuickStarted.md b/docs/en_us/1.1-QuickStarted.md index 4a51b8c4f..ec3004c5a 100644 --- a/docs/en_us/1.1-QuickStarted.md +++ b/docs/en_us/1.1-QuickStarted.md @@ -2,7 +2,7 @@ 1. Download the MaaFramework Release 2. Prepare Resource Files -3. Use a Generic GUI or Write Integration Code +3. Use a Generic CLI or Write Integration Code ## Download the MaaFramework Release @@ -22,10 +22,9 @@ my_resource │ ├── det.onnx │ ├── keys.txt │ └── rec.onnx -├── pipeline -│ ├── my_pipeline_1.json -│ └── my_pipeline_2.json -└── properties.json +└── pipeline + ├── my_pipeline_1.json + └── my_pipeline_2.json ``` You can modify the names of files and folders starting with "my_", but the others have fixed file names and should not be changed. Here's a breakdown: @@ -39,7 +38,7 @@ You can refer to the [Task Pipeline Protocol](3.1-PipelineProtocol.md) for writi Tools: - [JSON Schema](https://github.com/MaaAssistantArknights/MaaFramework/blob/main/tools/pipeline.schema.json) -- [Graphical Editor](https://github.com/MaaAssistantArknights/MaaJsonViewer) +- [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=nekosu.maa-support) ### Image Files @@ -57,31 +56,20 @@ You can use our pre-converted files: [MaaCommonAssets](https://github.com/MaaAss If needed, you can also fine-tune the official pre-trained models of PaddleOCR yourself (please refer to the official PaddleOCR documentation) and convert them to ONNX files for use. You can find conversion commands [here](https://github.com/MaaAssistantArknights/MaaCommonAssets/tree/main/OCR#command). -### Directory Property File +## Run -`properties.json` can be used to set properties for the current directory. A typical JSON structure is as follows: +You can integrate MaaFramework using MaaPiCli (Generic CLI) or by writing integration code yourself. -_Please note that JSON does not support comments, and the following is for demonstration purposes only. Do not copy and use directly._ +### Using MaaPiCli -```jsonc -{ - "is_base": true, // Whether it is a base resource - // If true, when reading resources in this directory, all previously read content will be cleared. -} -``` - -## Integration - -You can integrate MaaFramework using MaaY (Generic GUI) or by writing integration code yourself. - -### Using MaaY +Use MaaPiCli in the `bin` folder of the Release package, and write `interface.json` and place it in the same directory to use it. -Use [MaaY](https://github.com/MaaAssistantArknights/MaaY). Refer to the corresponding documentation in MaaY to organize the `.maay` folder and directly import and use it in the GUI. +The Cli has completed basic function development, and more functions are being continuously improved! Detailed documentation needs to be further improved. Currently, you can refer to sample to write it. Examples: -- [M9A.maay](https://github.com/MaaAssistantArknights/M9A/tree/main/assets/.maay) -- [MAS.maay](https://github.com/MaaAssistantArknights/MaaAssistantSkland/tree/main/.maay) +- [Sample](https://github.com/MaaAssistantArknights/MaaFramework/blob/main/sample/interface.json) +- [M9A](https://github.com/MaaAssistantArknights/M9A/tree/main/assets/interface.json) ### Writing Integration Code Yourself diff --git "a/docs/zh_cn/1.1-\345\277\253\351\200\237\345\274\200\345\247\213.md" "b/docs/zh_cn/1.1-\345\277\253\351\200\237\345\274\200\345\247\213.md" index d6f676a24..284bf0fb8 100644 --- "a/docs/zh_cn/1.1-\345\277\253\351\200\237\345\274\200\345\247\213.md" +++ "b/docs/zh_cn/1.1-\345\277\253\351\200\237\345\274\200\345\247\213.md" @@ -2,7 +2,7 @@ 1. 下载 MaaFramework 发行版 2. 准备资源文件 -3. 使用通用 GUI / 自行编写集成代码 +3. 使用通用 CLI / 自行编写集成代码 ## 下载 MaaFramework 发行版 @@ -22,10 +22,9 @@ my_resource │ ├── det.onnx │ ├── keys.txt │ └── rec.onnx -├── pipeline -│ ├── my_pipeline_1.json -│ └── my_pipeline_2.json -└── properties.json +└── pipeline + ├── my_pipeline_1.json + └── my_pipeline_2.json ``` 其中以 `my_` 开头的文件/文件夹均可自行修改名称,其他的则为固定文件名,不可修改,下面依次介绍: @@ -39,7 +38,7 @@ my_resource 小工具: - [JSON Schema](https://github.com/MaaAssistantArknights/MaaFramework/blob/main/tools/pipeline.schema.json) -- [图形化编辑器](https://github.com/MaaAssistantArknights/MaaJsonViewer) +- [VSCode 插件](https://marketplace.visualstudio.com/items?itemName=nekosu.maa-support) ### 图片文件 @@ -57,31 +56,20 @@ my_resource 若有需要也可以自行对 PaddleOCR 的官方预训练模型进行 fine-tuning (请自行参考 [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR) 官方文档),并转换成 ONNX 文件使用,转换命令可参考 [这里](https://github.com/MaaAssistantArknights/MaaCommonAssets/tree/main/OCR#command) -### 目录属性文件 +## 运行 -`properties.json` 可用于设置部分当前目录下的属性,典型 JSON 结构如下: +使用 MaaPiCli(通用 CLI)或者 自行编写集成代码 -_请注意,JSON 不支持注释,以下仅为演示,请勿直接复制使用。_ +### 使用 MaaPiCli -```jsonc -{ - "is_base": true, // 是否是基准资源 - // 若为 true,在读取本目录下的资源时,会清除之前读取的所有内容 -} -``` - -## 集成 - -使用 MaaY(通用 GUI)或者 自行编写集成代码 - -### 使用 MaaY +使用 Release 包 bin 文件夹中的 MaaPiCli,并编写 `interface.json` 置于同目录下,即可使用 -使用 [MaaY](https://github.com/MaaAssistantArknights/MaaY),参考 MaaY 中对应的文档整理 `.maay` 文件夹,并在 GUI 中直接导入使用。 +该 Cli 已完成基本功能开发,更多功能不断完善中!详细文档待进一步完善,当前可参考 sample 编写 实践: -- [M9A.maay](https://github.com/MaaAssistantArknights/M9A/tree/main/assets/.maay) -- [MAS.maay](https://github.com/MaaAssistantArknights/MaaAssistantSkland/tree/main/.maay) +- [Sample](https://github.com/MaaAssistantArknights/MaaFramework/blob/main/sample/interface.json) +- [M9A](https://github.com/MaaAssistantArknights/M9A/tree/main/assets/interface.json) ### 自行编写集成代码 diff --git a/sample/interface.json b/sample/interface.json index 93194ea99..a154b8446 100644 --- a/sample/interface.json +++ b/sample/interface.json @@ -1,50 +1,78 @@ { "resource": [ { - "name": "official", + "name": "Official", "path": [ "{PROJECT_DIR}/resource/base" ] }, { - "name": "bilibili", + "name": "Bilibili", "path": [ "{PROJECT_DIR}/resource/base", "{PROJECT_DIR}/resource/bilibili" ] } ], - "entry": [ + "task": [ { - "task": "StartUp" + "name": "启动游戏", + "entry": "StartUp" }, { - "task": "Awards" + "name": "收取荒原", + "entry": "Wilderness" }, { - "task": "Combat", + "name": "每日心相(意志解析)", + "entry": "Psychube" + }, + { + "name": "常规作战", + "entry": "Combat", "option": [ - "combat_stage", - "combat_times" + "作战关卡", + "复现次数", + "刷完全部体力", + "吃全部临期糖" ] - } - ], - "executor": [ + }, { - "type": "Recognizer", - "exec_name": "StageDropRecognition", - "exec_path": "Python", - "exec_param": [ - "{PROJECT_DIR}/exec_agent/StageDropRecognition/main.py" - ] + "name": "活动:绿湖噩梦 17 艰难(活动已结束)", + "entry": "ANightmareAtGreenLake", + "option": [ + "复现次数", + "刷完全部体力", + "吃全部临期糖" + ], + "param": { + "EnterTheShow": { + "next": "ANightmareAtGreenLake" + }, + "TargetStageName": { + "text": "17" + }, + "StageDifficulty": { + "next": "ActivityStageDifficulty" + } + } + }, + { + "name": "领取奖励", + "entry": "Awards" + }, + { + "name": "关闭游戏", + "entry": "Close1999" } ], - "option": [ - { - "name": "combat_stage", - "case": [ + "recognizer": {}, + "action": {}, + "option": { + "作战关卡": { + "cases": [ { - "name": "3-9", + "name": "3-9 厄险(百灵百验鸟)", "param": { "EnterTheShow": { "next": "MainChapter_3" @@ -58,7 +86,7 @@ } }, { - "name": "4-20", + "name": "4-20 厄险(双头形骨架)", "param": { "EnterTheShow": { "next": "MainChapter_4" @@ -73,10 +101,8 @@ } ] }, - { - "name": "combat_times", - "default": "x1", - "case": [ + "复现次数": { + "cases": [ { "name": "x1", "param": { @@ -110,6 +136,46 @@ } } ] + }, + "刷完全部体力": { + "cases": [ + { + "name": "Yes", + "param": { + "AllIn": { + "enabled": true + } + } + }, + { + "name": "No", + "param": { + "AllIn": { + "enabled": false + } + } + } + ] + }, + "吃全部临期糖": { + "cases": [ + { + "name": "Yes", + "param": { + "EatCandyWithin24H": { + "enabled": true + } + } + }, + { + "name": "No", + "param": { + "EatCandyWithin24H": { + "enabled": false + } + } + } + ] } - ] + } } \ No newline at end of file diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index c50fab202..e61ab53b4 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -14,9 +14,10 @@ if (WITH_DBG_CONTROLLER) endif() add_subdirectory(LibraryHolder) -add_subdirectory(ProjectInterface) add_subdirectory(MaaFramework) add_subdirectory(MaaToolkit) +add_subdirectory(ProjectInterface) +add_subdirectory(MaaProjectInterfaceCli) if(WITH_GRPC) add_subdirectory(MaaRpc) diff --git a/source/MaaProjectInterfaceCli/CMakeLists.txt b/source/MaaProjectInterfaceCli/CMakeLists.txt new file mode 100644 index 000000000..77f5dad3b --- /dev/null +++ b/source/MaaProjectInterfaceCli/CMakeLists.txt @@ -0,0 +1,18 @@ +file(GLOB_RECURSE maa_pi_cli_src *.h *.hpp *.cpp) +file(GLOB_RECURSE maa_pi_cli_header ../include/MaaPiCli/*) + +add_executable(MaaPiCli ${maa_pi_cli_src} ${maa_pi_cli_header}) + +target_include_directories(MaaPiCli + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../include ${CMAKE_CURRENT_SOURCE_DIR}/../../include) + +target_link_libraries(MaaPiCli ProjectInterface MaaUtils MaaFramework MaaToolkit HeaderOnlyLibraries Boost::system) + +add_dependencies(MaaPiCli ProjectInterface MaaUtils MaaFramework MaaToolkit) + +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${maa_pi_cli_src}) + +install(TARGETS MaaPiCli + RUNTIME DESTINATION bin + LIBRARY DESTINATION bin +) diff --git a/source/MaaProjectInterfaceCli/configurator.cpp b/source/MaaProjectInterfaceCli/configurator.cpp new file mode 100644 index 000000000..22ca16733 --- /dev/null +++ b/source/MaaProjectInterfaceCli/configurator.cpp @@ -0,0 +1,138 @@ +#include "configurator.h" + +#include + +#include "ProjectInterface/Parser.h" +#include "Utils/Logger.h" +#include "Utils/Platform.h" +#include "Utils/StringMisc.hpp" + +using namespace MAA_PROJECT_INTERFACE_NS; + +bool Configurator::load(const std::filesystem::path& project_dir) +{ + LogFunc << VAR(project_dir); + + auto data_opt = Parser::parse_interface(project_dir / kInterfaceFilename); + if (!data_opt) { + LogError << "Failed to parse interface.json"; + return false; + } + data_ = *std::move(data_opt); + if (data_.resource.empty()) { + LogError << "Resource is empty"; + return false; + } + + if (auto cfg_opt = Parser::parse_config(project_dir / kConfigFilename)) { + config_ = *std::move(cfg_opt); + first_time_use_ = false; + } + else { + first_time_use_ = true; + } + + project_dir_ = project_dir; + return true; +} + +bool Configurator::check_configuration() +{ + LogFunc; + + if (first_time_use_) { + return true; + } + + return Parser::check_configuration(data_, config_); +} + +void Configurator::save() +{ + std::ofstream ofs(project_dir_ / kConfigFilename); + ofs << config_.to_json(); +} + +std::optional Configurator::generate_runtime() const +{ + constexpr std::string_view kProjectDir = "{PROJECT_DIR}"; + + RuntimeParam runtime; + + auto resource_iter = + std::ranges::find_if(data_.resource, [&](const auto& resource) { return resource.name == config_.resource; }); + + if (resource_iter == data_.resource.end()) { + LogWarn << "Resource not found"; + return std::nullopt; + } + + for (const auto& path_string : resource_iter->path) { + auto dst = MaaNS::string_replace_all(path_string, kProjectDir, MaaNS::path_to_utf8_string(project_dir_)); + runtime.resource_path.emplace_back(dst); + } + + for (const auto& config_task : config_.task) { + auto task_opt = generate_runtime_task(config_task); + if (!task_opt) { + LogWarn << "failed to generate runtime, ignore" << VAR(config_task.name); + continue; + } + runtime.task.emplace_back(*std::move(task_opt)); + } + + runtime.recognizer = data_.recognizer; + runtime.action = data_.action; + for (auto& [name, executor] : runtime.recognizer) { + for (auto& param : executor.exec_param) { + MaaNS::string_replace_all_(param, kProjectDir, MaaNS::path_to_utf8_string(project_dir_)); + } + } + for (auto& [name, executor] : runtime.action) { + for (auto& param : executor.exec_param) { + MaaNS::string_replace_all_(param, kProjectDir, MaaNS::path_to_utf8_string(project_dir_)); + } + } + + runtime.adb_param = config_.adb_param; + runtime.adb_param.agent_path = MaaNS::path_to_utf8_string(project_dir_ / "MaaAgentBinary"); + + LogTrace << VAR(runtime); + return runtime; +} + +std::optional Configurator::generate_runtime_task(const Configuration::Task& config_task) const +{ + auto data_iter = + std::ranges::find_if(data_.task, [&](const auto& data_task) { return data_task.name == config_task.name; }); + if (data_iter == data_.task.end()) { + LogWarn << "task not found" << VAR(config_task.name); + return std::nullopt; + } + const auto& data_task = *data_iter; + + RuntimeParam::Task runtime_task { .name = data_task.name, .entry = data_task.entry, .param = data_task.param }; + + for (const auto& [config_option, config_option_value] : config_task.option) { + auto data_option_iter = std::ranges::find_if( + data_.option, [&](const auto& data_option_pair) { return data_option_pair.first == config_option; }); + if (data_option_iter == data_.option.end()) { + LogWarn << "option not found" << VAR(config_option); + continue; + } + const auto& data_option = data_option_iter->second; + + auto data_case_iter = std::ranges::find_if( + data_option.cases, [&](const auto& data_case) { return data_case.name == config_option_value; }); + if (data_case_iter == data_option.cases.end()) { + LogWarn << "case not found" << VAR(config_option_value); + continue; + } + const auto& data_case = *data_case_iter; + + // data_case first, duplicate keys will be overwritten by data_case.param + runtime_task.param = data_case.param | std::move(runtime_task.param); + } + + return runtime_task; +} diff --git a/source/MaaProjectInterfaceCli/configurator.h b/source/MaaProjectInterfaceCli/configurator.h new file mode 100644 index 000000000..b8e315227 --- /dev/null +++ b/source/MaaProjectInterfaceCli/configurator.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "ProjectInterface/Types.h" + +class Configurator +{ + static constexpr std::string_view kInterfaceFilename = "interface.json"; + static constexpr std::string_view kConfigFilename = "config.json"; + +public: + bool load(const std::filesystem::path& project_dir); + bool check_configuration(); + void save(); + + std::optional generate_runtime() const; + + const auto& interface_data() const { return data_; } + + const auto& configuration() const { return config_; } + auto& configuration() { return config_; } + + bool is_first_time_use() const { return first_time_use_; } + +private: + std::optional generate_runtime_task( + const MAA_PROJECT_INTERFACE_NS::Configuration::Task& config_task) const; + + std::filesystem::path project_dir_; + + MAA_PROJECT_INTERFACE_NS::InterfaceData data_; + bool first_time_use_ = false; + MAA_PROJECT_INTERFACE_NS::Configuration config_; +}; diff --git a/source/MaaProjectInterfaceCli/interactor.cpp b/source/MaaProjectInterfaceCli/interactor.cpp new file mode 100644 index 000000000..ba85bfe12 --- /dev/null +++ b/source/MaaProjectInterfaceCli/interactor.cpp @@ -0,0 +1,342 @@ +#include "interactor.h" + +#include + +#include "MaaToolkit/Device/MaaToolkitDevice.h" +#include "Utils/Logger.h" +#include "Utils/Platform.h" + +// return [1, size] +int input(size_t size, std::string_view prompt = "Please input") +{ + std::cout << std::format("{} [1-{}]: ", prompt, size); + size_t value = 0; + + while (true) { + std::cin >> value; + if (value > 0 && value <= size) { + break; + } + std::cout << std::format("Invalid value, {} [1-{}]: ", prompt, size); + } + + return static_cast(value); +} + +void clear_screen() +{ +#ifdef _WIN32 + system("cls"); +#else + system("clear"); +#endif +} + +void Interactor::interact_for_first_time_use() +{ + select_adb(); + select_resource(); + add_task(); +} + +bool Interactor::load(const std::filesystem::path& project_dir) +{ + LogFunc << VAR(project_dir); + + if (!config_.load(project_dir)) { + return false; + } + + if (!config_.check_configuration()) { + std::cout << "\n### The interface has changed and incompatible configurations have been deleted. ###\n\n"; + } + + return true; +} + +void Interactor::interact() +{ + if (config_.is_first_time_use()) { + interact_for_first_time_use(); + config_.save(); + } + + while (true) { + print_config(); + if (interact_once()) { + break; + } + config_.save(); + } + print_config(); +} + +std::optional Interactor::generate_runtime() const +{ + return config_.generate_runtime(); +} + +void Interactor::print_config() const +{ + clear_screen(); + std::cout << "\n### Current configuration ###\n\n"; + + std::cout << "Controller:\n\n"; + std::cout << "\t" + << MaaNS::utf8_to_crt(std::format("ADB\n\t\t{}\n\t\t{}", + MaaNS::path_to_utf8_string(config_.configuration().adb_param.adb_path), + config_.configuration().adb_param.address)) + << "\n\n"; + + std::cout << "Resource:\n\n"; + std::cout << "\t" << MaaNS::utf8_to_crt(config_.configuration().resource) << "\n\n"; + + std::cout << "Tasks:\n\n"; + print_config_tasks(false); +} + +bool Interactor::interact_once() +{ + std::cout << "\n\n### Select action ###\n\n"; + std::cout << "\t1. Add task\n"; + std::cout << "\t2. Delete task\n"; + std::cout << "\t3. Move task\n"; + std::cout << "\t4. Switch controller\n"; + std::cout << "\t5. Switch resource\n"; + std::cout << "\t6. Run tasks\n"; + std::cout << "\n"; + + int action = input(6); + + switch (action) { + case 1: + add_task(); + break; + case 2: + delete_task(); + break; + case 3: + move_task(); + break; + case 4: + // TODO: win32 + select_adb(); + break; + case 5: + select_resource(); + break; + case 6: + return true; + } + + return false; +} + +void Interactor::select_adb() +{ + std::cout << "\n\n\n"; + std::cout << "### Select ADB ###\n\n"; + + std::cout << "\t1. Auto detect\n"; + std::cout << "\t2. Manual input\n"; + std::cout << "\n"; + + int action = input(2); + + switch (action) { + case 1: + select_adb_auto_detect(); + break; + + case 2: + select_adb_manual_input(); + break; + } +} + +void Interactor::select_adb_auto_detect() +{ + std::cout << "\nFinding device...\n\n"; + + MaaToolkitFindDevice(); + auto size = MaaToolkitWaitForFindDeviceToComplete(); + if (size == 0) { + std::cout << "\nNo device found!\n"; + select_adb(); + return; + } + + std::cout << "## Select Device ##\n\n"; + + for (size_t i = 0; i < size; ++i) { + std::string name = MaaToolkitGetDeviceName(i); + std::string path = MaaToolkitGetDeviceAdbPath(i); + std::string address = MaaToolkitGetDeviceAdbSerial(i); + + std::cout << MaaNS::utf8_to_crt(std::format("\t{}. {}\n\t\t{}\n\t\t{}\n", i + 1, name, path, address)); + } + std::cout << "\n"; + + int index = input(size) - 1; + auto& adb_param = config_.configuration().adb_param; + + adb_param.adb_path = MaaToolkitGetDeviceAdbPath(index); + adb_param.address = MaaToolkitGetDeviceAdbSerial(index); + adb_param.type = MaaToolkitGetDeviceAdbControllerType(index); + adb_param.config = MaaToolkitGetDeviceAdbConfig(index); +} + +void Interactor::select_adb_manual_input() +{ + std::cout << "Please input ADB path: "; + std::string adb_path; + std::cin >> adb_path; + config_.configuration().adb_param.adb_path = adb_path; + + std::cout << "Please input ADB address: "; + std::string adb_address; + std::cin >> adb_address; + config_.configuration().adb_param.address = adb_address; +} + +void Interactor::select_resource() +{ + using namespace MAA_PROJECT_INTERFACE_NS; + + const auto& all_resources = config_.interface_data().resource; + if (all_resources.empty()) { + LogError << "Resource is empty"; + return; + } + + int index = 0; + if (all_resources.size() != 1) { + std::cout << "\n\n\n"; + std::cout << "### Select resource ###\n\n"; + for (size_t i = 0; i < all_resources.size(); ++i) { + std::cout << MaaNS::utf8_to_crt(std::format("\t{}. {}\n", i + 1, all_resources[i].name)); + } + std::cout << "\n"; + index = input(all_resources.size()) - 1; + } + else { + std::cout << "\n\n\n"; + std::cout << "### Only one resource, use it ###\n\n"; + index = 0; + } + const auto& resource = all_resources[index]; + + config_.configuration().resource = resource.name; +} + +void Interactor::add_task() +{ + using namespace MAA_PROJECT_INTERFACE_NS; + + const auto& all_data_tasks = config_.interface_data().task; + if (all_data_tasks.empty()) { + LogError << "Task is empty"; + return; + } + + std::cout << "\n\n\n"; + std::cout << "### Add task ###\n\n"; + for (size_t i = 0; i < all_data_tasks.size(); ++i) { + std::cout << MaaNS::utf8_to_crt(std::format("\t{}. {}\n", i + 1, all_data_tasks[i].name)); + } + std::cout << "\n"; + int input_index = input(all_data_tasks.size()) - 1; + const auto& data_task = all_data_tasks[input_index]; + + std::vector config_options; + for (const auto& option_name : data_task.option) { + if (!config_.interface_data().option.contains(option_name)) { + LogError << "Option not found" << VAR(option_name); + return; + } + + const auto& opt = config_.interface_data().option.at(option_name); + + if (!opt.default_case.empty()) { + config_options.emplace_back(Configuration::Option { option_name, opt.default_case }); + continue; + } + std::cout << MaaNS::utf8_to_crt(std::format("\n\n\n## Input option for \"{}\" ##\n\n", option_name)); + for (size_t i = 0; i < opt.cases.size(); ++i) { + std::cout << MaaNS::utf8_to_crt(std::format("\t{}. {}\n", i + 1, opt.cases[i].name)); + } + std::cout << "\n"; + + input_index = input(opt.cases.size()) - 1; + config_options.emplace_back(Configuration::Option { option_name, opt.cases[input_index].name }); + } + + config_.configuration().task.emplace_back( + Configuration::Task { .name = data_task.name, .option = std::move(config_options) }); +} + +void Interactor::edit_task() +{ + // TODO +} + +void Interactor::delete_task() +{ + using namespace MAA_PROJECT_INTERFACE_NS; + + auto& all_config_tasks = config_.configuration().task; + if (all_config_tasks.empty()) { + LogError << "Task is empty"; + return; + } + + std::cout << "\n\n\n### Delete task ###\n\n"; + + print_config_tasks(); + + int input_index = input(all_config_tasks.size()) - 1; + + all_config_tasks.erase(all_config_tasks.begin() + input_index); +} + +void Interactor::move_task() +{ + using namespace MAA_PROJECT_INTERFACE_NS; + + auto& all_config_tasks = config_.configuration().task; + if (all_config_tasks.empty()) { + LogError << "Task is empty"; + return; + } + + std::cout << "\n\n\n### Move task ###\n\n"; + + print_config_tasks(true); + + int from_index = input(all_config_tasks.size(), "From") - 1; + int to_index = input(all_config_tasks.size(), "To") - 1; + + auto task = std::move(all_config_tasks[from_index]); + all_config_tasks.erase(all_config_tasks.begin() + from_index); + all_config_tasks.insert(all_config_tasks.begin() + to_index, std::move(task)); +} + +void Interactor::print_config_tasks(bool with_index) const +{ + auto& all_config_tasks = config_.configuration().task; + + for (size_t i = 0; i < all_config_tasks.size(); ++i) { + const auto& task = all_config_tasks[i]; + if (with_index) { + std::cout << MaaNS::utf8_to_crt(std::format("\t{}. {}\n", i + 1, task.name)); + } + else { + std::cout << MaaNS::utf8_to_crt(std::format("\t- {}\n", task.name)); + } + + for (const auto& [key, value] : task.option) { + std::cout << "\t\t- " << MaaNS::utf8_to_crt(key) << ": " << MaaNS::utf8_to_crt(value) << "\n"; + } + } + std::cout << "\n"; +} diff --git a/source/MaaProjectInterfaceCli/interactor.h b/source/MaaProjectInterfaceCli/interactor.h new file mode 100644 index 000000000..42cdd54dc --- /dev/null +++ b/source/MaaProjectInterfaceCli/interactor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "configurator.h" + +class Interactor +{ +public: + bool load(const std::filesystem::path& project_dir); + void print_config() const; + void interact(); + std::optional generate_runtime() const; + +private: + void interact_for_first_time_use(); + + bool interact_once(); + void select_adb(); + void select_adb_auto_detect(); + void select_adb_manual_input(); + + void select_resource(); + void add_task(); + void edit_task(); + void delete_task(); + void move_task(); + + void print_config_tasks(bool with_index = true) const; + +private: + Configurator config_; +}; diff --git a/source/MaaProjectInterfaceCli/main.cpp b/source/MaaProjectInterfaceCli/main.cpp new file mode 100644 index 000000000..233db8253 --- /dev/null +++ b/source/MaaProjectInterfaceCli/main.cpp @@ -0,0 +1,47 @@ +#include + +#include "Utils/Platform.h" + +#include "interactor.h" +#include "runner.h" + +int main(int argc, char** argv) +{ + std::ignore = argc; + + auto app_path = MaaNS::path(argv[0]); + auto project_dir = app_path.parent_path(); + + Interactor interactor; + if (!interactor.load(project_dir)) { + return -1; + } + + bool direct = false; + for (int i = 1; i < argc; ++i) { + if (std::string_view(argv[i]) != "-d") { + continue; + } + direct = true; + break; + } + + if (direct) { + interactor.print_config(); + } + else { + interactor.interact(); + } + + auto runtime_opt = interactor.generate_runtime(); + if (!runtime_opt) { + return -1; + } + + bool ret = Runner::run(*runtime_opt); + if (!ret) { + return -1; + } + + return 0; +} diff --git a/source/MaaProjectInterfaceCli/runner.cpp b/source/MaaProjectInterfaceCli/runner.cpp new file mode 100644 index 000000000..cca5e3bc1 --- /dev/null +++ b/source/MaaProjectInterfaceCli/runner.cpp @@ -0,0 +1,80 @@ +#include "runner.h" + +#include +#include + +#include + +#include "MaaFramework/MaaAPI.h" +#include "MaaToolkit/MaaToolkitAPI.h" + +#include "Utils/ScopeLeave.hpp" + +bool Runner::run(const MAA_PROJECT_INTERFACE_NS::RuntimeParam& param) +{ + MaaToolkitInit(); + + auto maa_handle = MaaCreate(&Runner::on_maafw_notify, nullptr); + + auto controller_handle = MaaAdbControllerCreateV2(param.adb_param.adb_path.c_str(), param.adb_param.address.c_str(), + param.adb_param.type, param.adb_param.config.c_str(), + param.adb_param.agent_path.c_str(), nullptr, nullptr); + auto resource_handle = MaaResourceCreate(nullptr, nullptr); + + int64_t cid = MaaControllerPostConnection(controller_handle); + int64_t rid = 0; + for (const auto& path : param.resource_path) { + rid = MaaResourcePostPath(resource_handle, path.c_str()); + } + + MaaBindResource(maa_handle, resource_handle); + MaaBindController(maa_handle, controller_handle); + + if (MaaStatus_Failed == MaaControllerWait(controller_handle, cid)) { + std::cout << "Failed to connect to device:\n" + << param.adb_param.adb_path << "\n" + << param.adb_param.address << std::endl; + return false; + } + + if (MaaStatus_Failed == MaaResourceWait(resource_handle, rid)) { + std::cout << "Failed to load resource" << std::endl; + return false; + } + + OnScopeLeave([&]() { + MaaDestroy(maa_handle); + MaaResourceDestroy(resource_handle); + MaaControllerDestroy(controller_handle); + MaaToolkitUninit(); + }); + + for (const auto& [name, executor] : param.recognizer) { + std::string exec_param = json::array(executor.exec_param).to_string(); + MaaToolkitRegisterCustomRecognizerExecutor(maa_handle, name.c_str(), executor.exec_path.c_str(), + exec_param.c_str()); + } + for (const auto& [name, executor] : param.action) { + std::string exec_param = json::array(executor.exec_param).to_string(); + MaaToolkitRegisterCustomActionExecutor(maa_handle, name.c_str(), executor.exec_path.c_str(), + exec_param.c_str()); + } + + int64_t tid = 0; + for (const auto& task : param.task) { + std::string task_param = task.param.to_string(); + tid = MaaPostTask(maa_handle, task.entry.c_str(), task_param.c_str()); + } + + MaaWaitTask(maa_handle, tid); + + return true; +} + +void Runner::on_maafw_notify(MaaStringView msg, MaaStringView details_json, MaaTransparentArg callback_arg) +{ + std::ignore = callback_arg; + + std::string entry = json::parse(details_json).value_or(json::value())["entry"].as_string(); + std::cout << std::format("on_maafw_notify: {} {}", msg, entry) << std::endl; +} diff --git a/source/MaaProjectInterfaceCli/runner.h b/source/MaaProjectInterfaceCli/runner.h new file mode 100644 index 000000000..59b8643f2 --- /dev/null +++ b/source/MaaProjectInterfaceCli/runner.h @@ -0,0 +1,14 @@ +#pragma once + +#include "ProjectInterface/Types.h" + +#include "MaaFramework/MaaDef.h" + +class Runner +{ +public: + static bool run(const MAA_PROJECT_INTERFACE_NS::RuntimeParam& param); + +private: + static void on_maafw_notify(MaaStringView msg, MaaStringView details_json, MaaTransparentArg callback_arg); +}; diff --git a/source/ProjectInterface/Parser/Parser.cpp b/source/ProjectInterface/Parser/Parser.cpp index bda4b97e9..438305f77 100644 --- a/source/ProjectInterface/Parser/Parser.cpp +++ b/source/ProjectInterface/Parser/Parser.cpp @@ -4,24 +4,77 @@ MAA_PROJECT_INTERFACE_NS_BEGIN -std::optional Parser::parse(const std::filesystem::path& path) +std::optional Parser::parse_interface(const std::filesystem::path& path) { LogFunc << VAR(path); - auto root_opt = json::open(path); - if (!root_opt) { + auto json_opt = json::open(path); + if (!json_opt) { LogError << "failed to parse" << path; return std::nullopt; } - const json::value& root = *root_opt; + + const json::value& json = *json_opt; + return parse_interface(json); +} + +std::optional Parser::parse_interface(const json::value& json) +{ + std::string error_key; + if (!InterfaceData().check_json(json, error_key)) { + LogError << "json is not an InterfaceData" << VAR(error_key) << VAR(json); + return std::nullopt; + } + + auto data = json.as(); + + // check option for task + for (auto& task : data.task) { + for (auto& option : task.option) { + if (!data.option.contains(option)) { + LogError << "Option not found" << VAR(option); + return std::nullopt; + } + } + } + + return data; +} + +std::optional Parser::parse_config(const std::filesystem::path& path) +{ + LogFunc << VAR(path); + + auto json_opt = json::open(path); + if (!json_opt) { + LogWarn << "failed to parse" << path; + return std::nullopt; + } + + const json::value& json = *json_opt; + return parse_config(json); +} + +std::optional Parser::parse_config(const json::value& json) +{ + LogFunc << VAR(json); std::string error_key; - if (!InterfaceData().check_json(root, error_key)) { - LogError << "json is not an InterfaceData" << VAR(error_key) << VAR(root); + if (!Configuration().check_json(json, error_key)) { + LogError << "json is not a Configuration" << VAR(error_key) << VAR(json); return std::nullopt; } - return root.as(); + return json.as(); +} + +bool Parser::check_configuration(const InterfaceData& data, Configuration& config) +{ + // TODO + std::ignore = data; + std::ignore = config; + + return true; } MAA_PROJECT_INTERFACE_NS_END diff --git a/source/include/ProjectInterface/Parser.h b/source/include/ProjectInterface/Parser.h index 3cc71c7e5..361c83d5d 100644 --- a/source/include/ProjectInterface/Parser.h +++ b/source/include/ProjectInterface/Parser.h @@ -12,7 +12,11 @@ MAA_PROJECT_INTERFACE_NS_BEGIN class Parser { public: - static std::optional parse(const std::filesystem::path& path); + static std::optional parse_interface(const std::filesystem::path& path); + static std::optional parse_interface(const json::value& json); + static std::optional parse_config(const std::filesystem::path& path); + static std::optional parse_config(const json::value& json); + static bool check_configuration(const InterfaceData& data, Configuration& config); }; MAA_PROJECT_INTERFACE_NS_END diff --git a/source/include/ProjectInterface/Types.h b/source/include/ProjectInterface/Types.h index 86f5276d1..e5db030fc 100644 --- a/source/include/ProjectInterface/Types.h +++ b/source/include/ProjectInterface/Types.h @@ -1,73 +1,133 @@ #pragma once #include +#include #include -#include - #include "Conf/Conf.h" +#include "MaaFramework/MaaDef.h" +#include "Utils/JsonExt.hpp" MAA_PROJECT_INTERFACE_NS_BEGIN -struct Resource +struct Executor { - std::string name; - std::vector path; + std::string exec_path; + std::vector exec_param; - MEO_JSONIZATION(name, path); + MEO_JSONIZATION(exec_path, MEO_OPT exec_param); }; -struct Option +struct InterfaceData { - struct Case + struct Resource + { + std::string name; + std::vector path; + + MEO_JSONIZATION(name, path); + }; + + struct Task { std::string name; - json::value param; + std::string entry; + json::object param; + std::vector option; - MEO_JSONIZATION(name, param); + MEO_JSONIZATION(name, entry, MEO_OPT param, MEO_OPT option); }; - std::string name; - std::vector cases; - std::string default_case; // case.name + struct Option + { + struct Case + { + std::string name; + json::object param; + + MEO_JSONIZATION(name, param); + }; + + std::vector cases; + std::string default_case; // case.name + + MEO_JSONIZATION(cases, MEO_OPT default_case); + }; - MEO_JSONIZATION(name, cases, MEO_OPT default_case); + std::vector resource; + std::vector task; + std::unordered_map option; + std::unordered_map recognizer; + std::unordered_map action; + + MEO_JSONIZATION(resource, task, MEO_OPT option, MEO_OPT recognizer, MEO_OPT action); }; -struct Entry +struct AdbControllerParam { - std::string name; - std::string task; - json::value param; - std::vector