├── src ├── backend │ ├── hook │ │ ├── hook.h │ │ ├── logging.h │ │ ├── game_loop.h │ │ ├── logging.cc │ │ ├── mock_trainer.h │ │ ├── hook_point.h │ │ ├── memory_api.h │ │ ├── mock_trainer.cc │ │ ├── hook.cc │ │ ├── game_loop.cc │ │ ├── trainer.h │ │ └── memory_api.cc │ ├── record.h │ ├── config.h │ ├── record.cc │ ├── bin │ │ └── test_server.cc │ ├── lib │ │ └── dllmain.cc │ ├── config.cc │ └── tech.h ├── base │ ├── debug.h │ ├── windows_shit.h │ ├── thread.h │ ├── debug.cc │ ├── task_queue.h │ ├── task_queue.cc │ ├── macro.h │ ├── thread.cc │ └── logging.cc ├── frontend │ ├── web │ │ ├── vite.config.ts │ │ ├── package.json │ │ ├── index.html │ │ ├── protocol.js │ │ ├── styles.css │ │ ├── localization.js │ │ ├── client.js │ │ └── main.js │ └── desktop │ │ ├── glfw.h │ │ ├── context.h │ │ ├── timer.h │ │ ├── timer.cc │ │ ├── gui_context.h │ │ ├── gui.h │ │ ├── config.cc │ │ ├── config.h │ │ ├── bin │ │ └── main.cc │ │ ├── char_table.h │ │ ├── gui.cc │ │ └── gui_context.cc ├── wsock32 │ ├── export_table.def │ └── wsock32.h ├── formatter │ └── std.h └── protocol │ ├── server.h │ ├── client.h │ ├── test_json.cc │ ├── server.cc │ ├── client.cc │ └── model.h ├── .gitignore ├── scripts ├── ra2_trainer.toml ├── generate_web_main_page.py ├── generate_glyph_ranges.py ├── tech.txt └── ra2_trainer_backend.toml ├── README.md └── CMakeLists.txt /src/backend/hook/hook.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void InstallHooks(); 4 | -------------------------------------------------------------------------------- /src/base/debug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace yrtr { 5 | namespace debug { 6 | 7 | std::string DumpStackTrace(); 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | build/ 3 | deps/ 4 | !deps/dglad/ 5 | !deps/glad/ 6 | # Generated by npx vite build 7 | src/frontend/web/dist/ 8 | src/frontend/web/node_modules/ 9 | # Generated by scripts/generate_web_main_page.py 10 | src/protocol/main_page.h 11 | -------------------------------------------------------------------------------- /src/frontend/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import { viteSingleFile } from "vite-plugin-singlefile" 3 | 4 | export default defineConfig({ 5 | plugins: [viteSingleFile()], 6 | build: { 7 | minify: true 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/backend/hook/logging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace yrtr { 4 | namespace backend { 5 | namespace hook { 6 | 7 | // Output game builtin logging. 8 | void HookLogging(); 9 | 10 | } // namespace hook 11 | } // namespace backend 12 | } // namespace yrtr 13 | -------------------------------------------------------------------------------- /src/base/windows_shit.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #include 5 | #include 6 | 7 | #ifdef YRTR_DEBUG 8 | #include 9 | #include 10 | #include 11 | #endif 12 | 13 | #include 14 | #include 15 | -------------------------------------------------------------------------------- /src/frontend/desktop/glfw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "base/macro.h" 4 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 5 | #define GLFW_INCLUDE_GLU 6 | #include "GLFW/glfw3.h" 7 | #define GLFW_EXPOSE_NATIVE_WIN32 8 | #define GLFW_EXPOSE_NATIVE_WGL 9 | #include "GLFW/glfw3native.h" 10 | __YRTR_END_THIRD_PARTY_HEADERS 11 | -------------------------------------------------------------------------------- /src/frontend/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yrtr", 3 | "version": "1.0.0", 4 | "description": "Web client for RA2 Yuri's Revenge trainer.", 5 | "main": "main.js", 6 | "scripts": { 7 | "build": "vite build" 8 | }, 9 | "author": "adjwang", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "vite": "^6.4.1", 13 | "vite-plugin-singlefile": "^2.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/hook/game_loop.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "base/windows_shit.h" 3 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 4 | 5 | namespace yrtr { 6 | namespace backend { 7 | namespace hook { 8 | 9 | void ReclaimResourceOnce(); 10 | 11 | void HookUpdate(); 12 | void HookExitGame(); 13 | 14 | } // namespace hook 15 | } // namespace backend 16 | } // namespace yrtr 17 | -------------------------------------------------------------------------------- /src/wsock32/export_table.def: -------------------------------------------------------------------------------- 1 | LIBRARY wsock32 2 | EXPORTS 3 | _getsockopt @7 4 | _ntohl @14 5 | _htons @9 6 | _recvfrom @17 7 | _htonl @8 8 | _setsockopt @21 9 | _sendto @20 10 | _ntohs @15 11 | _gethostname @57 12 | _WSAStartup @115 13 | _EnumProtocolsA @1111 14 | _WSAAsyncSelect @101 15 | _WSACleanup @116 16 | _WSAGetLastError @111 17 | _inet_ntoa @11 18 | _socket @23 19 | _bind @2 20 | _closesocket @3 21 | _gethostbyname @52 22 | -------------------------------------------------------------------------------- /src/backend/record.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | namespace fs = std::filesystem; 4 | 5 | #include "protocol/model.h" 6 | 7 | namespace yrtr { 8 | namespace backend { 9 | 10 | bool WriteCheckboxStateToToml(const CheckboxStateMap& state_map, 11 | const fs::path& filepath); 12 | bool ReadCheckboxStateFromToml(const fs::path& filepath, 13 | CheckboxStateMap& state_map); 14 | 15 | } // namespace backend 16 | } // namespace yrtr -------------------------------------------------------------------------------- /src/frontend/desktop/context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "base/macro.h" 3 | 4 | namespace yrtr { 5 | namespace frontend { 6 | 7 | class GuiContext { 8 | public: 9 | GuiContext() {} 10 | virtual ~GuiContext() {} 11 | 12 | virtual void UpdateViewport(int width, int height) = 0; 13 | virtual void BeginFrame() = 0; 14 | virtual void EndFrame() = 0; 15 | virtual void Render() = 0; 16 | 17 | private: 18 | DISALLOW_COPY_AND_ASSIGN(GuiContext); 19 | }; 20 | 21 | } // namespace frontend 22 | } // namespace yrtr 23 | -------------------------------------------------------------------------------- /src/frontend/desktop/timer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace yrtr { 6 | namespace frontend { 7 | 8 | class Timer { 9 | public: 10 | using Handler = std::function; 11 | static void SetTimer(int id, double interval, Handler handler); 12 | static void Update(); 13 | 14 | private: 15 | struct TimerData { 16 | double interval; 17 | double time_stamp; 18 | Handler handler; 19 | }; 20 | static std::unordered_map timer_data_; 21 | }; 22 | 23 | } // namespace frontend 24 | } // namespace yrtr 25 | -------------------------------------------------------------------------------- /scripts/ra2_trainer.toml: -------------------------------------------------------------------------------- 1 | [ra2_trainer] 2 | # Available: zh, en 3 | language = "zh" 4 | enable_dpi_awareness = false 5 | port = 35271 6 | 7 | # font_path is optional, fontex_path is required. 8 | # If using fonts that separate characters and emojis, set both font_path and fontex_path. 9 | # If using fonts both supplies characters and emojis, set only fontex_path. 10 | # font_path = "C:\\Windows\\Fonts\\arial.ttf" 11 | # fontex_path = "C:\\Windows\\Fonts\\NotoColorEmoji-Regular.ttf" 12 | # Segoe UI Emoji font family is available on win10 and win11. 13 | # https://learn.microsoft.com/en-us/typography/font-list/segoe-ui-emoji 14 | font_path = "C:\\Windows\\Fonts\\simhei.ttf" 15 | fontex_path = "C:\\Windows\\Fonts\\seguiemj.ttf" 16 | -------------------------------------------------------------------------------- /src/base/thread.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace yrtr { 5 | using ThreadId = uint32_t; 6 | // Use 0 as invalid thread id. 7 | // https://learn.microsoft.com/en-us/windows/win32/procthread/thread-handles-and-identifiers 8 | 9 | ThreadId GetGameLoopThreadId(); 10 | ThreadId GetRendererThreadId(); 11 | ThreadId GetNetThreadId(); 12 | void SetupGameLoopThreadOnce(ThreadId tid = 0); 13 | void SetupRendererThreadOnce(ThreadId tid = 0); 14 | void SetupNetThreadOnce(ThreadId tid = 0); 15 | bool IsWithinThread(ThreadId tid); 16 | bool IsWithinGameLoopThread(); 17 | bool IsWithinRendererThread(); 18 | bool IsWithinNetThread(); 19 | // For debugging. 20 | std::string InspectThreads(); 21 | 22 | } // namespace yrtr 23 | -------------------------------------------------------------------------------- /src/base/debug.cc: -------------------------------------------------------------------------------- 1 | #include "base/debug.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace yrtr { 8 | namespace debug { 9 | 10 | std::string DumpStackTrace() { 11 | auto trace = std::stacktrace::current(); 12 | std::stringstream output; 13 | output << "Stack trace:\n"; 14 | size_t frame_num = 0; 15 | for (const auto& entry : trace) { 16 | output << " #" << frame_num++ << " "; 17 | if (entry) { 18 | output << entry.description() << " in " << entry.source_file() << ":" 19 | << entry.source_line(); 20 | } else { 21 | output << "[unknown]"; 22 | } 23 | output << "\n"; 24 | } 25 | return output.str(); 26 | } 27 | 28 | } // namespace debug 29 | } // namespace yrtr 30 | -------------------------------------------------------------------------------- /src/frontend/desktop/timer.cc: -------------------------------------------------------------------------------- 1 | #include "frontend/desktop/timer.h" 2 | 3 | #include "frontend/desktop/glfw.h" 4 | 5 | namespace yrtr { 6 | namespace frontend { 7 | 8 | std::unordered_map Timer::timer_data_; 9 | 10 | void Timer::SetTimer(int id, double interval, Handler handler) { 11 | timer_data_.erase(id); 12 | timer_data_.emplace(id, TimerData{ 13 | .interval = interval, 14 | .time_stamp = glfwGetTime(), 15 | .handler = std::move(handler), 16 | }); 17 | } 18 | 19 | void Timer::Update() { 20 | double ts = glfwGetTime(); 21 | for (auto& [id, data] : timer_data_) { 22 | if (ts - data.time_stamp > data.interval) { 23 | data.handler(); 24 | data.time_stamp = ts; 25 | } 26 | } 27 | } 28 | 29 | } // namespace frontend 30 | } // namespace yrtr 31 | -------------------------------------------------------------------------------- /scripts/generate_web_main_page.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | SCRIPT_DIR = Path(sys.argv[0]).parent 4 | 5 | CPP_TEMPLATE = """ 6 | // Auto generated file, do not modify. 7 | #pragma once 8 | #include 9 | namespace yrtr {{ 10 | inline static constexpr std::string_view kMainPageHtml = R"({})"; 11 | }} 12 | """ 13 | 14 | if __name__ == '__main__': 15 | WEB_DIST_PATH = SCRIPT_DIR / '../src/frontend/web/dist/index.html' 16 | OUTPUT_PATH = SCRIPT_DIR / '../src/protocol/main_page.h' 17 | 18 | if not Path.exists(WEB_DIST_PATH): 19 | print(f'Failed to find dist dir={WEB_DIST_PATH}, build web first') 20 | sys.exit(1) 21 | with open(WEB_DIST_PATH, 'r', encoding='utf-8') as f: 22 | html_content = f.read() 23 | cpp_content = CPP_TEMPLATE.format(html_content) 24 | with open(OUTPUT_PATH, 'w', encoding='utf-8') as f: 25 | f.write(cpp_content) 26 | 27 | -------------------------------------------------------------------------------- /src/frontend/desktop/gui_context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "base/windows_shit.h" 6 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 7 | #include "frontend/desktop/glfw.h" 8 | #include "base/logging.h" 9 | #include "base/macro.h" 10 | #include "frontend/desktop/context.h" 11 | 12 | namespace yrtr { 13 | namespace frontend { 14 | 15 | #define __YRTR_WINDOW_TYPE GLFWwindow* 16 | 17 | class ImGuiWindow : public GuiContext { 18 | public: 19 | ImGuiWindow(__YRTR_WINDOW_TYPE window); 20 | virtual ~ImGuiWindow(); 21 | 22 | void UpdateViewport(int width, int height) override; 23 | void BeginFrame() override; 24 | void EndFrame() override; 25 | void Render() override; 26 | 27 | private: 28 | __YRTR_WINDOW_TYPE window_; 29 | float hdpi_scale_factor_; 30 | 31 | DISALLOW_COPY_AND_ASSIGN(ImGuiWindow); 32 | }; 33 | 34 | } // namespace frontend 35 | } // namespace yrtr 36 | -------------------------------------------------------------------------------- /src/backend/hook/logging.cc: -------------------------------------------------------------------------------- 1 | #include "backend/hook/logging.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "Unsorted.h" 7 | #include "backend/hook/hook_point.h" 8 | #include "backend/hook/memory_api.h" 9 | #include "base/logging.h" 10 | 11 | namespace yrtr { 12 | namespace backend { 13 | namespace hook { 14 | 15 | namespace { 16 | static char log_buf[0x200]; 17 | 18 | static void __cdecl BuiltinLogging(const char* fmt, ...) { 19 | // caller ptr 20 | uint32_t* p_caller = 21 | reinterpret_cast(reinterpret_cast(&fmt) - 4); 22 | va_list arg = (va_list)(reinterpret_cast(&fmt) + 4); 23 | int n = vsprintf_s(log_buf, 0x200, fmt, arg); 24 | DCHECK_LE(n, 0x200); 25 | if (n > 0 && log_buf[n - 1] == '\n') { 26 | log_buf[n - 1] = '\0'; 27 | } 28 | int frame = yrpp::Unsorted::CurrentFrame; 29 | LOG_F(INFO, "F={} P={:08X} {}", frame, *p_caller, log_buf); 30 | } 31 | } // namespace 32 | 33 | void HookLogging() { 34 | DLOG_F(INFO, "[YRTR-HOOK] {}", __func__); 35 | MemoryAPI::instance()->HookJump(kHpBuiltinLogging, BuiltinLogging); 36 | } 37 | 38 | } // namespace hook 39 | } // namespace backend 40 | } // namespace yrtr 41 | -------------------------------------------------------------------------------- /src/base/task_queue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "base/macro.h" 6 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 7 | #include "absl/container/inlined_vector.h" 8 | #include "absl/synchronization/mutex.h" 9 | __YRTR_END_THIRD_PARTY_HEADERS 10 | #include "base/thread.h" 11 | 12 | namespace yrtr { 13 | 14 | using Task = std::function; 15 | 16 | // Perform cross thread event passing. 17 | class TaskQueue { 18 | public: 19 | TaskQueue(); 20 | // Tasks are not invoked until the thread id is set. There has the potential 21 | // to add tasks before setup thread id, therefore this is not a constructor. 22 | void SetThreadId(ThreadId tid); 23 | // Execute task if this function is called inside the setting thread, 24 | // otherwise pending until a call to ExecuteTasks(). This function can be 25 | // called from any thread. 26 | void ExecuteOrScheduleTask(Task&& task); 27 | // Execute pending tasks pushed from other threads. This function has to be 28 | // called inside the setting thread. 29 | void ExecuteTasks(); 30 | 31 | private: 32 | using TaskBuffer = absl::InlinedVector; 33 | std::atomic tid_; 34 | absl::Mutex pending_tasks_mu_; 35 | TaskBuffer pending_tasks_ ABSL_GUARDED_BY(pending_tasks_mu_); 36 | 37 | DISALLOW_COPY_AND_ASSIGN(TaskQueue); 38 | }; 39 | 40 | } // namespace yrtr 41 | -------------------------------------------------------------------------------- /src/base/task_queue.cc: -------------------------------------------------------------------------------- 1 | #include "base/task_queue.h" 2 | 3 | #include "base/logging.h" 4 | #include "base/debug.h" 5 | 6 | namespace yrtr { 7 | 8 | TaskQueue::TaskQueue() {} 9 | 10 | void TaskQueue::SetThreadId(ThreadId tid) { 11 | // Only allowed to set once. 12 | ThreadId old_tid = 0; 13 | tid_.compare_exchange_strong(old_tid, tid); 14 | } 15 | 16 | void TaskQueue::ExecuteOrScheduleTask(Task&& task) { 17 | // Not pending task if the loop is not start yet. Or remove this clause to 18 | // pending task before target thread starting. 19 | ThreadId tid = tid_.load(); 20 | if (tid == 0) [[unlikely]] { 21 | LOG_F(WARNING, "Trying to add task before setting loop thread"); 22 | return; 23 | } 24 | if (IsWithinThread(tid)) { 25 | task(); 26 | } else { 27 | absl::MutexLock lk(&pending_tasks_mu_); 28 | pending_tasks_.emplace_back(std::move(task)); 29 | } 30 | } 31 | 32 | void TaskQueue::ExecuteTasks() { 33 | ThreadId tid = tid_.load(); 34 | if (tid == 0) [[unlikely]] { 35 | LOG_F(ERROR, "Trying to execute task before setting loop thread"); 36 | return; 37 | } 38 | DCHECK(IsWithinThread(tid)); 39 | TaskBuffer pending_tasks; 40 | { 41 | absl::MutexLock lk(&pending_tasks_mu_); 42 | pending_tasks_.swap(pending_tasks); 43 | } 44 | for (Task& task : pending_tasks) { 45 | task(); 46 | } 47 | } 48 | 49 | } // namespace yrtr 50 | -------------------------------------------------------------------------------- /src/backend/hook/mock_trainer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include "backend/hook/trainer.h" 5 | #include "base/macro.h" 6 | #include "protocol/model.h" 7 | 8 | namespace yrtr { 9 | namespace backend { 10 | namespace hook { 11 | 12 | class MockTrainer : public ITrainer { 13 | public: 14 | MockTrainer(Config* cfg); 15 | MockTrainer(MockTrainer&&) = delete; 16 | MockTrainer& operator=(MockTrainer&&) = delete; 17 | ~MockTrainer(); 18 | 19 | State state() const final { 20 | DCHECK(IsWithinGameLoopThread()); 21 | return state_; 22 | } 23 | 24 | void set_on_state_updated(std::function cb) final { 25 | on_state_updated_ = std::move(cb); 26 | } 27 | 28 | void Update(double delta) final; 29 | void OnInputEvent(FnLabel label, uint32_t val) final; 30 | void OnButtonEvent(FnLabel label) final; 31 | void OnCheckboxEvent(FnLabel label, bool activate) final; 32 | void OnProtectedListEvent(SideMap&& side_map) final; 33 | 34 | private: 35 | // Update from state before use. 36 | static SideMap protected_houses_; 37 | 38 | Config* cfg_; 39 | State state_; 40 | std::function on_state_updated_; 41 | bool state_dirty_; 42 | 43 | void PropagateStateIfDirty(); 44 | void UpdateCheckboxState(FnLabel label, bool activate); 45 | 46 | DISALLOW_COPY_AND_ASSIGN(MockTrainer); 47 | }; 48 | 49 | } // namespace hook 50 | } // namespace backend 51 | } // namespace yrtr 52 | -------------------------------------------------------------------------------- /src/formatter/std.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | namespace fs = std::filesystem; 5 | #include 6 | #include 7 | #include 8 | 9 | template <> 10 | struct std::formatter { 11 | constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } 12 | auto format(const fs::path& path, std::format_context& ctx) const { 13 | return std::format_to(ctx.out(), "{}", path.string()); 14 | } 15 | }; 16 | 17 | template 18 | struct std::formatter> { 19 | constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } 20 | auto format(const std::vector& vec, std::format_context& ctx) const { 21 | if (vec.empty()) { 22 | return std::format_to(ctx.out(), "[]"); 23 | } 24 | std::stringstream ss; 25 | ss << "["; 26 | for (auto& val : vec | std::views::take(vec.size() - 1)) { 27 | ss << val << ", "; 28 | } 29 | ss << vec[vec.size() - 1] << "]"; 30 | return std::format_to(ctx.out(), "{}", ss.str()); 31 | } 32 | }; 33 | 34 | template 35 | struct std::formatter> { 36 | constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } 37 | auto format(const std::array& vec, std::format_context& ctx) const { 38 | if (vec.empty()) { 39 | return std::format_to(ctx.out(), "[]"); 40 | } 41 | std::stringstream ss; 42 | ss << "["; 43 | for (auto& val : vec | std::views::take(vec.size() - 1)) { 44 | ss << val << ", "; 45 | } 46 | ss << vec[vec.size() - 1] << "]"; 47 | return std::format_to(ctx.out(), "{}", ss.str()); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/frontend/desktop/gui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "base/macro.h" 6 | #include "frontend/desktop/char_table.h" 7 | #include "protocol/model.h" 8 | 9 | namespace yrtr { 10 | namespace frontend { 11 | // MVC -- view. 12 | 13 | class Gui { 14 | public: 15 | using InputHandler = std::function; 16 | using ButtonHandler = std::function; 17 | using CheckboxHandler = std::function; 18 | using HouseListHandler = std::function; 19 | 20 | Gui(Lang lang); 21 | ~Gui(); 22 | 23 | void UpdateState(const State& state); 24 | void Render(); 25 | // Provides a way to trigger from outside event, e.g., hotkeys. Triggered 26 | // widgets are represented on gui and would change states, just like which 27 | // triggered through gui. 28 | void Trigger(FnLabel label) const; 29 | 30 | void AddButtonListener(FnLabel label, ButtonHandler cb); 31 | void AddInputListener(FnLabel label, InputHandler cb); 32 | void AddCheckboxListener(FnLabel label, CheckboxHandler cb); 33 | void AddHouseListListener(HouseListHandler cb); 34 | 35 | private: 36 | State state_; 37 | const Lang lang_; 38 | 39 | std::map input_cbs_; 40 | std::map btn_cbs_; 41 | std::map ckbox_cbs_; 42 | HouseListHandler house_list_cb_; 43 | 44 | void RenderClientArea(); 45 | void RenderTabAssists(); 46 | void RenderTabFilters(); 47 | 48 | std::string GetGuiStr(GuiLabel label); 49 | std::string GetFnStr(FnLabel label); 50 | 51 | DISALLOW_COPY_AND_ASSIGN(Gui); 52 | }; 53 | 54 | } // namespace frontend 55 | } // namespace yrtr 56 | -------------------------------------------------------------------------------- /scripts/generate_glyph_ranges.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | SCRIPT_DIR = Path(sys.argv[0]).parent 4 | import re 5 | 6 | def cut_ranges(data): 7 | range_collection = [] 8 | sub_range = [] 9 | for a, b in zip(data[:-1], data[1:]): 10 | sub_range.append(a) 11 | if a + 1 != b: 12 | range_collection.append(sub_range) 13 | sub_range = [] 14 | if len(sub_range) > 0: 15 | range_collection.append(sub_range + [b]) 16 | else: 17 | range_collection.append([b]) 18 | 19 | return range_collection 20 | 21 | 22 | def compress_ranges(range_collection): 23 | return [(r[0], r[-1]) for r in range_collection] 24 | 25 | def print_cpp_range_array(range_collection): 26 | print('{') 27 | for a, b in range_collection: 28 | print(f" 0x{a:04X}, 0x{b:04X}, ") 29 | print(' 0,') 30 | print('};') 31 | 32 | 33 | if __name__ == '__main__': 34 | with open(SCRIPT_DIR / '../src/char_table.h', 'r') as f: 35 | char_tables = f.read() 36 | with open(SCRIPT_DIR / '../src/config.h', 'r') as f: 37 | hot_keys = f.read() 38 | res = re.findall(r'u8"(.*)"', char_tables + hot_keys) 39 | # print(res, len(res)) 40 | zh_chs = list(set(''.join(res))) 41 | num_chs = [f"{i}" for i in range(10)] 42 | custom_chs = list(": ()") 43 | for c in (zh_chs + num_chs + custom_chs): 44 | print(c, f"0x{ord(c):04X}") 45 | ords = sorted([ord(c) for c in (zh_chs + num_chs + custom_chs)]) 46 | # for i in ords: 47 | # print(f"0x{i:04X}") 48 | range_collection = cut_ranges(ords) 49 | # for r in range_collection: 50 | # print([f"0x{i:04X}" for i in r]) 51 | range_collection = compress_ranges(range_collection) 52 | # for r in range_collection: 53 | # print([f"0x{i:04X}" for i in r]) 54 | print_cpp_range_array(range_collection) 55 | -------------------------------------------------------------------------------- /src/protocol/server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "backend/hook/trainer.h" 6 | #include "base/macro.h" 7 | #include "base/task_queue.h" 8 | #include "protocol/model.h" 9 | #include "websocketpp/config/asio_no_tls.hpp" 10 | #include "websocketpp/server.hpp" 11 | 12 | namespace yrtr { 13 | 14 | class Server { 15 | public: 16 | Server(backend::hook::ITrainer* trainer, uint16_t port); 17 | void Stop(); 18 | void Update(); 19 | 20 | private: 21 | using WebsocketServer = websocketpp::server; 22 | // This tool is used in LAN, 50ms should be enough. Large timeout queues too 23 | // many jobs in sending queue when the backend is not running, making the gui 24 | // actions, like close window, acts lagging. 25 | static constexpr int kConnTimeoutMilliseconds = 50; 26 | backend::hook::ITrainer* trainer_; 27 | std::thread evloop_; 28 | WebsocketServer svr_; 29 | // Record connection to propagate state. 30 | std::set> 32 | conns_; 33 | TaskQueue game_loop_ch_; 34 | 35 | void OnOpenConn(websocketpp::connection_hdl hdl); 36 | void OnCloseConn(websocketpp::connection_hdl hdl); 37 | void OnMessage(WebsocketServer& svr, websocketpp::connection_hdl hdl, 38 | WebsocketServer::message_ptr msg); 39 | void OnHttpRequest(WebsocketServer& svr, websocketpp::connection_hdl hdl); 40 | void OnStateUpdated(State state); 41 | void OnGetStateEvent(websocketpp::connection_hdl hdl); 42 | void SendState(State&& state, websocketpp::connection_hdl hdl); 43 | void OnPostInputEvent(Event&& event); 44 | void OnPostButtonEvent(Event&& event); 45 | void OnPostCheckboxEvent(Event&& event); 46 | void OnPostProtectedListEvent(Event&& event); 47 | 48 | DISALLOW_COPY_AND_ASSIGN(Server); 49 | }; 50 | 51 | } // namespace yrtr 52 | -------------------------------------------------------------------------------- /src/backend/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | namespace fs = std::filesystem; 5 | #include 6 | #include 7 | #include 8 | 9 | #include "backend/tech.h" 10 | #include "base/macro.h" 11 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 12 | #include "absl/container/flat_hash_set.h" 13 | __YRTR_END_THIRD_PARTY_HEADERS 14 | #include "toml++/toml.hpp" 15 | 16 | namespace yrtr { 17 | namespace backend { 18 | 19 | class Config { 20 | public: 21 | static constexpr std::string_view kCfgFileName = "ra2_trainer_backend.toml"; 22 | static constexpr std::string_view kLogFileName = "ra2_trainer_backend.log"; 23 | 24 | static Config* instance() { return inst_.get(); } 25 | static bool Load(const fs::path& cfg_dir); 26 | 27 | Config(const fs::path& cfg_path); 28 | 29 | uint16_t port() const { return port_; } 30 | const fs::path& hotreload_dir() const { return hotreload_dir_; } 31 | bool auto_record() const { return auto_record_; } 32 | fs::path record_path() const { return cfg_dir_ / kDefaultRecordFilename; } 33 | const TechList& tech_list() const { return tech_list_; } 34 | 35 | // Inputs a relative path, return absolute path relative to configuration file 36 | // directory. 37 | fs::path GetAbsolutePath(const fs::path& relpath) const; 38 | 39 | private: 40 | static std::unique_ptr inst_; 41 | static constexpr std::string_view kDefaultHotreloadDir = 42 | "ra2_trainer_hotreload"; 43 | static constexpr std::string_view kDefaultRecordFilename = 44 | "ra2_trainer_record.toml"; 45 | 46 | const fs::path cfg_dir_; 47 | const fs::path cfg_path_; 48 | 49 | uint16_t port_; 50 | fs::path hotreload_dir_; 51 | bool auto_record_; 52 | TechList tech_list_; 53 | 54 | void LoadGlobal(const toml::table& global); 55 | void LoadTechList(const toml::table& tech_tb); 56 | 57 | DISALLOW_COPY_AND_ASSIGN(Config); 58 | }; 59 | 60 | } // namespace backend 61 | } // namespace yrtr 62 | -------------------------------------------------------------------------------- /src/backend/record.cc: -------------------------------------------------------------------------------- 1 | #include "backend/record.h" 2 | 3 | #include 4 | 5 | #include "base/logging.h" 6 | #include "formatter/std.h" 7 | #include "gsl/util" 8 | #include "toml++/toml.hpp" 9 | 10 | namespace yrtr { 11 | namespace backend { 12 | 13 | bool WriteCheckboxStateToToml(const CheckboxStateMap& state_map, 14 | const fs::path& filepath) { 15 | toml::table root_tbl; 16 | toml::table checkbox_tbl; 17 | for (const auto& [label, state] : state_map) { 18 | checkbox_tbl.insert(StrFnLabel(label), state.activate); 19 | } 20 | root_tbl.insert("checkbox", std::move(checkbox_tbl)); 21 | std::ofstream file(filepath); 22 | if (!file.is_open()) { 23 | return false; 24 | } 25 | auto _ = gsl::finally([&]() { file.close(); }); 26 | DLOG_F(INFO, "Write record to file={}", filepath); 27 | file << root_tbl; 28 | return file.good(); 29 | } 30 | 31 | bool ReadCheckboxStateFromToml(const fs::path& filepath, 32 | CheckboxStateMap& state_map) { 33 | try { 34 | auto root_tbl = toml::parse_file(filepath.string()); 35 | DLOG_F(INFO, "Load record from file={}", filepath); 36 | auto* checkbox_tbl = root_tbl["checkbox"].as_table(); 37 | if (!checkbox_tbl) { 38 | LOG_F(WARNING, "Missing [checkbox] section in file={}", filepath); 39 | return false; 40 | } 41 | for (const auto& [key, value] : *checkbox_tbl) { 42 | FnLabel label = StrToFnLabel(key.str()); 43 | if (label == FnLabel::kInvalid) { 44 | // Skip unknown keys 45 | continue; 46 | } 47 | if (auto activate = value.value()) { 48 | state_map[label].activate = *activate; 49 | } 50 | } 51 | return true; 52 | } catch (const toml::parse_error& err) { 53 | LOG_F(WARNING, "Failed to parse record file={} err={}", filepath, 54 | err.what()); 55 | return false; 56 | } 57 | } 58 | 59 | } // namespace backend 60 | } // namespace yrtr 61 | -------------------------------------------------------------------------------- /src/frontend/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | YRTR Assist Tool 8 | 9 | 10 | 11 | 12 |
13 |
14 |
Filter
15 |
Assist
16 |
17 | 18 |
19 |
20 |
21 |
22 |

Selecting House List

23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |

Protected House List

33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 |
57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/protocol/client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "base/macro.h" 6 | #include "base/task_queue.h" 7 | #include "frontend/desktop/gui.h" 8 | #include "protocol/model.h" 9 | #include "websocketpp/client.hpp" 10 | #include "websocketpp/config/asio_no_tls_client.hpp" 11 | 12 | namespace yrtr { 13 | 14 | class Client { 15 | public: 16 | static constexpr int kTimerIdUpdateState = 1; 17 | 18 | Client(frontend::Gui& gui, uint16_t port); 19 | ~Client(); 20 | // Despite invoked by render loop, I'm still gonna name it "Update". 21 | void Update(); 22 | // Use an explicit stop to put it before the window destroied. Otherwise if 23 | // someday a background buggy thread blocks after the main window closed, 24 | // it leaves a zombie background process that really hard to be noticed. 25 | void Stop(); 26 | void GetState(); 27 | 28 | private: 29 | using WebsocketClient = websocketpp::client; 30 | // Limit maximum pending state getting requests. 31 | static constexpr int kMaxGetState = 1; 32 | // This tool is used in LAN, 50ms should be enough. Large timeout queues too 33 | // many jobs in sending queue when the backend is not running, making the gui 34 | // actions, like close window, acts lagging. 35 | static constexpr int kConnTimeoutMilliseconds = 50; 36 | const std::string uri_; 37 | frontend::Gui& gui_; 38 | std::thread evloop_; 39 | WebsocketClient cli_; 40 | WebsocketClient::connection_ptr conn_; 41 | TaskQueue render_loop_ch_; 42 | std::atomic get_state_count_; 43 | std::atomic stop_; 44 | 45 | WebsocketClient::connection_ptr GetOrCreateConn(); 46 | void OnMessage(WebsocketClient& cli, websocketpp::connection_hdl hdl, 47 | WebsocketClient::message_ptr msg); 48 | void SendGetState(); 49 | void OnGetStateEvent(Event&& event); 50 | void SendPostInput(FnLabel label, uint32_t val); 51 | void SendPostButton(FnLabel label); 52 | void SendPostCheckbox(FnLabel label, bool activate); 53 | void SendPostProtectedList(SideMap&& side_map); 54 | void SendData(std::string_view path, std::string&& data); 55 | 56 | DISALLOW_COPY_AND_ASSIGN(Client); 57 | }; 58 | 59 | } // namespace yrtr 60 | -------------------------------------------------------------------------------- /src/wsock32/wsock32.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define WIN32_LEAN_AND_MEAN 3 | #include 4 | #include 5 | #include 6 | 7 | extern "C" { 8 | __declspec(dllexport) int WSAAPI _getsockopt(int sockfd, int level, int optname, 9 | char* optval, socklen_t* optlen); 10 | __declspec(dllexport) u_long WSAAPI _ntohl(u_long netlong); 11 | __declspec(dllexport) u_short WSAAPI _htons(u_short hostshort); 12 | __declspec(dllexport) int WSAAPI _recvfrom(SOCKET s, char* buf, int len, 13 | int flags, sockaddr* from, 14 | int* fromlen); 15 | __declspec(dllexport) u_long WSAAPI _htonl(u_long hostlong); 16 | __declspec(dllexport) int WSAAPI _setsockopt(SOCKET s, int level, int optname, 17 | const char* optval, int optlen); 18 | __declspec(dllexport) int WSAAPI _sendto(SOCKET s, const char* buf, int len, 19 | int flags, const sockaddr* to, 20 | int tolen); 21 | __declspec(dllexport) u_short WSAAPI _ntohs(u_short netshort); 22 | __declspec(dllexport) int WSAAPI _gethostname(char* name, int namelen); 23 | __declspec(dllexport) int WSAAPI _WSAStartup(WORD wVersionRequested, 24 | LPWSADATA lpWSAData); 25 | __declspec(dllexport) int WSAAPI _EnumProtocolsA(LPINT lpiProtocols, 26 | LPVOID lpProtocolBuffer, 27 | LPDWORD lpdwBufferLength); 28 | __declspec(dllexport) int WSAAPI _WSAAsyncSelect(SOCKET s, HWND hWnd, 29 | u_int wMsg, long lEvent); 30 | __declspec(dllexport) int WSAAPI _WSACleanup(); 31 | __declspec(dllexport) int WSAAPI _WSAGetLastError(); 32 | __declspec(dllexport) char* WSAAPI _inet_ntoa(in_addr in); 33 | __declspec(dllexport) SOCKET WSAAPI _socket(int af, int type, int protocol); 34 | __declspec(dllexport) int WSAAPI _bind(SOCKET s, const sockaddr* name, 35 | int namelen); 36 | __declspec(dllexport) int WSAAPI _closesocket(SOCKET s); 37 | __declspec(dllexport) hostent* WSAAPI _gethostbyname(const char* name); 38 | } 39 | -------------------------------------------------------------------------------- /src/backend/bin/test_server.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | namespace fs = std::filesystem; 6 | #include 7 | 8 | #include "base/windows_shit.h" 9 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 10 | #include "backend/config.h" 11 | #include "backend/hook/mock_trainer.h" 12 | #include "base/logging.h" 13 | #include "base/macro.h" 14 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 15 | #include "absl/debugging/failure_signal_handler.h" 16 | #include "absl/debugging/symbolize.h" 17 | __YRTR_END_THIRD_PARTY_HEADERS 18 | #include "base/thread.h" 19 | #include "protocol/server.h" 20 | 21 | namespace yrtr { 22 | static std::unique_ptr trainer; 23 | static std::unique_ptr server; 24 | 25 | static void Init(const char* exe_path) { 26 | absl::InitializeSymbolizer(exe_path); 27 | absl::FailureSignalHandlerOptions options; 28 | options.call_previous_handler = true; 29 | absl::InstallFailureSignalHandler(options); 30 | logging::InitLogging(); 31 | // Setup thread id. 32 | SetupGameLoopThreadOnce(); 33 | CHECK(backend::Config::Load(fs::canonical(fs::path(exe_path)).parent_path())); 34 | trainer = 35 | std::make_unique(backend::Config::instance()); 36 | server = std::make_unique(trainer.get(), 37 | backend::Config::instance()->port()); 38 | } 39 | 40 | static void Update() { 41 | DCHECK(IsWithinGameLoopThread()); 42 | DCHECK_NOTNULL(trainer); 43 | trainer->Update(/*delta*/ 0.015); 44 | DCHECK_NOTNULL(server); 45 | server->Update(); 46 | } 47 | 48 | static void OnExit() { 49 | server->Stop(); 50 | server.reset(); 51 | trainer.reset(); 52 | } 53 | } // namespace yrtr 54 | 55 | volatile bool should_stop = false; 56 | 57 | static void SignalHandlerToStopServer(int signal) { 58 | should_stop = true; 59 | } 60 | 61 | int main(int argc, char* argv[]) { 62 | // Win32 does not support SIGINT, what can I say... 63 | signal(SIGABRT, SignalHandlerToStopServer); 64 | // Simulate game loop. 65 | yrtr::Init(argv[0]); 66 | while (!should_stop) { 67 | yrtr::Update(); 68 | std::this_thread::sleep_for(std::chrono::milliseconds(15)); 69 | } 70 | LOG_F(INFO, "Exit test server"); 71 | yrtr::OnExit(); 72 | } 73 | -------------------------------------------------------------------------------- /src/base/macro.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef DISALLOW_COPY 4 | #define DISALLOW_COPY(T) T(const T& other) = delete 5 | #endif 6 | 7 | #ifndef DISALLOW_ASSIGN 8 | #define DISALLOW_ASSIGN(T) T operator=(const T& other) = delete 9 | #endif 10 | 11 | #ifndef DISALLOW_COPY_AND_ASSIGN 12 | #define DISALLOW_COPY_AND_ASSIGN(T) \ 13 | DISALLOW_COPY(T); \ 14 | DISALLOW_ASSIGN(T) 15 | #endif 16 | 17 | #ifndef UNREACHABLE 18 | #ifdef _WIN32 19 | #define UNREACHABLE() LOG(FATAL) << "unreachable"; __assume(false) 20 | #elif __linux__ 21 | #define UNREACHABLE() LOG(FATAL) << "unreachable"; __builtin_unreachable() 22 | #else 23 | #error unknown platform 24 | #endif 25 | #endif 26 | 27 | #ifndef NOT_IMPLEMENTED 28 | #define NOT_IMPLEMENTED() LOG(FATAL) << "not implemented"; abort() 29 | #endif 30 | 31 | #define LIKELY(cond) __builtin_expect (!!(cond), 1) 32 | #define UNLIKELY(cond) __builtin_expect (!!(cond), 0) 33 | 34 | // Unfortunately, msvc not have this. 35 | #ifndef _WIN32 36 | // For clang. 37 | #if __has_attribute(guarded_by) 38 | #else 39 | #warning "No attribute to check mutex" 40 | #endif 41 | #endif 42 | 43 | #ifndef UNUSED_VAR 44 | #ifdef _MSC_VER 45 | #define UNUSED_VAR(x) (x) 46 | #else 47 | #define UNUSED_VAR(x) 48 | #endif 49 | #endif 50 | 51 | #ifdef _MSC_VER 52 | // From io.h 53 | #define WRITE _write 54 | // From unistd.h 55 | /* Standard file descriptors. */ 56 | #define STDIN_FILENO 0 /* Standard input. */ 57 | #define STDOUT_FILENO 1 /* Standard output. */ 58 | #define STDERR_FILENO 2 /* Standard error output. */ 59 | #elif __linux__ 60 | // From unistd.h 61 | #define WRITE write 62 | #else 63 | #error unknown platform 64 | #endif 65 | 66 | #define CACHE_LINE_SIZE 64 67 | 68 | #ifdef _MSC_VER 69 | // Abseil logging reports warning C4127: conditional expression is constant 70 | // Abseil flags reports warning C4324: 'absl::lts_20240722::flags_internal::FlagValueAndInitBit': structure was padded due to alignment specifier 71 | // warning C4731: 'yrpp::XxxClass::Xxx': frame pointer register 'ebp' modified by inline assembly 72 | #define __YRTR_BEGIN_THIRD_PARTY_HEADERS \ 73 | _Pragma("warning(push, 0)") \ 74 | _Pragma("warning(disable:4127)") \ 75 | _Pragma("warning(disable:4324)") \ 76 | _Pragma("warning(disable:4731)") 77 | #define __YRTR_END_THIRD_PARTY_HEADERS _Pragma("warning(pop)") 78 | #else 79 | #define __YRTR_BEGIN_THIRD_PARTY_HEADERS 80 | #define __YRTR_END_THIRD_PARTY_HEADERS 81 | #endif 82 | -------------------------------------------------------------------------------- /src/backend/hook/hook_point.h: -------------------------------------------------------------------------------- 1 | // List all hook points here to detect conflictions easily. 2 | #pragma once 3 | #include 4 | #include 5 | 6 | namespace yrtr { 7 | namespace backend { 8 | namespace hook { 9 | // Hook at where and how many asm code bytes the hook point occupies. 10 | using HookPoint = std::pair; 11 | 12 | constexpr uint32_t GetJumpBack(HookPoint hp) { 13 | return hp.first + hp.second; 14 | } 15 | 16 | // Assume the executable's image base always be 0x00400000. 17 | static constexpr uint32_t kImageBase = 0x00400000; 18 | 19 | static constexpr HookPoint kHpBuiltinLogging = {0x004068E0, 5}; 20 | static constexpr HookPoint kHpUpdate = {0x007E1530, 5}; 21 | static constexpr HookPoint kHpExitGame = {0x007E14AC, 5}; 22 | 23 | static constexpr HookPoint kHpSellTheWorldCursor = {0x006929D1, 7}; 24 | static constexpr HookPoint kHpSellTheWorldBlong = {0x004C6F48, 6}; 25 | static constexpr HookPoint kHpSellTheWorldBuilder = {0x0044711B, 6}; 26 | static constexpr HookPoint kHpGodPlayer = {0x005F5509, 7}; 27 | // Chrono legionnaire check attackable. 28 | static constexpr HookPoint kHpCanWrapTarget = {0x0071AE9D, 7}; 29 | static constexpr HookPoint kHpInstBuild = {0x004C9B92, 5}; 30 | static constexpr HookPoint kHpInstFire = {0x006FC955, 6}; 31 | static constexpr HookPoint kHpRangeToYourBase = {0x006F7248, 6}; 32 | static constexpr HookPoint kHpFireToYourBase = {0x0070138F, 7}; 33 | static constexpr HookPoint kHpFreezeGapGenerator = {0x006FAF0D, 6}; 34 | static constexpr HookPoint kHpBuildEveryWhereGround = {0x004A8EB0, 5}; 35 | static constexpr HookPoint kHpBuildEveryWhereWater = {0x0047C9CD, 7}; 36 | static constexpr HookPoint kHpAutoRepairNeutral = {0x00452644, 6}; 37 | static constexpr HookPoint kHpCapturedMine = {0x00519F71, 6}; 38 | static constexpr HookPoint kHpSocialismMajestyCome = {0x004692BD, 6}; 39 | static constexpr HookPoint kHpSocialismMajestyBack = {0x00471FF0, 8}; 40 | static constexpr HookPoint kHpGarrisonedMine = {0x0045831A, 6}; 41 | static constexpr HookPoint kHpInvadeMode = {0x006F85DD, 5}; 42 | static constexpr HookPoint kHpUnlimitTech = {0x004F7870, 7}; 43 | static constexpr HookPoint kHpFastReload = {0x006B7D08, 6}; 44 | static constexpr HookPoint kHpMoreAmmunition = {0x006B6D0A, 6}; 45 | static constexpr HookPoint kHpInstChronoMove = {0x00719579, 7}; 46 | static constexpr HookPoint kHpInstChronoAttack = {0x0071AFB7, 5}; 47 | static constexpr HookPoint kHpSpySpy = {0x0051A002, 6}; 48 | static constexpr HookPoint kHpEverythingElited = {0x0075001A, 5}; 49 | 50 | } // namespace hook 51 | } // namespace backend 52 | } // namespace yrtr 53 | -------------------------------------------------------------------------------- /src/protocol/test_json.cc: -------------------------------------------------------------------------------- 1 | #include "base/logging.h" 2 | #include "nlohmann/json.hpp" 3 | #include "protocol/model.h" 4 | using json = nlohmann::json; 5 | 6 | namespace yrtr { 7 | static void InitStates(State& state) { 8 | state.ckbox_states.emplace(FnLabel::kGod, CheckboxState{.enable=true, .activate=false}); 9 | state.ckbox_states.emplace(FnLabel::kInstBuild, CheckboxState{.enable=true, .activate=false}); 10 | state.ckbox_states.emplace(FnLabel::kUnlimitSuperWeapon, CheckboxState{.enable=true, .activate=false}); 11 | state.ckbox_states.emplace(FnLabel::kInstFire, CheckboxState{.enable=true, .activate=false}); 12 | state.ckbox_states.emplace(FnLabel::kInstTurn, CheckboxState{.enable=true, .activate=false}); 13 | state.ckbox_states.emplace(FnLabel::kRangeToYourBase, CheckboxState{.enable=true, .activate=false}); 14 | state.ckbox_states.emplace(FnLabel::kFireToYourBase, CheckboxState{.enable=false, .activate=false}); 15 | state.ckbox_states.emplace(FnLabel::kFreezeGapGenerator, CheckboxState{.enable=true, .activate=false}); 16 | state.ckbox_states.emplace(FnLabel::kSellTheWorld, CheckboxState{.enable=true, .activate=false}); 17 | state.ckbox_states.emplace(FnLabel::kBuildEveryWhere, CheckboxState{.enable=true, .activate=false}); 18 | state.ckbox_states.emplace(FnLabel::kAutoRepair, CheckboxState{.enable=true, .activate=false}); 19 | state.ckbox_states.emplace(FnLabel::kSocialismMajesty, CheckboxState{.enable=true, .activate=false}); 20 | state.ckbox_states.emplace(FnLabel::kMakeGarrisonedMine, CheckboxState{.enable=true, .activate=false}); 21 | state.ckbox_states.emplace(FnLabel::kInvadeMode, CheckboxState{.enable=true, .activate=false}); 22 | state.ckbox_states.emplace(FnLabel::kUnlimitTech, CheckboxState{.enable=true, .activate=false}); 23 | state.ckbox_states.emplace(FnLabel::kUnlimitFirePower, CheckboxState{.enable=true, .activate=false}); 24 | state.ckbox_states.emplace(FnLabel::kInstChrono, CheckboxState{.enable=true, .activate=false}); 25 | state.ckbox_states.emplace(FnLabel::kSpySpy, CheckboxState{.enable=true, .activate=false}); 26 | state.ckbox_states.emplace(FnLabel::kAdjustGameSpeed, CheckboxState{.enable=true, .activate=false}); 27 | state.selecting_houses.emplace(456, SideDesc{456, "456"}); 28 | state.protected_houses.emplace(123, SideDesc{123, "123"}); 29 | } 30 | } // namespace yrtr 31 | 32 | int main() { 33 | yrtr::logging::InitLogging(); 34 | yrtr::State state_send; 35 | yrtr::InitStates(state_send); 36 | 37 | json data(state_send); 38 | LOG(INFO) << data.dump(); 39 | LOG_F(INFO, "data length={}", data.dump().size()); 40 | 41 | yrtr::State state_recv = json::parse(data.dump()); 42 | json data2(state_recv); 43 | LOG(INFO) << data2.dump(); 44 | 45 | return 0; 46 | } 47 | -------------------------------------------------------------------------------- /src/backend/lib/dllmain.cc: -------------------------------------------------------------------------------- 1 | #include 2 | namespace fs = std::filesystem; 3 | #include 4 | 5 | #include "base/windows_shit.h" 6 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 7 | #include "backend/config.h" 8 | #include "backend/hook/game_loop.h" 9 | #include "backend/hook/hook.h" 10 | #include "base/macro.h" 11 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 12 | #include "absl/debugging/failure_signal_handler.h" 13 | #include "absl/debugging/symbolize.h" 14 | __YRTR_END_THIRD_PARTY_HEADERS 15 | #include "base/logging.h" 16 | 17 | namespace { 18 | std::string GetModule(HINSTANCE hInst) { 19 | char path[MAX_PATH] = {0}; 20 | DWORD nSize = GetModuleFileNameA(hInst, path, _countof(path)); 21 | if (nSize > 0) { 22 | return std::string(path); 23 | } else { 24 | return ""; 25 | } 26 | } 27 | } // namespace 28 | 29 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, 30 | LPVOID lpvReserved) { 31 | switch (fdwReason) { 32 | case DLL_PROCESS_ATTACH: { 33 | OutputDebugStringA(std::format("[YRTR] Process attach base={:08X}", 34 | reinterpret_cast(hinstDLL)) 35 | .c_str()); 36 | yrtr::logging::InitLogging(yrtr::logging::LogSink::kFile, 37 | yrtr::backend::Config::kLogFileName); 38 | std::string dll_path = GetModule(hinstDLL); 39 | if (!dll_path.empty()) { 40 | absl::InitializeSymbolizer(dll_path.c_str()); 41 | absl::FailureSignalHandlerOptions options; 42 | options.call_previous_handler = true; 43 | absl::InstallFailureSignalHandler(options); 44 | // Only install hooks inside game executable. Maybe gamemd.exe, 45 | // gameares.exe, gamemd-spawn.exe... 46 | std::string exe_path = GetModule(NULL); 47 | if (fs::path(exe_path).filename().string().find("game") != 48 | std::string::npos) { 49 | InstallHooks(); 50 | } 51 | } else { 52 | DWORD err = GetLastError(); 53 | std::string message = std::system_category().message(err); 54 | MessageBoxA(NULL, 55 | std::format("Failed to get module file name, err=[{}]{}", 56 | err, message) 57 | .c_str(), 58 | "Ra2 trainer module symbol loading error", 0); 59 | // Allow carry on without symbol hints. 60 | } 61 | break; 62 | } 63 | case DLL_THREAD_ATTACH: 64 | break; 65 | case DLL_THREAD_DETACH: 66 | break; 67 | case DLL_PROCESS_DETACH: { 68 | OutputDebugStringA("[YRTR] Process detach"); 69 | yrtr::backend::hook::ReclaimResourceOnce(); 70 | if (lpvReserved != nullptr) { 71 | break; 72 | } 73 | break; 74 | } 75 | } 76 | return TRUE; 77 | } 78 | -------------------------------------------------------------------------------- /src/base/thread.cc: -------------------------------------------------------------------------------- 1 | #include "base/thread.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "base/windows_shit.h" 9 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 10 | 11 | namespace yrtr { 12 | 13 | namespace { 14 | // Not use thread_local because thread ids are passed to other dll modules and 15 | // set in initialize stage to serve some invocation, like SetMenuActive, before 16 | // any of loops initialize its thread local thread id. 17 | 18 | // Game loop thread is only available on backend. 19 | static std::atomic tid_game_loop = 0; 20 | // Render loop thread is only available on frontend. 21 | static std::atomic tid_render_loop = 0; 22 | // 1 thread for network should be enough. 23 | static std::atomic tid_net_loop = 0; 24 | } // namespace 25 | 26 | ThreadId GetGameLoopThreadId() { return tid_game_loop.load(); } 27 | ThreadId GetRendererThreadId() { return tid_render_loop.load(); } 28 | ThreadId GetNetThreadId() { return tid_net_loop.load(); } 29 | 30 | void SetupGameLoopThreadOnce(ThreadId tid) { 31 | static std::once_flag once_flag; 32 | std::call_once(once_flag, [&]() { 33 | if (tid == 0) { 34 | tid_game_loop.store(GetCurrentThreadId()); 35 | } else { 36 | tid_game_loop.store(tid); 37 | } 38 | }); 39 | } 40 | 41 | void SetupRendererThreadOnce(ThreadId tid) { 42 | static std::once_flag once_flag; 43 | std::call_once(once_flag, [&]() { 44 | if (tid == 0) { 45 | tid_render_loop.store(GetCurrentThreadId()); 46 | } else { 47 | tid_render_loop.store(tid); 48 | } 49 | }); 50 | } 51 | 52 | void SetupNetThreadOnce(ThreadId tid) { 53 | static std::once_flag once_flag; 54 | std::call_once(once_flag, [&]() { 55 | if (tid == 0) { 56 | tid_net_loop.store(GetCurrentThreadId()); 57 | } else { 58 | tid_net_loop.store(tid); 59 | } 60 | }); 61 | } 62 | 63 | bool IsWithinThread(ThreadId tid) { 64 | return tid == GetCurrentThreadId(); 65 | } 66 | 67 | bool IsWithinGameLoopThread() { 68 | return tid_game_loop.load() == GetCurrentThreadId(); 69 | } 70 | 71 | bool IsWithinRendererThread() { 72 | return tid_render_loop.load() == GetCurrentThreadId(); 73 | } 74 | 75 | bool IsWithinNetThread() { 76 | return tid_net_loop.load() == GetCurrentThreadId(); 77 | } 78 | 79 | std::string InspectThreads() { 80 | ThreadId tid_current = GetCurrentThreadId(); 81 | std::stringstream ss; 82 | ss << "===== Threads: =====\n"; 83 | ss << std::format("Current tid={}\n", tid_current); 84 | ss << std::format("Game loop tid={}\n", tid_game_loop.load()); 85 | ss << std::format("Renderer tid={}\n", tid_render_loop.load()); 86 | ss << std::format("Net tid={}\n", tid_net_loop.load()); 87 | ss << "====================\n"; 88 | return ss.str(); 89 | } 90 | 91 | } // namespace yrtr 92 | -------------------------------------------------------------------------------- /src/frontend/web/protocol.js: -------------------------------------------------------------------------------- 1 | export { 2 | BtnFnLabelFirst, BtnFnLabelLast, 3 | CheckboxFnLabelFirst, CheckboxFnLabelLast, 4 | FnLabel, strFnLabel 5 | }; 6 | 7 | const BtnFnLabelFirst = 1; 8 | const BtnFnLabelLast = 8; 9 | const CheckboxFnLabelFirst = 9; 10 | const CheckboxFnLabelLast = 28; 11 | // Enum mapping 12 | const FnLabel = { 13 | kInvalid: -1, 14 | // Button 15 | kApply: 0, 16 | kIAMWinner: 1, 17 | kDeleteUnit: 2, 18 | kClearShroud: 3, 19 | kGiveMeABomb: 4, 20 | kUnitLevelUp: 5, 21 | kUnitSpeedUp: 6, 22 | kFastBuild: 7, 23 | kThisIsMine: 8, 24 | // Checkbox 25 | kGod: 9, 26 | kInstBuild: 10, 27 | kUnlimitSuperWeapon: 11, 28 | kInstFire: 12, 29 | kInstTurn: 13, 30 | kRangeToYourBase: 14, 31 | kFireToYourBase: 15, 32 | kFreezeGapGenerator: 16, 33 | kSellTheWorld: 17, 34 | kBuildEveryWhere: 18, 35 | kAutoRepair: 19, 36 | kSocialismMajesty: 20, 37 | kMakeCapturedMine: 21, 38 | kMakeGarrisonedMine: 22, 39 | kInvadeMode: 23, 40 | kUnlimitTech: 24, 41 | kUnlimitFirePower: 25, 42 | kInstChrono: 26, 43 | kSpySpy: 27, 44 | kAdjustGameSpeed: 28, 45 | kCount: 29 46 | }; 47 | 48 | // Helper function to get string representation of FnLabel. Only for internal 49 | // use. To get localized name, use getGuiStr and getFnStr. 50 | function strFnLabel(label) { 51 | switch (label) { 52 | case FnLabel.kInvalid: return "Invalid"; 53 | // Button 54 | case FnLabel.kApply: return "Apply"; 55 | case FnLabel.kIAMWinner: return "IAMWinner"; 56 | case FnLabel.kDeleteUnit: return "DeleteUnit"; 57 | case FnLabel.kClearShroud: return "ClearShroud"; 58 | case FnLabel.kGiveMeABomb: return "GiveMeABomb"; 59 | case FnLabel.kUnitLevelUp: return "UnitLevelUp"; 60 | case FnLabel.kUnitSpeedUp: return "UnitSpeedUp"; 61 | case FnLabel.kFastBuild: return "FastBuild"; 62 | case FnLabel.kThisIsMine: return "ThisIsMine"; 63 | // Checkbox 64 | case FnLabel.kGod: return "God"; 65 | case FnLabel.kInstBuild: return "InstBuild"; 66 | case FnLabel.kUnlimitSuperWeapon: return "UnlimitSuperWeapon"; 67 | case FnLabel.kInstFire: return "InstFire"; 68 | case FnLabel.kInstTurn: return "InstTurn"; 69 | case FnLabel.kRangeToYourBase: return "RangeToYourBase"; 70 | case FnLabel.kFireToYourBase: return "FireToYourBase"; 71 | case FnLabel.kFreezeGapGenerator: return "FreezeGapGenerator"; 72 | case FnLabel.kSellTheWorld: return "SellTheWorld"; 73 | case FnLabel.kBuildEveryWhere: return "BuildEveryWhere"; 74 | case FnLabel.kAutoRepair: return "AutoRepair"; 75 | case FnLabel.kSocialismMajesty: return "SocialismMajesty"; 76 | case FnLabel.kMakeCapturedMine: return "MakeCapturedMine"; 77 | case FnLabel.kMakeGarrisonedMine: return "MakeGarrisonedMine"; 78 | case FnLabel.kInvadeMode: return "InvadeMode"; 79 | case FnLabel.kUnlimitTech: return "UnlimitTech"; 80 | case FnLabel.kUnlimitFirePower: return "UnlimitFirePower"; 81 | case FnLabel.kInstChrono: return "InstChrono"; 82 | case FnLabel.kSpySpy: return "SpySpy"; 83 | case FnLabel.kAdjustGameSpeed: return "AdjustGameSpeed"; 84 | case FnLabel.kCount: return "Count"; 85 | default: return "unknown"; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/backend/config.cc: -------------------------------------------------------------------------------- 1 | #include "backend/config.h" 2 | 3 | #include 4 | 5 | #include "base/logging.h" 6 | #include "formatter/std.h" 7 | #include "gsl/narrow" 8 | 9 | namespace yrtr { 10 | namespace backend { 11 | 12 | namespace { 13 | std::string_view get_log_header() { return "Config "; } 14 | 15 | template 16 | std::optional TryLoad(const toml::table& tb, std::string_view key) { 17 | auto raw_val = tb.get_as(key); 18 | if (raw_val) { 19 | return raw_val->get(); 20 | } else { 21 | return std::nullopt; 22 | } 23 | } 24 | 25 | template 26 | T CheckLoad(const toml::table& tb, std::string_view key) { 27 | auto raw_val = tb.get_as(key); 28 | CHECK_NOTNULL(raw_val); 29 | if constexpr (std::is_integral_v>) { 30 | return *raw_val; 31 | } else { 32 | return raw_val->get(); 33 | } 34 | } 35 | } // namespace 36 | 37 | std::unique_ptr Config::inst_; 38 | 39 | bool Config::Load(const fs::path& cfg_dir) { 40 | fs::path cfg_path = cfg_dir / kCfgFileName; 41 | if (!fs::exists(cfg_path)) { 42 | HLOG_F(ERROR, "Not found config file={}", fs::absolute(cfg_path)); 43 | return false; 44 | } 45 | inst_ = std::make_unique(cfg_path); 46 | HLOG_F(INFO, "Load from path={}", fs::canonical(cfg_path)); 47 | auto cfg = toml::parse_file(cfg_path.string()); 48 | if (toml::table* global = cfg["ra2_trainer"].as_table()) { 49 | inst_->LoadGlobal(*global); 50 | } else { 51 | HLOG_F(ERROR, "Failed to load table [ra2_trainer]"); 52 | return false; 53 | } 54 | if (toml::table* tech_tb = cfg["tech"].as_table()) { 55 | inst_->LoadTechList(*tech_tb); 56 | } else { 57 | HLOG_F(ERROR, "Failed to load table [tech]"); 58 | return false; 59 | } 60 | return true; 61 | } 62 | 63 | // Default configurations. 64 | Config::Config(const fs::path& cfg_path) 65 | : cfg_dir_(cfg_path.parent_path()), 66 | cfg_path_(cfg_path), 67 | hotreload_dir_(kDefaultHotreloadDir) {} 68 | 69 | fs::path Config::GetAbsolutePath(const fs::path& relpath) const { 70 | if (relpath.is_absolute()) { 71 | return relpath; 72 | } else { 73 | return cfg_dir_ / relpath; 74 | } 75 | } 76 | 77 | void Config::LoadGlobal(const toml::table& global) { 78 | port_ = gsl::narrow_cast(CheckLoad(global, "port")); 79 | hotreload_dir_ = 80 | cfg_dir_ / TryLoad(global, "hotreload_directory") 81 | .value_or(std::string(kDefaultHotreloadDir)); 82 | // Verify. 83 | #ifdef YRTR_DEBUG 84 | if (hotreload_dir_ != "" && !fs::exists(hotreload_dir_)) { 85 | // Hotreload directory is optional, only provides warning. 86 | HLOG_F(WARNING, "Failed to find hotreload_dir={}", hotreload_dir_); 87 | } 88 | #endif 89 | auto_record_ = CheckLoad(global, "auto_record"); 90 | } 91 | 92 | void Config::LoadTechList(const toml::table& tech_tb) { 93 | for (auto [k, v] : tech_tb) { 94 | std::string_view key = k.str(); 95 | Tech tech = GetTech(key); 96 | CHECK(tech != Tech::kUnknown && tech != Tech::kCount); 97 | auto raw_val = v.as(); 98 | CHECK_NOTNULL(raw_val); 99 | bool enable = raw_val->get(); 100 | tech_list_[static_cast(tech)] = enable; 101 | } 102 | } 103 | 104 | } // namespace backend 105 | } // namespace yrtr 106 | -------------------------------------------------------------------------------- /src/backend/hook/memory_api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #include "base/logging.h" 7 | #include "base/macro.h" 8 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 9 | #include "absl/container/inlined_vector.h" 10 | #include "absl/container/flat_hash_map.h" 11 | __YRTR_END_THIRD_PARTY_HEADERS 12 | #include "backend/hook/hook_point.h" 13 | 14 | namespace yrtr { 15 | namespace backend { 16 | namespace hook { 17 | 18 | class HandleGuard; 19 | 20 | class MemoryAPI { 21 | public: 22 | static MemoryAPI* instance() { return inst_.get(); } 23 | static void Init(); 24 | static void Destroy(); 25 | MemoryAPI(); 26 | ~MemoryAPI(); 27 | MemoryAPI(MemoryAPI&&) = delete; 28 | MemoryAPI& operator=(MemoryAPI&&) = delete; 29 | 30 | bool ReadMemory(uint32_t address, std::span data) const; 31 | 32 | template 33 | requires(!std::is_same_v>) 34 | bool ReadMemory(uint32_t address, T* data) const { 35 | return ReadMemory( 36 | address, 37 | std::span(reinterpret_cast(data), sizeof(T))); 38 | } 39 | 40 | bool ReadAddress(uint32_t address, uint32_t* data) const; 41 | 42 | template 43 | requires(N >= 1) 44 | bool ReadAddress(uint32_t address, uint32_t const (&offsets)[N], 45 | uint32_t* addr_output) const { 46 | CHECK(addr_output != nullptr); 47 | uint32_t addr_data; 48 | if (!ReadAddress(address, &addr_data)) { 49 | return false; 50 | } 51 | for (uint32_t offset : offsets | std::views::take(N - 1)) { 52 | uint32_t addr_next = addr_data + offset; 53 | if (!ReadAddress(addr_next, &addr_data)) { 54 | return false; 55 | } 56 | } 57 | *addr_output = addr_data + offsets[N - 1]; 58 | return true; 59 | } 60 | 61 | template 62 | requires(N >= 1) 63 | bool ReadMemory(uint32_t address, uint32_t const (&offsets)[N], 64 | uint32_t* data) const { 65 | CHECK(data != nullptr); 66 | uint32_t addr_output; 67 | if (!ReadAddress(address, offsets, &addr_output)) { 68 | return false; 69 | } 70 | return ReadMemory(addr_output, data); 71 | } 72 | 73 | bool WriteMemory(uint32_t address, std::span data) const; 74 | 75 | template 76 | requires(!std::is_convertible_v>) 77 | bool WriteMemory(uint32_t address, T data) const { 78 | return WriteMemory( 79 | address, 80 | std::span(reinterpret_cast(&data), sizeof(T))); 81 | } 82 | 83 | bool HasHook(const HookPoint hook_point) const; 84 | bool HookJump(const HookPoint hook_point, void* dest); 85 | bool HookNop(const HookPoint hook_point); 86 | bool RestoreHook(const HookPoint hook_point); 87 | 88 | bool CheckHandle() const; 89 | bool AutoAssemble(std::string_view script, bool activate) const; 90 | 91 | private: 92 | static std::unique_ptr inst_; 93 | std::unique_ptr handle_; 94 | absl::flat_hash_map> 95 | hooks_; 96 | 97 | bool WriteHook(uint32_t addr, std::span code); 98 | // For reclaiming resources. 99 | void RestoreAllHooks(); 100 | 101 | DISALLOW_COPY_AND_ASSIGN(MemoryAPI); 102 | }; 103 | 104 | } // namespace hook 105 | } // namespace backend 106 | } // namespace yrtr 107 | -------------------------------------------------------------------------------- /src/backend/hook/mock_trainer.cc: -------------------------------------------------------------------------------- 1 | #include "backend/hook/mock_trainer.h" 2 | 3 | #include "backend/record.h" 4 | #include "base/logging.h" 5 | #include "base/thread.h" 6 | 7 | namespace yrtr { 8 | namespace backend { 9 | namespace hook { 10 | SideMap MockTrainer::protected_houses_; 11 | 12 | MockTrainer::MockTrainer(Config* cfg) 13 | : cfg_(cfg), 14 | state_dirty_(true) { 15 | InitStates(state_); 16 | if (cfg_->auto_record()) { 17 | ReadCheckboxStateFromToml(cfg_->record_path(), /*out*/ state_.ckbox_states); 18 | } 19 | } 20 | 21 | MockTrainer::~MockTrainer() { 22 | if (cfg_->auto_record()) { 23 | WriteCheckboxStateToToml(state_.ckbox_states, cfg_->record_path()); 24 | } 25 | } 26 | 27 | void MockTrainer::Update(double /*delta*/) { 28 | CHECK(IsWithinGameLoopThread()); 29 | // Update selecting houses to view. 30 | SideMap selecting_houses; 31 | selecting_houses.emplace(123, SideDesc{ 32 | .uniq_id = 123, 33 | .name = "mock player 123", 34 | }); 35 | selecting_houses.emplace(456, SideDesc{ 36 | .uniq_id = 456, 37 | .name = "mock player 456", 38 | }); 39 | if (!AreEqual(state_.selecting_houses, selecting_houses)) { 40 | state_.selecting_houses = std::move(selecting_houses); 41 | state_dirty_ = true; 42 | } 43 | protected_houses_ = state_.protected_houses; 44 | PropagateStateIfDirty(); 45 | } 46 | 47 | void MockTrainer::OnInputEvent(FnLabel label, uint32_t val) { 48 | CHECK(IsWithinGameLoopThread()); 49 | // There's only one input event for now. 50 | CHECK_EQ(static_cast(label), static_cast(FnLabel::kApply)); 51 | LOG_F(INFO, "OnInputEvent label={} val={}", StrFnLabel(label), val); 52 | BeepEnable(); 53 | PropagateStateIfDirty(); 54 | } 55 | 56 | void MockTrainer::OnButtonEvent(FnLabel label) { 57 | CHECK(IsWithinGameLoopThread()); 58 | LOG_F(INFO, "OnButtonEvent label={}", StrFnLabel(label)); 59 | BeepEnable(); 60 | PropagateStateIfDirty(); 61 | } 62 | 63 | void MockTrainer::OnCheckboxEvent(FnLabel label, bool activate) { 64 | CHECK(IsWithinGameLoopThread()); 65 | LOG_F(INFO, "OnCheckboxEvent label={} activate={}", StrFnLabel(label), 66 | activate); 67 | UpdateCheckboxState(label, activate); 68 | PropagateStateIfDirty(); 69 | } 70 | 71 | void MockTrainer::OnProtectedListEvent(SideMap&& side_map) { 72 | CHECK(IsWithinGameLoopThread()); 73 | if (!AreEqual(state_.protected_houses, side_map)) { 74 | state_.protected_houses = std::move(side_map); 75 | state_dirty_ = true; 76 | } 77 | PropagateStateIfDirty(); 78 | } 79 | 80 | void MockTrainer::PropagateStateIfDirty() { 81 | DCHECK(IsWithinGameLoopThread()); 82 | if (!state_dirty_) { 83 | return; 84 | } 85 | state_dirty_ = false; 86 | if (on_state_updated_ == nullptr) { 87 | return; 88 | } 89 | on_state_updated_(state_); 90 | } 91 | 92 | void MockTrainer::UpdateCheckboxState(FnLabel label, bool activate) { 93 | if (activate) { 94 | BeepEnable(); 95 | } else { 96 | BeepDisable(); 97 | } 98 | if (state_.ckbox_states[label].activate != activate) { 99 | state_.ckbox_states[label].activate = activate; 100 | state_dirty_ = true; 101 | } 102 | } 103 | 104 | } // namespace hook 105 | } // namespace backend 106 | } // namespace yrtr 107 | -------------------------------------------------------------------------------- /src/base/logging.cc: -------------------------------------------------------------------------------- 1 | #include "base/logging.h" 2 | 3 | #include 4 | 5 | #ifdef _WIN32 6 | #include "base/windows_shit.h" 7 | #endif 8 | 9 | namespace yrtr { 10 | namespace logging { 11 | 12 | #ifdef _WIN32 13 | WindowsDebuggerLogSink WindowsDebuggerLogSink::kLogSink; 14 | 15 | void WindowsDebuggerLogSink::Send(const absl::LogEntry& entry) { 16 | if (entry.log_severity() < absl::StderrThreshold()) { 17 | return; 18 | } 19 | mu_.lock(); 20 | ::OutputDebugStringA(entry.text_message_with_prefix_and_newline_c_str()); 21 | if (!entry.stacktrace().empty()) [[unlikely]] { 22 | ::OutputDebugStringA(entry.stacktrace().data()); 23 | } 24 | mu_.unlock(); 25 | } 26 | #endif 27 | 28 | StderrLogSink StderrLogSink::kLogSink; 29 | 30 | void StderrLogSink::Send(const absl::LogEntry& entry) { 31 | if (entry.log_severity() < absl::StderrThreshold()) { 32 | return; 33 | } 34 | mu_.lock(); 35 | absl::log_internal::WriteToStderr( 36 | entry.text_message_with_prefix_and_newline(), entry.log_severity()); 37 | if (!entry.stacktrace().empty()) [[unlikely]] { 38 | absl::log_internal::WriteToStderr(entry.stacktrace(), entry.log_severity()); 39 | } 40 | mu_.unlock(); 41 | } 42 | 43 | FileLogSink FileLogSink::kLogSink; 44 | 45 | FileLogSink::~FileLogSink() { 46 | std::lock_guard lock(mu_); 47 | if (log_file_.is_open()) { 48 | log_file_.close(); 49 | } 50 | } 51 | 52 | bool FileLogSink::SetLogFile(std::string_view filename) { 53 | std::lock_guard lock(kLogSink.mu_); 54 | if (kLogSink.log_file_.is_open()) { 55 | kLogSink.log_file_.close(); 56 | } 57 | kLogSink.log_file_.open(filename.data(), std::ios::out | std::ios::trunc); 58 | if (!kLogSink.log_file_.is_open()) { 59 | return false; 60 | } 61 | // Write initial marker. 62 | auto now = std::chrono::system_clock::now(); 63 | auto now_time = std::chrono::system_clock::to_time_t(now); 64 | std::string time_str; 65 | time_str.resize(26); 66 | errno_t ret = ctime_s(time_str.data(), time_str.size(), &now_time); 67 | // Remove the trailing newline. 68 | while (ret == 0 && !time_str.empty() && 69 | (time_str.back() == '\n' || time_str.back() == '\0')) { 70 | time_str.pop_back(); 71 | } 72 | kLogSink.log_file_ << "=== Log started at " << time_str << " ===\n"; 73 | return true; 74 | } 75 | 76 | void FileLogSink::Send(const absl::LogEntry& entry) { 77 | std::lock_guard lock(mu_); 78 | if (log_file_.is_open()) { 79 | log_file_ << entry.text_message_with_prefix_and_newline(); 80 | if (!entry.stacktrace().empty()) [[unlikely]] { 81 | log_file_ << entry.stacktrace(); 82 | } 83 | log_file_.flush(); 84 | } 85 | } 86 | 87 | namespace { 88 | static absl::LogSink* kLogSinkInst = nullptr; 89 | static std::ostringstream g_nullstream; 90 | } 91 | 92 | std::ostringstream& get_nullstream() { return g_nullstream; } 93 | 94 | void InitLogging(LogSink log_sink, std::string_view log_file) { 95 | if (log_sink == LogSink::kStd) { 96 | absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); 97 | kLogSinkInst = StderrLogSink::get(); 98 | } else if (log_sink == LogSink::kFile) { 99 | if (!FileLogSink::SetLogFile(log_file)) { 100 | perror(std::format("Failed to open log file: {}", log_file).c_str()); 101 | abort(); 102 | } 103 | kLogSinkInst = FileLogSink::get(); 104 | } 105 | #ifdef _WIN32 106 | else if (log_sink == LogSink::kDbgView) { 107 | kLogSinkInst = WindowsDebuggerLogSink::get(); 108 | } 109 | #endif 110 | else { 111 | perror("Invalid log sink."); 112 | abort(); 113 | } 114 | // BUG: crash in DllMain 115 | // absl::InitializeLog(); 116 | } 117 | 118 | absl::LogSink* GetLogSink() { 119 | return kLogSinkInst; 120 | } 121 | 122 | } // namespace logging 123 | } // namespace yrtr 124 | -------------------------------------------------------------------------------- /src/frontend/web/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #f88080; 3 | --primary-dark: #f42929; 4 | --primary-light: #f9c8c8; 5 | --text-color: #000000; 6 | --bg-color: #f1f1f1; 7 | --disabled-color: #cccccc; 8 | --border-color: #cccccc; 9 | --hover-color: #f9a8a8; 10 | --active-color: #f42929; 11 | --tab-active: #f88080; 12 | --tab-hover: #f88; 13 | --frame-bg: #aaa; 14 | } 15 | 16 | body { 17 | font-family: Arial, sans-serif; 18 | margin: 0; 19 | padding: 0; 20 | background-color: var(--bg-color); 21 | color: var(--text-color); 22 | } 23 | 24 | .container { 25 | padding: 8px; 26 | padding-bottom: 60px; 27 | box-sizing: border-box; 28 | } 29 | 30 | .tab-bar { 31 | display: flex; 32 | border-bottom: 1px solid var(--border-color); 33 | margin-bottom: 8px; 34 | } 35 | 36 | .tab { 37 | padding: 8px 16px; 38 | cursor: pointer; 39 | border: 1px solid transparent; 40 | border-bottom: none; 41 | border-radius: 4px 4px 0 0; 42 | margin-right: 4px; 43 | background-color: var(--primary-light); 44 | } 45 | 46 | .tab:hover { 47 | background-color: var(--tab-hover); 48 | color: white; 49 | } 50 | 51 | .tab.active { 52 | background-color: var(--tab-active); 53 | color: white; 54 | border-color: var(--border-color); 55 | } 56 | 57 | .tab-content { 58 | display: none; 59 | } 60 | 61 | .tab-content.active { 62 | display: block; 63 | } 64 | 65 | .input-group { 66 | display: flex; 67 | align-items: center; 68 | margin-bottom: 8px; 69 | column-gap: 8px; 70 | } 71 | 72 | label { 73 | display: block; 74 | margin-top: 4px; 75 | margin-bottom: 4px; 76 | } 77 | 78 | input[type="text"], 79 | input[type="number"] { 80 | width: 100px; 81 | padding: 4px; 82 | border: 1px solid var(--border-color); 83 | border-radius: 4px; 84 | } 85 | 86 | button { 87 | display: flex; 88 | padding: 6px 8px; 89 | background-color: var(--primary-color); 90 | color: white; 91 | border: none; 92 | border-radius: 4px; 93 | cursor: pointer; 94 | margin-right: 8px; 95 | margin-top: 8px; 96 | margin-bottom: 8px; 97 | user-select: none; 98 | } 99 | 100 | button:hover { 101 | background-color: var(--primary-dark); 102 | } 103 | 104 | button:disabled { 105 | background-color: var(--disabled-color); 106 | cursor: not-allowed; 107 | } 108 | 109 | .checkbox-group { 110 | padding-top: 1px; 111 | margin-bottom: 8px; 112 | } 113 | 114 | .checkbox-label { 115 | display: flex; 116 | align-items: center; 117 | cursor: pointer; 118 | user-select: none; 119 | } 120 | 121 | .checkbox-label input { 122 | margin-right: 8px; 123 | scale: 140%; 124 | } 125 | 126 | .split-view { 127 | display: flex; 128 | width: 100%; 129 | gap: 8px; 130 | } 131 | 132 | .panel { 133 | flex: 1; 134 | border: 1px solid var(--border-color); 135 | border-radius: 4px; 136 | padding: 8px; 137 | background-color: white; 138 | } 139 | 140 | .panel-header { 141 | display: flex; 142 | justify-content: space-between; 143 | align-items: center; 144 | margin-bottom: 8px; 145 | } 146 | 147 | .list-box { 148 | height: 300px; 149 | overflow-y: auto; 150 | border: 1px solid var(--border-color); 151 | border-radius: 4px; 152 | padding: 4px; 153 | } 154 | 155 | .selectable { 156 | padding: 4px; 157 | cursor: pointer; 158 | margin-bottom: 2px; 159 | border-radius: 2px; 160 | user-select: none; 161 | } 162 | 163 | .selectable:hover { 164 | background-color: var(--hover-color); 165 | } 166 | 167 | .selectable.selected { 168 | background-color: var(--primary-color); 169 | color: white; 170 | } 171 | 172 | .disabled { 173 | opacity: 0.6; 174 | pointer-events: none; 175 | } -------------------------------------------------------------------------------- /src/frontend/web/localization.js: -------------------------------------------------------------------------------- 1 | export class Localization { 2 | constructor() { 3 | this.setLanguage(navigator.language); 4 | this.guiLabels = { 5 | zh: { 6 | kTitle: "辅助工具", 7 | kState: "状态", 8 | kStateOk: "游戏运行中", 9 | kStateIdle: "未检测到游戏", 10 | kMoney: "钱", 11 | kAssist: "功能", 12 | kFilter: "过滤", 13 | kSelectingHouseList: "选中阵营", 14 | kProtectedHouseList: "保护阵营", 15 | kAddAll: "添加全部", 16 | kClearAll: "删除全部" 17 | }, 18 | en: { 19 | kTitle: "Assist Tool", 20 | kState: "State", 21 | kStateOk: "Game running", 22 | kStateIdle: "Game not running", 23 | kMoney: "Money", 24 | kAssist: "Assist", 25 | kFilter: "Filter", 26 | kSelectingHouseList: "Selecting house", 27 | kProtectedHouseList: "Protected house", 28 | kAddAll: "Add all", 29 | kClearAll: "Clear all" 30 | } 31 | }; 32 | 33 | this.fnLabels = { 34 | zh: { 35 | kApply: "修改", 36 | kFastBuild: "快速建造", 37 | kDeleteUnit: "删除单位", 38 | kClearShroud: "地图全开", 39 | kGiveMeABomb: "核弹攻击", 40 | kUnitLevelUp: "单位升级", 41 | kUnitSpeedUp: "单位加速", 42 | kIAMWinner: "立即胜利", 43 | kThisIsMine: "这是我的", 44 | kGod: "无敌", 45 | kInstBuild: "瞬间建造", 46 | kUnlimitSuperWeapon: "无限超武", 47 | kInstFire: "极速攻击", 48 | kInstTurn: "极速转身", 49 | kRangeToYourBase: "远程攻击", 50 | kFireToYourBase: "远程警戒", 51 | kFreezeGapGenerator: "瘫痪裂缝产生器", 52 | kSellTheWorld: "卖卖卖", 53 | kBuildEveryWhere: "随意建筑", 54 | kAutoRepair: "自动修理", 55 | kSocialismMajesty: "社会主义万岁", 56 | kMakeCapturedMine: "全是我的-工程师占领", 57 | kMakeGarrisonedMine: "全是我的-房屋驻军", 58 | kInvadeMode: "侵略模式", 59 | kUnlimitTech: "全科技", 60 | kUnlimitFirePower: "大量弹药-重新建造生效", 61 | kInstChrono: "瞬间超时空", 62 | kSpySpy: "无间道", 63 | kAdjustGameSpeed: "任务调速" 64 | }, 65 | en: { 66 | kApply: "Apply", 67 | kFastBuild: "Fast build", 68 | kDeleteUnit: "Delete unit", 69 | kClearShroud: "Clear shroud", 70 | kGiveMeABomb: "Give me a bomb", 71 | kUnitLevelUp: "Selected units level up", 72 | kUnitSpeedUp: "Selected units speed up", 73 | kIAMWinner: "I am winner", 74 | kThisIsMine: "This is mine", 75 | kGod: "I am god", 76 | kInstBuild: "Instant build", 77 | kUnlimitSuperWeapon: "No super weapon cd", 78 | kInstFire: "No fire cd", 79 | kInstTurn: "Instant turn", 80 | kRangeToYourBase: "Range to your base", 81 | kFireToYourBase: "Fire to your base", 82 | kFreezeGapGenerator: "Disable gap generator", 83 | kSellTheWorld: "Sell the world", 84 | kBuildEveryWhere: "Build everywhere", 85 | kAutoRepair: "Auto repair", 86 | kSocialismMajesty: "Socialism majesty", 87 | kMakeCapturedMine: "Make captured mine", 88 | kMakeGarrisonedMine: "Make garrisoned mine", 89 | kInvadeMode: "Invade mode", 90 | kUnlimitTech: "Unlimit technology", 91 | kUnlimitFirePower: "Unlimit gun power", 92 | kInstChrono: "Instant chrono", 93 | kSpySpy: "Spy spy", 94 | kAdjustGameSpeed: "Enable game speed adjustment" 95 | } 96 | }; 97 | } 98 | 99 | setLanguage(lang) { 100 | if (lang === 'zh-CN') { 101 | this.lang = 'zh'; 102 | } else { 103 | this.lang = 'en'; 104 | } 105 | } 106 | 107 | getGuiStr(label) { 108 | return this.guiLabels[this.lang][label] || label; 109 | } 110 | 111 | getFnStr(label) { 112 | return this.fnLabels[this.lang][label] || label; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/backend/hook/hook.cc: -------------------------------------------------------------------------------- 1 | #include "base/windows_shit.h" 2 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 3 | // Enable yrpp .syhks segment marker. 4 | #define SYR_VER 2 5 | #include "Syringe.h" 6 | #include "backend/hook/game_loop.h" 7 | #include "backend/hook/logging.h" 8 | #include "backend/hook/memory_api.h" 9 | #include "base/logging.h" 10 | 11 | #ifdef YRTR_DEBUG 12 | // https://github.com/Phobos-developers/Phobos/blob/6391e7def58c8dc3168438087612cfaa8267c98d/src/Phobos.Ext.cpp#L306 13 | static bool DetachFromDebugger() { 14 | auto GetDebuggerProcessId = [](DWORD dwSelfProcessId) -> DWORD { 15 | DWORD dwParentProcessId = 0; 16 | HANDLE hSnapshot = CreateToolhelp32Snapshot(2, 0); 17 | PROCESSENTRY32 pe32; 18 | pe32.dwSize = sizeof(PROCESSENTRY32); 19 | Process32First(hSnapshot, &pe32); 20 | do { 21 | if (pe32.th32ProcessID == dwSelfProcessId) { 22 | dwParentProcessId = pe32.th32ParentProcessID; 23 | break; 24 | } 25 | } while (Process32Next(hSnapshot, &pe32)); 26 | CloseHandle(hSnapshot); 27 | return dwParentProcessId; 28 | }; 29 | 30 | HMODULE hModule = LoadLibrary("ntdll.dll"); 31 | if (hModule != NULL) { 32 | auto const NtRemoveProcessDebug = (NTSTATUS(__stdcall*)( 33 | HANDLE, HANDLE))GetProcAddress(hModule, "NtRemoveProcessDebug"); 34 | auto const NtSetInformationDebugObject = (NTSTATUS(__stdcall*)( 35 | HANDLE, ULONG, PVOID, ULONG, 36 | PULONG))GetProcAddress(hModule, "NtSetInformationDebugObject"); 37 | auto const NtQueryInformationProcess = (NTSTATUS(__stdcall*)( 38 | HANDLE, ULONG, PVOID, ULONG, 39 | PULONG))GetProcAddress(hModule, "NtQueryInformationProcess"); 40 | auto const NtClose = 41 | (NTSTATUS(__stdcall*)(HANDLE))GetProcAddress(hModule, "NtClose"); 42 | 43 | HANDLE hDebug; 44 | HANDLE hCurrentProcess = GetCurrentProcess(); 45 | NTSTATUS status = NtQueryInformationProcess(hCurrentProcess, 30, &hDebug, 46 | sizeof(HANDLE), 0); 47 | if (0 <= status) { 48 | ULONG killProcessOnExit = FALSE; 49 | status = NtSetInformationDebugObject(hDebug, 1, &killProcessOnExit, 50 | sizeof(ULONG), NULL); 51 | if (0 <= status) { 52 | const auto pid = GetDebuggerProcessId(GetProcessId(hCurrentProcess)); 53 | status = NtRemoveProcessDebug(hCurrentProcess, hDebug); 54 | if (0 <= status) { 55 | HANDLE hDbgProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); 56 | if (INVALID_HANDLE_VALUE != hDbgProcess) { 57 | BOOL ret = TerminateProcess(hDbgProcess, EXIT_SUCCESS); 58 | CloseHandle(hDbgProcess); 59 | return ret; 60 | } 61 | } 62 | } 63 | NtClose(hDebug); 64 | } 65 | FreeLibrary(hModule); 66 | } 67 | 68 | return false; 69 | } 70 | 71 | static void DetachAresDebugger() { 72 | // For debugging ares. 73 | if (IsDebuggerPresent()) { 74 | DetachFromDebugger(); 75 | LOG_F(INFO, "Detach debugger succeed={}", !IsDebuggerPresent()); 76 | // DEBUG 77 | // MessageBox(NULL, "Debugger detatched", "Notice", MB_OK); 78 | } 79 | } 80 | 81 | DEFINE_HOOK(0x007CD810, DetachDebugger, 9) { 82 | UNREFERENCED_PARAMETER(R); 83 | #ifdef YRTR_DEBUG 84 | DetachAresDebugger(); 85 | #endif 86 | return 0; 87 | } 88 | #endif 89 | 90 | void InstallHooks() { 91 | using namespace yrtr::backend::hook; 92 | static std::once_flag install_once; 93 | std::call_once(install_once, []() { 94 | MemoryAPI::Init(); 95 | // HookLogging(); 96 | HookUpdate(); 97 | HookExitGame(); 98 | }); 99 | } 100 | 101 | DEFINE_HOOK(0x006BB9D8, RA2WinMain, 5) { 102 | UNREFERENCED_PARAMETER(R); 103 | LOG_F(INFO, "WinMain"); 104 | InstallHooks(); 105 | #ifdef YRTR_DEBUG 106 | DetachAresDebugger(); 107 | #endif 108 | return 0; 109 | } 110 | -------------------------------------------------------------------------------- /src/frontend/desktop/config.cc: -------------------------------------------------------------------------------- 1 | #include "frontend/desktop/config.h" 2 | 3 | #include 4 | 5 | #include "base/logging.h" 6 | #include "formatter/std.h" 7 | #include "gsl/narrow" 8 | 9 | namespace yrtr { 10 | namespace frontend { 11 | 12 | namespace { 13 | std::string_view get_log_header() { return "Config "; } 14 | 15 | template 16 | std::optional TryLoad(const toml::table& tb, std::string_view key) { 17 | auto raw_val = tb.get_as(key); 18 | if (raw_val) { 19 | return raw_val->get(); 20 | } else { 21 | return std::nullopt; 22 | } 23 | } 24 | 25 | template 26 | T CheckLoad(const toml::table& tb, std::string_view key) { 27 | auto raw_val = tb.get_as(key); 28 | CHECK_NOTNULL(raw_val); 29 | return raw_val->get(); 30 | } 31 | } // namespace 32 | 33 | std::unique_ptr Config::inst_; 34 | 35 | bool Config::Load(const fs::path& cfg_dir) { 36 | fs::path cfg_path = cfg_dir / kCfgFileName; 37 | if (!fs::exists(cfg_path)) { 38 | HLOG_F(ERROR, "Not found config file={}", fs::absolute(cfg_path)); 39 | return false; 40 | } 41 | inst_ = std::make_unique(cfg_path); 42 | HLOG_F(INFO, "Load from path={}", fs::canonical(cfg_path)); 43 | auto cfg = toml::parse_file(cfg_path.string()); 44 | if (toml::table* global = cfg["ra2_trainer"].as_table()) { 45 | inst_->LoadGlobal(*global); 46 | } else { 47 | HLOG_F(ERROR, "Failed to load table [ra2_trainer]"); 48 | return false; 49 | } 50 | return true; 51 | } 52 | 53 | // Default configurations. 54 | Config::Config(const fs::path& cfg_path) 55 | : cfg_dir_(cfg_path.parent_path()), 56 | cfg_path_(cfg_path), 57 | lang_(kDefaultLang), 58 | enable_dpi_awareness_(false), 59 | font_path_(kDefaultFontPath), 60 | fontex_path_(kDefaultFontExPath) {} 61 | 62 | Lang Config::lang() const { 63 | if (lang_ == "zh") { 64 | return Lang::kZh; 65 | } else if (lang_ == "en") { 66 | return Lang::kEn; 67 | } else { 68 | UNREACHABLE(); 69 | } 70 | } 71 | 72 | void Config::DisableHotkeyGUI(int key) { 73 | disabled_hot_key_.emplace(key); 74 | } 75 | 76 | std::string Config::GetFnStrWithKey(FnLabel label) { 77 | int hot_key = Config::GetHotkey(label); 78 | if (disabled_hot_key_.contains(hot_key) || hot_key == GLFW_KEY_UNKNOWN) { 79 | return std::format("{}()", 80 | reinterpret_cast(GetFnStr(label, lang()))); 81 | } 82 | const char8_t* fn_str = GetFnStr(label, lang()); 83 | const char8_t* key_str = Config::KeyToString(hot_key); 84 | return std::format("{}({})", reinterpret_cast(fn_str), 85 | reinterpret_cast(key_str)); 86 | } 87 | 88 | fs::path Config::GetAbsolutePath(const fs::path& relpath) const { 89 | if (relpath.is_absolute()) { 90 | return relpath; 91 | } else { 92 | return cfg_dir_ / relpath; 93 | } 94 | } 95 | 96 | void Config::LoadGlobal(const toml::table& global) { 97 | lang_ = CheckLoad(global, "language"); 98 | enable_dpi_awareness_ = 99 | TryLoad(global, "enable_dpi_awareness").value_or(false); 100 | port_ = gsl::narrow_cast(CheckLoad(global, "port")); 101 | font_path_ = TryLoad(global, "font_path") 102 | .value_or(std::string(kDefaultFontPath)); 103 | fontex_path_ = CheckLoad(global, "fontex_path"); 104 | // Verify. 105 | CHECK_F(std::find(kAvailableLang.begin(), kAvailableLang.end(), lang_) != 106 | kAvailableLang.end(), 107 | "Unable to find lang={} in available={}", lang_, kAvailableLang); 108 | if (font_path_ != "") { 109 | CHECK_F(fs::exists(font_path_), "Failed to find font_path={}", font_path_); 110 | } 111 | CHECK_F(fs::exists(fontex_path_), "Failed to find fontex_path={}", 112 | fontex_path_); 113 | CHECK_F(!(font_path_ == "" && fontex_path_ == ""), "Not providing font"); 114 | } 115 | 116 | } // namespace frontend 117 | } // namespace yrtr 118 | -------------------------------------------------------------------------------- /src/frontend/web/client.js: -------------------------------------------------------------------------------- 1 | import { FnLabel } from "./protocol"; 2 | 3 | export class YRTRClient { 4 | #onStateUpdate; 5 | #socket; 6 | #getStateCount; 7 | #maxGetState; 8 | #connectionTimeout; // ms 9 | #reconnectInterval; // ms 10 | #getStateInterval; // ms 11 | #stopRequested; 12 | 13 | constructor(uri, onStateUpdate) { 14 | this.uri = uri; 15 | this.#onStateUpdate = onStateUpdate; 16 | this.#socket = null; 17 | this.#getStateCount = 0; 18 | this.#maxGetState = 1; 19 | this.#connectionTimeout = 50; // ms 20 | this.#reconnectInterval = 1000; // ms 21 | this.#getStateInterval = 300; // ms 22 | this.#stopRequested = false; 23 | } 24 | 25 | connect() { 26 | if (this.#stopRequested) return; 27 | 28 | this.#socket = new WebSocket(this.uri); 29 | 30 | this.#socket.onopen = () => { 31 | console.log('WebSocket connection established'); 32 | setInterval(() => { 33 | if (!this.#stopRequested) { 34 | this.#getState(); 35 | } 36 | }, this.#getStateInterval); 37 | }; 38 | 39 | this.#socket.onmessage = (event) => { 40 | this.#handleMessage(event.data); 41 | }; 42 | 43 | this.#socket.onclose = (event) => { 44 | console.log('WebSocket connection closed', event); 45 | if (!this.#stopRequested) { 46 | setTimeout(() => this.connect(), this.#reconnectInterval); 47 | } 48 | }; 49 | 50 | this.#socket.onerror = (error) => { 51 | console.error('WebSocket error:', error); 52 | }; 53 | } 54 | 55 | disconnect() { 56 | this.#stopRequested = true; 57 | if (this.#socket) { 58 | this.#socket.close(); 59 | } 60 | } 61 | 62 | sendInput(label, value) { 63 | const event = { 64 | type: 'input', 65 | label: label, 66 | val: value 67 | }; 68 | 69 | this.#sendData(event); 70 | } 71 | 72 | sendButton(label) { 73 | const event = { 74 | type: 'button', 75 | label: label, 76 | val: -1 77 | }; 78 | 79 | this.#sendData(event); 80 | } 81 | 82 | sendCheckbox(label, activate) { 83 | const event = { 84 | type: 'checkbox', 85 | label: label, 86 | val: activate 87 | }; 88 | 89 | this.#sendData(event); 90 | } 91 | 92 | sendProtectedList(sideMap) { 93 | const event = { 94 | type: 'protected_list', 95 | label: FnLabel.kInvalid, 96 | val: sideMap 97 | }; 98 | 99 | this.#sendData(event); 100 | } 101 | 102 | #handleMessage(data) { 103 | try { 104 | const message = JSON.parse(data); 105 | 106 | switch (message.type) { 107 | case 'get_state': 108 | this.#handleGetState(message); 109 | break; 110 | // Add other message types if needed 111 | default: 112 | console.warn('Unknown message type:', message.type); 113 | } 114 | } catch (error) { 115 | console.error('Error processing message:', error); 116 | } 117 | } 118 | 119 | #handleGetState(message) { 120 | if (message.type !== 'get_state' || 121 | message.label !== FnLabel.kInvalid || 122 | !message.val) { 123 | console.error('Invalid get_state message format'); 124 | return; 125 | } 126 | 127 | this.#getStateCount--; 128 | this.#onStateUpdate(message.val); 129 | } 130 | 131 | #getState() { 132 | if (this.#getStateCount >= this.#maxGetState) { 133 | console.log('Too many get_state requests pending'); 134 | return; 135 | } 136 | 137 | const event = { 138 | type: 'get_state', 139 | label: FnLabel.kInvalid, 140 | val: {} 141 | }; 142 | 143 | this.#sendData(event); 144 | this.#getStateCount++; 145 | } 146 | 147 | #sendData(data) { 148 | if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) { 149 | console.error('WebSocket is not connected, drop data'); 150 | return; 151 | } 152 | 153 | try { 154 | this.#socket.send(JSON.stringify(data)); 155 | } catch (error) { 156 | console.error('Error sending data:', error); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/backend/hook/game_loop.cc: -------------------------------------------------------------------------------- 1 | #include "backend/hook/game_loop.h" 2 | 3 | #include "base/windows_shit.h" 4 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 5 | #include "base/macro.h" 6 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 7 | #include "GameOptionsClass.h" 8 | #include "TacticalClass.h" 9 | #include "Unsorted.h" 10 | #include "WWMouseClass.h" 11 | __YRTR_END_THIRD_PARTY_HEADERS 12 | #include "backend/config.h" 13 | #include "backend/hook/hook_point.h" 14 | #include "backend/hook/memory_api.h" 15 | #include "backend/hook/trainer.h" 16 | #include "base/logging.h" 17 | #include "base/thread.h" 18 | #include "protocol/server.h" 19 | 20 | namespace yrtr { 21 | namespace backend { 22 | namespace hook { 23 | 24 | namespace { 25 | static std::once_flag init_once; 26 | static std::unique_ptr trainer; 27 | static std::unique_ptr server; 28 | 29 | static void ReclaimResource() { 30 | DLOG_F(INFO, "Reclaim resources"); 31 | server->Stop(); 32 | server.reset(); 33 | trainer.reset(); 34 | } 35 | 36 | // int __thiscall CreateWindow_777C30(HINSTANCE hInstance, int xRight, int 37 | // yBottom) 38 | static void Init(HINSTANCE hInstance) { 39 | // Setup thread id. 40 | SetupGameLoopThreadOnce(); 41 | // Load configurations. 42 | char exe_path[MAX_PATH] = {0}; 43 | DWORD nSize = GetModuleFileNameA(hInstance, exe_path, _countof(exe_path)); 44 | if (nSize == 0) { 45 | DWORD err = GetLastError(); 46 | std::string message = std::system_category().message(err); 47 | LOG_F(FATAL, "Failed to get module file name, err=[{}]{}", err, message); 48 | UNREACHABLE(); 49 | } 50 | // Search configuration file at the same directory with the dll. 51 | CHECK(Config::Load(fs::canonical(fs::path(exe_path)).parent_path())); 52 | trainer = std::make_unique(Config::instance()); 53 | server = std::make_unique(trainer.get(), Config::instance()->port()); 54 | } 55 | 56 | static void Update() { 57 | std::call_once(init_once, [&]() { 58 | // https://devblogs.microsoft.com/oldnewthing/20040614-00/?p=38903 59 | HINSTANCE hInstance = reinterpret_cast(kImageBase); 60 | Init(hInstance); 61 | }); 62 | DCHECK(IsWithinGameLoopThread()); 63 | static std::chrono::system_clock::time_point last_ts = 64 | std::chrono::system_clock::now(); 65 | auto ts_now = std::chrono::system_clock::now(); 66 | int64_t delta_us = 67 | std::chrono::duration_cast(ts_now - last_ts) 68 | .count(); 69 | last_ts = ts_now; 70 | double delta_sec = static_cast(delta_us) / 1e6; 71 | DCHECK_NOTNULL(trainer); 72 | trainer->Update(delta_sec); 73 | DCHECK_NOTNULL(server); 74 | server->Update(); 75 | } 76 | 77 | static DWORD WINAPI InjectTimeGetTime() { 78 | static uint32_t last_frame = 0; 79 | if (yrpp::Game::IsActive) { 80 | uint32_t current_frame = yrpp::Unsorted::CurrentFrame; 81 | if (current_frame > last_frame) { 82 | last_frame = current_frame; 83 | Update(); 84 | } 85 | } 86 | // Original code. 87 | return timeGetTime(); 88 | } 89 | 90 | static void ExitGame() { 91 | LOG_F(INFO, "Manually send destroy event"); 92 | ReclaimResourceOnce(); 93 | auto ra2WndProc = reinterpret_cast(0x007775C0); 94 | CHECK_NOTNULL(ra2WndProc); 95 | HWND hWnd = *(reinterpret_cast(0x00B73550)); 96 | CallWindowProc(ra2WndProc, hWnd, WM_DESTROY, NULL, NULL); 97 | } 98 | static void WINAPI InjectPostMessageA(HWND hWnd, UINT Msg, WPARAM wParam, 99 | LPARAM lParam) { 100 | if (Msg == WM_DESTROY) [[unlikely]] { 101 | ExitGame(); 102 | } 103 | PostMessageA(hWnd, Msg, wParam, lParam); 104 | } 105 | } // namespace 106 | 107 | void ReclaimResourceOnce() { 108 | static std::once_flag destroy_flag; 109 | std::call_once(destroy_flag, []() { ReclaimResource(); }); 110 | } 111 | 112 | void HookUpdate() { 113 | DLOG_F(INFO, "[YRTR-HOOK] {}", __func__); 114 | MemoryAPI::instance()->WriteMemory( 115 | kHpUpdate.first, reinterpret_cast(InjectTimeGetTime)); 116 | } 117 | 118 | void HookExitGame() { 119 | DLOG_F(INFO, "[YRTR-HOOK] {}", __func__); 120 | MemoryAPI::instance()->WriteMemory( 121 | kHpExitGame.first, reinterpret_cast(InjectPostMessageA)); 122 | } 123 | 124 | } // namespace hook 125 | } // namespace backend 126 | } // namespace yrtr 127 | -------------------------------------------------------------------------------- /src/backend/hook/trainer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "backend/config.h" 6 | #include "backend/hook/memory_api.h" 7 | #include "base/macro.h" 8 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 9 | #include "absl/container/inlined_vector.h" 10 | #include "absl/container/flat_hash_map.h" 11 | __YRTR_END_THIRD_PARTY_HEADERS 12 | #include "base/thread.h" 13 | #include "protocol/model.h" 14 | 15 | namespace yrpp { 16 | class AbstractClass; 17 | class BuildingTypeClass; 18 | class HouseClass; 19 | class ObjectClass; 20 | } // namespace yrpp 21 | 22 | namespace yrtr { 23 | namespace backend { 24 | namespace hook { 25 | // MVC -- controller. 26 | 27 | // Export for testing. 28 | void BeepEnable(); 29 | void BeepDisable(); 30 | void InitStates(State& state); 31 | 32 | // For testing. 33 | class ITrainer { 34 | public: 35 | virtual ~ITrainer() {} 36 | virtual State state() const = 0; 37 | virtual void set_on_state_updated(std::function cb) = 0; 38 | virtual void Update(double delta) = 0; 39 | virtual void OnInputEvent(FnLabel label, uint32_t val) = 0; 40 | virtual void OnButtonEvent(FnLabel label) = 0; 41 | virtual void OnCheckboxEvent(FnLabel label, bool activate) = 0; 42 | virtual void OnProtectedListEvent(SideMap&& side_map) = 0; 43 | }; 44 | 45 | class Trainer : public ITrainer { 46 | public: 47 | static bool is_active_disable_gagap() { return activate_disable_gagap_; } 48 | static bool ShouldProtect(yrpp::AbstractClass* obj); 49 | static bool ShouldProtect(yrpp::HouseClass* house); 50 | static bool ShouldEnableTech(yrpp::BuildingTypeClass* tech); 51 | 52 | Trainer(Config* cfg); 53 | Trainer(Trainer&&) = delete; 54 | Trainer& operator=(Trainer&&) = delete; 55 | ~Trainer(); 56 | 57 | State state() const final { 58 | DCHECK(IsWithinGameLoopThread()); 59 | return state_; 60 | } 61 | 62 | void set_on_state_updated(std::function cb) final { 63 | on_state_updated_ = std::move(cb); 64 | } 65 | 66 | void Update(double delta) final; 67 | void OnInputEvent(FnLabel label, uint32_t val) final; 68 | void OnButtonEvent(FnLabel label) final; 69 | void OnCheckboxEvent(FnLabel label, bool activate) final; 70 | void OnProtectedListEvent(SideMap&& side_map) final; 71 | 72 | private: 73 | // Update from state before use. 74 | static SideMap protected_houses_; 75 | static bool activate_disable_gagap_; 76 | 77 | Config* cfg_; 78 | State state_; 79 | std::function on_state_updated_; 80 | bool state_dirty_; 81 | 82 | std::unique_ptr mem_api_; 83 | 84 | bool activate_inst_building_; 85 | bool activate_inst_superweapon_; 86 | bool activate_inst_turn_turret_; 87 | bool activate_inst_turn_body_; 88 | // From VA:00450645, controls auto repair. 89 | absl::flat_hash_map iq_levels_; 90 | 91 | static void ForeachSelectingObject( 92 | std::function cb); 93 | static void ForeachProtectedHouse(std::function cb); 94 | 95 | void PropagateStateIfDirty(); 96 | void OnInputCredit(uint32_t val); 97 | void OnBtnFastBuild(); 98 | void OnBtnDeleteUnit(); 99 | void OnBtnClearShroud(); 100 | void OnBtnGiveMeABomb(); 101 | void OnBtnUnitLevelUp(); 102 | void OnBtnUnitSpeedUp(); 103 | void OnBtnIAMWinner(); 104 | void OnBtnThisIsMine(); 105 | 106 | void OnCkboxGod(bool activate); 107 | void OnCkboxInstBuild(bool activate); 108 | void OnCkboxUnlimitSuperWeapon(bool activate); 109 | void OnCkboxInstFire(bool activate); 110 | void OnCkboxInstTurn(bool activate); 111 | void OnCkboxRangeToYourBase(bool activate); 112 | void OnCkboxFireToYourBase(bool activate); 113 | void OnCkboxFreezeGapGenerator(bool activate); 114 | void OnCkboxSellTheWorld(bool activate); 115 | void OnCkboxBuildEveryWhere(bool activate); 116 | void OnCkboxAutoRepair(bool activate); 117 | void OnCkboxSocialismMajesty(bool activate); 118 | void OnCkboxMakeCapturedMine(bool activate); 119 | void OnCkboxMakeGarrisonedMine(bool activate); 120 | void OnCkboxInvadeMode(bool activate); 121 | void OnCkboxUnlimitTech(bool activate); 122 | void OnCkboxUnlimitFirePower(bool activate); 123 | void OnCkboxInstChrono(bool activate); 124 | void OnCkboxSpySpy(bool activate); 125 | void OnCkboxAdjustGameSpeed(bool activate); 126 | 127 | void UpdateCheckboxState(FnLabel label, bool activate); 128 | // Return the activate state before set enable. 129 | bool SetEnableCheckbox(FnLabel label, bool enable); 130 | void FinishBuilding() const; 131 | void FinishSuperweapon() const; 132 | void RotateUnit() const; 133 | bool IsGaming() const; 134 | bool WriteCredit(uint32_t credit) const; 135 | bool UnlimitRadar() const; 136 | 137 | DISALLOW_COPY_AND_ASSIGN(Trainer); 138 | }; 139 | 140 | } // namespace hook 141 | } // namespace backend 142 | } // namespace yrtr 143 | -------------------------------------------------------------------------------- /src/frontend/desktop/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | namespace fs = std::filesystem; 5 | #include 6 | #include 7 | #include 8 | 9 | #include "base/macro.h" 10 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 11 | #include "absl/container/flat_hash_set.h" 12 | __YRTR_END_THIRD_PARTY_HEADERS 13 | #include "frontend/desktop/char_table.h" 14 | #include "frontend/desktop/glfw.h" 15 | #include "toml++/toml.hpp" 16 | 17 | namespace yrtr { 18 | namespace frontend { 19 | 20 | class Config { 21 | public: 22 | static constexpr std::string_view kCfgFileName = "ra2_trainer.toml"; 23 | static constexpr int kWin32HotKeyMod = MOD_ALT; 24 | 25 | constexpr int GetHotkey(FnLabel label) { 26 | switch(label) { 27 | case FnLabel::kApply: return GLFW_KEY_1; 28 | case FnLabel::kIAMWinner: return GLFW_KEY_2; 29 | case FnLabel::kDeleteUnit: return GLFW_KEY_3; 30 | case FnLabel::kClearShroud: return GLFW_KEY_4; 31 | case FnLabel::kGiveMeABomb: return GLFW_KEY_5; 32 | case FnLabel::kUnitLevelUp: return GLFW_KEY_6; 33 | case FnLabel::kUnitSpeedUp: return GLFW_KEY_7; 34 | case FnLabel::kFastBuild: return GLFW_KEY_8; 35 | case FnLabel::kThisIsMine: return GLFW_KEY_9; 36 | case FnLabel::kGod: return GLFW_KEY_Q; 37 | case FnLabel::kInstBuild: return GLFW_KEY_W; 38 | case FnLabel::kUnlimitSuperWeapon: return GLFW_KEY_E; 39 | case FnLabel::kInstFire: return GLFW_KEY_T; 40 | case FnLabel::kInstTurn: return GLFW_KEY_Y; 41 | case FnLabel::kRangeToYourBase: return GLFW_KEY_U; 42 | case FnLabel::kFireToYourBase: return GLFW_KEY_I; 43 | case FnLabel::kFreezeGapGenerator: return GLFW_KEY_O; 44 | case FnLabel::kSellTheWorld: return GLFW_KEY_A; 45 | case FnLabel::kBuildEveryWhere: return GLFW_KEY_D; 46 | case FnLabel::kAutoRepair: return GLFW_KEY_F; 47 | case FnLabel::kSocialismMajesty: return GLFW_KEY_H; 48 | case FnLabel::kMakeGarrisonedMine: return GLFW_KEY_L; 49 | case FnLabel::kInvadeMode: return GLFW_KEY_Z; 50 | case FnLabel::kUnlimitTech: return GLFW_KEY_X; 51 | case FnLabel::kUnlimitFirePower: return GLFW_KEY_V; 52 | case FnLabel::kInstChrono: return GLFW_KEY_B; 53 | case FnLabel::kSpySpy: return GLFW_KEY_N; 54 | case FnLabel::kAdjustGameSpeed: return GLFW_KEY_PERIOD; /* . */ 55 | default: return GLFW_KEY_UNKNOWN; 56 | } 57 | } 58 | 59 | constexpr const char8_t* KeyToString(int key) { 60 | switch (key) { 61 | case GLFW_KEY_1: return u8"1"; 62 | case GLFW_KEY_2: return u8"2"; 63 | case GLFW_KEY_3: return u8"3"; 64 | case GLFW_KEY_4: return u8"4"; 65 | case GLFW_KEY_5: return u8"5"; 66 | case GLFW_KEY_6: return u8"6"; 67 | case GLFW_KEY_7: return u8"7"; 68 | case GLFW_KEY_8: return u8"8"; 69 | case GLFW_KEY_9: return u8"9"; 70 | case GLFW_KEY_0: return u8"0"; 71 | case GLFW_KEY_Q: return u8"Q"; 72 | case GLFW_KEY_W: return u8"W"; 73 | case GLFW_KEY_E: return u8"E"; 74 | case GLFW_KEY_R: return u8"R"; 75 | case GLFW_KEY_T: return u8"T"; 76 | case GLFW_KEY_Y: return u8"Y"; 77 | case GLFW_KEY_U: return u8"U"; 78 | case GLFW_KEY_I: return u8"I"; 79 | case GLFW_KEY_O: return u8"O"; 80 | case GLFW_KEY_P: return u8"P"; 81 | case GLFW_KEY_A: return u8"A"; 82 | case GLFW_KEY_S: return u8"S"; 83 | case GLFW_KEY_D: return u8"D"; 84 | case GLFW_KEY_F: return u8"F"; 85 | case GLFW_KEY_G: return u8"G"; 86 | case GLFW_KEY_H: return u8"H"; 87 | case GLFW_KEY_J: return u8"J"; 88 | case GLFW_KEY_K: return u8"K"; 89 | case GLFW_KEY_L: return u8"L"; 90 | case GLFW_KEY_Z: return u8"Z"; 91 | case GLFW_KEY_X: return u8"X"; 92 | case GLFW_KEY_C: return u8"C"; 93 | case GLFW_KEY_V: return u8"V"; 94 | case GLFW_KEY_B: return u8"B"; 95 | case GLFW_KEY_N: return u8"N"; 96 | case GLFW_KEY_M: return u8"M"; 97 | case GLFW_KEY_COMMA: return u8","; 98 | case GLFW_KEY_PERIOD: return u8"."; 99 | default: return u8"unknown"; 100 | } 101 | } 102 | 103 | static Config* instance() { return inst_.get(); } 104 | static bool Load(const fs::path& cfg_dir); 105 | 106 | Config(const fs::path& cfg_path); 107 | Lang lang() const; 108 | bool enable_dpi_awareness() const { return enable_dpi_awareness_; } 109 | 110 | uint16_t port() const { return port_; } 111 | const fs::path& font_path() const { return font_path_; } 112 | const fs::path& fontex_path() const { return fontex_path_; } 113 | 114 | void DisableHotkeyGUI(int key); 115 | std::string GetFnStrWithKey(FnLabel label); 116 | // Inputs a relative path, return absolute path relative to configuration file 117 | // directory. 118 | fs::path GetAbsolutePath(const fs::path& relpath) const; 119 | 120 | private: 121 | static std::unique_ptr inst_; 122 | static constexpr std::string_view kDefaultLang = "zh"; 123 | static constexpr std::string_view kDefaultFontPath = ""; 124 | static constexpr std::string_view kDefaultFontExPath = 125 | "C:\\Windows\\Fonts\\seguiemj.ttf"; 126 | static constexpr std::array kAvailableLang{"zh", "en"}; 127 | 128 | const fs::path cfg_dir_; 129 | const fs::path cfg_path_; 130 | std::string lang_; 131 | bool enable_dpi_awareness_; 132 | uint16_t port_; 133 | 134 | fs::path font_path_; 135 | fs::path fontex_path_; 136 | 137 | absl::flat_hash_set disabled_hot_key_; 138 | 139 | void LoadGlobal(const toml::table& global); 140 | 141 | DISALLOW_COPY_AND_ASSIGN(Config); 142 | }; 143 | 144 | } // namespace frontend 145 | } // namespace yrtr 146 | -------------------------------------------------------------------------------- /src/frontend/desktop/bin/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "base/windows_shit.h" 4 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 5 | #include "base/macro.h" 6 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 7 | #include "absl/debugging/symbolize.h" 8 | __YRTR_END_THIRD_PARTY_HEADERS 9 | #include "base/logging.h" 10 | #include "base/thread.h" 11 | #include "frontend/desktop/char_table.h" 12 | #include "frontend/desktop/config.h" 13 | #include "frontend/desktop/glfw.h" 14 | #include "frontend/desktop/gui.h" 15 | #include "frontend/desktop/gui_context.h" 16 | #include "frontend/desktop/timer.h" 17 | #include "protocol/client.h" 18 | #include "protocol/model.h" 19 | 20 | using namespace yrtr; 21 | using namespace yrtr::frontend; 22 | 23 | void SetViewport(int x, int y, int width, int height) { 24 | glViewport(x, y, width, height); 25 | } 26 | 27 | namespace { 28 | static void ErrorCallback(int error, const char* description) { 29 | LOG_F(FATAL, "error=[{}] {}", error, description); 30 | } 31 | 32 | static void FramebufferSizeCallback(GLFWwindow* window, int width, int height) { 33 | auto gui_ctx = 34 | reinterpret_cast(glfwGetWindowUserPointer(window)); 35 | SetViewport(0, 0, width, height); 36 | gui_ctx->UpdateViewport(width, height); 37 | } 38 | 39 | static std::unordered_map kHotkeyTable; 40 | 41 | static void InitHotkey(HWND hWnd) { 42 | Config* config = Config::instance(); 43 | DCHECK_NOTNULL(config); 44 | for (int label = 0; label < static_cast(FnLabel::kCount); label++) { 45 | int key = config->GetHotkey(static_cast(label)); 46 | if (key != GLFW_KEY_UNKNOWN) { 47 | int scancode = glfwGetKeyScancode(key); 48 | int vk = MapVirtualKey(scancode, MAPVK_VSC_TO_VK); 49 | CHECK(vk > 0); 50 | if (!RegisterHotKey(hWnd, label, Config::kWin32HotKeyMod | MOD_NOREPEAT, 51 | vk)) { 52 | config->DisableHotkeyGUI(key); 53 | char key_name[10]; 54 | if (GetKeyNameText(scancode << 16, key_name, 10) > 0) { 55 | LOG_F(WARNING, "RegisterHotkey failed. Key name={}", key_name); 56 | } else { 57 | LOG_F(WARNING, "RegisterHotkey failed. Key num={}", vk); 58 | } 59 | continue; 60 | } 61 | } 62 | kHotkeyTable.emplace(key, static_cast(label)); 63 | } 64 | } 65 | 66 | static FnLabel GetHotkeyLabel(int key) { 67 | if (kHotkeyTable.contains(key)) { 68 | return kHotkeyTable[key]; 69 | } else { 70 | return FnLabel::kInvalid; 71 | } 72 | } 73 | 74 | static WNDPROC glfw_wndproc = NULL; 75 | static LRESULT WINAPI WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, 76 | LPARAM lParam) { 77 | if (uMsg == WM_HOTKEY && 78 | (LOWORD(lParam) & Config::kWin32HotKeyMod) > 0) { 79 | // LOG(INFO, "msg={} key={}", uMsg, HIWORD(lParam)); 80 | Gui* gui = reinterpret_cast(GetProp(hWnd, "Gui")); 81 | CHECK(gui); 82 | gui->Trigger(GetHotkeyLabel(HIWORD(lParam))); 83 | return DefWindowProc(hWnd, uMsg, wParam, lParam); 84 | } 85 | return CallWindowProc(glfw_wndproc, hWnd, uMsg, wParam, lParam); 86 | } 87 | 88 | } // namespace 89 | 90 | int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, PSTR, int) { 91 | #ifdef YRTR_LOG_FILE 92 | if (atexit(DumpLog) != 0) { 93 | perror("Failed to register log dump function"); 94 | } 95 | #endif 96 | logging::InitLogging(logging::LogSink::kDbgView); 97 | SetupRendererThreadOnce(); 98 | 99 | char exe_path[MAX_PATH] = {0}; 100 | DWORD nSize = GetModuleFileNameA(hInstance, exe_path, _countof(exe_path)); 101 | if (nSize == 0) { 102 | DWORD err = GetLastError(); 103 | std::string message = std::system_category().message(err); 104 | LOG_F(FATAL, "Failed to get module file name, err=[{}]{}", err, message); 105 | UNREACHABLE(); 106 | } 107 | absl::InitializeSymbolizer(exe_path); 108 | 109 | GLFWwindow* window; 110 | glfwSetErrorCallback(ErrorCallback); 111 | if (!glfwInit()) { 112 | exit(EXIT_FAILURE); 113 | } 114 | 115 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 116 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); 117 | 118 | window = glfwCreateWindow(556, 900, "RA2 Trainer", NULL, NULL); 119 | if (!window) { 120 | glfwTerminate(); 121 | exit(EXIT_FAILURE); 122 | } 123 | // Load configurations. 124 | // Search configuration file at the same directory with the dll. 125 | CHECK(Config::Load(fs::canonical(fs::path(exe_path)).parent_path())); 126 | // GLFW not implements global hotkey listener, thus the win32 message hook 127 | // is necessary 128 | HWND hWnd = glfwGetWin32Window(window); 129 | InitHotkey(hWnd); 130 | glfw_wndproc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC); 131 | CHECK(glfw_wndproc) << GetLastError(); 132 | LRESULT res1 = SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)WndProc); 133 | CHECK(res1) << GetLastError(); 134 | 135 | glfwMakeContextCurrent(window); 136 | glfwSwapInterval(1); // Enable vsync 137 | 138 | ImGuiWindow gui_ctx(window); 139 | Gui gui(Config::instance()->lang()); 140 | { 141 | BOOL res = SetProp(hWnd, "GuiContext", static_cast(&gui_ctx)); 142 | CHECK(res) << GetLastError(); 143 | } 144 | { 145 | BOOL res = SetProp(hWnd, "Gui", &gui); 146 | CHECK(res) << GetLastError(); 147 | } 148 | Client client(gui, Config::instance()->port()); 149 | 150 | Timer::SetTimer(Client::kTimerIdUpdateState, 0.3 /*second*/, 151 | std::bind_front(&Client::GetState, &client)); 152 | 153 | glfwSetWindowUserPointer(window, &gui_ctx); 154 | glfwSetFramebufferSizeCallback(window, FramebufferSizeCallback); 155 | 156 | glEnable(GL_BLEND); 157 | glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); 158 | while (!glfwWindowShouldClose(window)) { 159 | glfwPollEvents(); 160 | Timer::Update(); 161 | client.Update(); 162 | 163 | glClearColor(0.0f, 0.0f, 0.0f, 0.0f); 164 | glClear(GL_COLOR_BUFFER_BIT); 165 | 166 | gui_ctx.BeginFrame(); 167 | gui.Render(); 168 | gui_ctx.EndFrame(); 169 | gui_ctx.Render(); 170 | 171 | glfwSwapBuffers(window); 172 | } 173 | // Stop before window destroied to catch bugs immediately instead of leaving a 174 | // zombie background process without visible window. 175 | client.Stop(); 176 | 177 | glfwDestroyWindow(window); 178 | glfwTerminate(); 179 | exit(EXIT_SUCCESS); 180 | } 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RA2YurisRevengeTrainer 2 | 3 | 红警2尤里的复仇内存修改器, 适用于 尤里的复仇 1.001 原版及 Ares 版本。 4 | 5 | ## 编译 6 | 7 | 使用 [CMake](https://cmake.org/), [Python3 >= 3.12](https://www.python.org/), [npm](https://www.npmjs.com/), [Ninja](https://ninja-build.org/) 和 [Visual Studio 17 2022](https://visualstudio.microsoft.com/) 构建。 8 | 9 | 必须在 x86 模式的 [Developer Command Prompt 或 Developer PowerShell](https://learn.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2022) 中执行编译命令。 10 | 11 |
12 | 13 | 我的编译环境 14 | 15 | ``` 16 | Windows PowerShell 17 | Copyright (C) Microsoft Corporation. All rights reserved. 18 | 19 | Try the new cross-platform PowerShell https://aka.ms/pscore6 20 | 21 | Could not start Developer PowerShell using the script path. 22 | Attempting to launch from the latest Visual Studio installation. 23 | ********************************************************************** 24 | ** Visual Studio 2022 Developer PowerShell v17.14.7 25 | ** Copyright (c) 2025 Microsoft Corporation 26 | ********************************************************************** 27 | PS C:\Projects\RA2YurisRevengeTrainer> python --version 28 | Python 3.12.5 29 | PS C:\Projects\RA2YurisRevengeTrainer> python -m pip show exccpkg 30 | Name: exccpkg 31 | Version: 3.0.3 32 | Summary: An explicit C++ package builder. 33 | Home-page: 34 | Author: AdjWang 35 | Author-email: wwang230513@gmail.com 36 | License: 37 | Location: C:\Programs\Python312\Lib\site-packages 38 | Requires: requests 39 | Required-by: 40 | PS C:\Projects\RA2YurisRevengeTrainer> npm --version 41 | 10.9.2 42 | PS C:\Projects\RA2YurisRevengeTrainer> ninja --version 43 | 1.13.0.git 44 | PS C:\Projects\RA2YurisRevengeTrainer> cl 45 | Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x86 46 | Copyright (C) Microsoft Corporation. All rights reserved. 47 | 48 | usage: cl [ option... ] filename... [ /link linkoption... ] 49 | PS C:\Projects\RA2YurisRevengeTrainer> cmake --version 50 | cmake version 3.30.2 51 | 52 | CMake suite maintained and supported by Kitware (kitware.com/cmake). 53 | PS C:\Projects\RA2YurisRevengeTrainer> 54 | ``` 55 | 56 |
57 | 58 | ### 安装依赖编译工具 59 | 60 | ``` 61 | python -m pip install exccpkg 62 | ``` 63 | 64 | ### 编译依赖 65 | 66 | ``` 67 | git clone https://github.com/AdjWang/RA2YurisRevengeTrainer.git 68 | cd ./RA2YurisRevengeTrainer 69 | python exccpkgfile.py 70 | ``` 71 | 72 | ### 编译前端 73 | 74 | ``` 75 | cd ./src/frontend/web 76 | npm install 77 | npm run build 78 | cd ../../.. 79 | python scripts/generate_web_main_page.py 80 | cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_INSTALL_PREFIX=deps/out/Release -G Ninja -S . -B ./build 81 | cmake --build ./build --config Release --target ra2_trainer -j $env:NUMBER_OF_PROCESSORS 82 | ``` 83 | 84 | > 在 `powershell` 中使用 `$env:NUMBER_OF_PROCESSORS`,在 `cmd` 中使用 `%NUMBER_OF_PROCESSORS%` 85 | 86 | 如果不想编译网页,只需要桌面程序: 87 | 88 | ``` 89 | cd . 90 | cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_INSTALL_PREFIX=deps/out/Release -DYRTR_ENABLE_WEB=OFF -G Ninja -S . -B ./build 91 | cmake --build ./build --config Release --target ra2_trainer -j $env:NUMBER_OF_PROCESSORS 92 | ``` 93 | 94 | ### 编译后端 95 | 96 | ``` 97 | cmake --build ./build --config Release --target wsock32 ra2_trainer_backend -j $env:NUMBER_OF_PROCESSORS 98 | ``` 99 | 100 | ## 使用方式 101 | 102 | dll 注入比远程线程安全,程序也更好写,但是代价是注入流程更加复杂。没有注入 Ares 需求的玩家可以选择继续使用[之前的 v4 版本](https://github.com/AdjWang/RA2YurisRevengeTrainer/releases/tag/v4.2)。 103 | 104 | ### 后端模块 105 | 106 | 能自动注入最好,不行就得手动注入。可在游戏启动后执行一些功能,如 `Alt+1` 看是否有提示音判断注入是否成功。 107 | 108 | #### 自动注入 109 | 110 | 原版:将 `wsock32.dll, ra2_trainer_backend.dll` 和 `ra2_trainer_backend.toml` 放到游戏目录。 111 | 112 | Ares:将 `ra2_trainer_backend.dll` 和 `ra2_trainer_backend.toml` 放到游戏目录。 113 | 114 | 对于某些 Ares 版本,如 `Tiberium Crisis 2`,`wsock32.dll` 会导致游戏启动时崩溃,所以不要误放 `wsock32.dll` 到 Ares 版本中。 115 | 116 | #### 手动注入 117 | 118 | 对于一些 Ares 版本,如 `Tiberium Crisis 2` 和复仇时刻,不会自动识别可注入模块,此时需要手动注入。使用任意 dll 注入工具注入 `ra2_trainer_backend.dll` 即可,注意仍需将 `ra2_trainer_backend.dll` 和 `ra2_trainer_backend.toml` 放在游戏目录下,如果已经放置 `wsock32.dll` 记得删除。 119 | 120 | 例如 [CheatEngine 自带的 dll 注入功能 (Inject DLL)](https://wiki.cheatengine.org/index.php?title=Help_File:Menus_and_Features)。 121 | 122 | ### 前端页面 123 | 124 | 桌面端和网页端可开启任意数量,自动同步。通常二选一即可。 125 | 126 | #### 桌面端 127 | 128 | 打开 `ra2_trainer.exe` 即可,目录下需要有 `ra2_trainer.toml`. 默认访问端口 `35271` 收发数据,不直接修改游戏,因此不需要管理员权限。 129 | 130 | #### 网页端 131 | 132 | 后端模块注入游戏后,会在默认端口 `35271` 开启监听服务供前端连接。此时将手机和电脑连接至同一局域网,手机访问 `http://<电脑 ip>:35271` 即可打开控制页面。 133 | 134 | > 可在 `powershell` 或 `cmd` 中使用 `ipconfig` 指令获取电脑端 ip 地址。 135 | > 136 | > 如果需要修改端口,需要保持 `ra2_trainer.toml` 和 `ra2_trainer_backend.toml` 中的配置一致。 137 | > 138 | > 如果电脑浏览器可以打开 `http://localhost:35271` 但是手机访问超时,需要关闭[电脑防火墙](https://support.microsoft.com/en-us/windows/firewall-and-network-protection-in-the-windows-security-app-ec0844f7-aebd-0583-67fe-601ecf5d774f)。 139 | 140 | ## 注意事项 141 | 142 | - 如果全局快捷键存在冲突,会注册失败,对应功能后的括号为空。 143 | 144 | - 项目使用 [`exccpkg`](https://github.com/AdjWang/exccpkg) 管理依赖,需要安装 `python >= 3.12, ninja`,如果不喜欢 `exccpkg`,需要自行拉取和编译依赖,依赖列表在 `exccpkgfile.py` 末尾。 145 | 146 | - 修改器后端会在游戏目录下产生 `ra2_trainer_backend.log` 日志,以供故障排查。 147 | 148 | ## 已知问题 149 | 150 | - 用于 Steam 尤里的复仇原版时游戏卡顿。需要将游戏目录下 `DDrawCompat.ini` 中的 `CpuAffinity = 1` 改为 `CpuAffinity = all`,修改后前端响应仍然略卡,但是后端看起来可以正常工作。其他版本可能也设置了单核绑定,但是不一定卡顿,如果卡顿,优先[检查 CPU 亲和性配置](https://poweradm.com/set-cpu-affinity-powershell/)。 151 | 152 | - `Tiberium Crisis 2` 新篇章第一关使用工程师占领无线电台时不能开启无敌,否则无法触发任务事件导致卡关。 153 | 154 | - `Tiberium Crisis 2` 任务结束退出时如果没有附加 Ares 调试器会无法保存当前游戏进度和勋章。仅在使用 `Debug` 模式编译时会遇到此问题。 155 | 156 | ## 界面说明 157 | 158 | ### 阵营过滤列表页面 159 | 160 | **与单位或阵营相关的功能仅对被保护阵营生效**。框选任意单位后可在修改器中添加被选单位所属阵营为被保护阵营,也允许保护 AI 玩家。如果忘记配置保护阵营,可能直接冲锋陷阵 => 炮灰 => Game Over. 161 | 162 | ### 功能页面 163 | 164 | 快捷键:Alt+功能括号后标记的按键 165 | 166 | 说明信息中涉及的功能用斜体表示。 167 | 168 | #### 输入框 169 | 170 | 1. 钱:顾名思义 171 | 172 | #### 按钮 173 | 174 | 1. 立即胜利:跳关用,解决偶尔游戏出 bug 卡关问题 175 | 2. 删除单位:让**选中的**单位直接消失 176 | 3. 地图全开:地图迷雾全开(如果要看透裂缝产生器还需要开启 *瘫痪裂缝产生器* 功能) 177 | 4. 核弹攻击:获得一枚核弹 178 | 5. 单位升级:**选中的**单位升3级(可以对群体使用) 179 | 6. 单位加速:**选中的**单位移动速度增加(可以对群体使用) 180 | 7. 快速建造:提高建造速度 181 | 8. 这是我的:**选中的**单位归到我方阵营 182 | 183 | #### 选项 184 | 185 | 1. 无敌:免疫任何伤害和超时空,但是不免疫工程师占领(免疫工程师占领的功能是 *全是我的-占领* ) 186 | 2. 瞬间建造:顾名思义。 187 | 3. 无限超武:无限施放超级武器和伞兵之类,但是对上面的一次性 *核弹攻击* 功能无效。如果有发射井,*核弹攻击* 也是无效的。 188 | 4. 极速攻击:攻击速度最大化。 189 | 5. 极速转身:战车和炮塔旋转速度最大化。(巨炮会变得很变态...) 190 | 6. 远程攻击:攻击距离最大化。(但是不警戒,不会自动远距离攻击) 191 | 7. 远程警戒:警戒距离最大化。( *远程攻击* 开启后使能。配合 *远程攻击* 使用,自动远距离攻击) 192 | 8. 瘫痪裂缝产生器:裂缝产生器无效化。 193 | 9. 卖卖卖:可以售卖地图上所有单位,比如敌方建筑,任意中立建筑,步兵,战车等。 194 | 10. 随意建筑:无视是否邻近、水面和陆地。 195 | 11. 自动修理:个人认为是最爽的功能。玩家占领的中立建筑也是可以修的~ 196 | 12. 社会主义万岁:企图控制玩家单位的尤里(包括建筑和车)会归属给玩家。玩家的尤里阵亡后控制的单位不会回到敌方阵营。 197 | 13. 全是我的-占领:任何"占领"事件的目标单位归属给玩家。 198 | 14. 全是我的-房屋驻军:任何"房屋驻军"事件的目标单位归属给玩家,但是房子里的部队不会改变归属。 199 | 15. 侵略模式:开启后可以自动攻击敌方建筑物。 200 | 16. 全科技:开启全科技,开启后需要随便造个东西才能生效。 201 | 17. 无限火力:单位重新装填速度最大化,且单位弹药容量扩容到`15`。 202 | 18. 瞬间超时空:超时空单位移动和攻击无冷却。配合 *极速攻击* 使用效果更佳。 203 | 19. 无间道:被敌方间谍入侵会获得敌方科技。 204 | 20. 任务调速:开启后执行任务时可以在暂停界面调整游戏速度。 205 | 206 | ## 感谢 207 | 208 | 感谢 [bigsinger](https://github.com/bigsinger/) 提供的建议和咨询。 209 | -------------------------------------------------------------------------------- /src/frontend/desktop/char_table.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "base/logging.h" 6 | #include "base/macro.h" 7 | #include "protocol/model.h" 8 | 9 | namespace yrtr { 10 | namespace frontend { 11 | 12 | enum class Lang { 13 | kZh, 14 | kEn, 15 | }; 16 | 17 | inline constexpr std::string_view StrLang(Lang lang) { 18 | switch (lang) { 19 | case Lang::kZh: return "zh"; 20 | case Lang::kEn: return "en"; 21 | default: return "unknown"; 22 | } 23 | } 24 | 25 | enum class GuiLabel { 26 | kTitle, 27 | kState, 28 | kStateOk, 29 | kStateIdle, 30 | kMoney, 31 | kAssist, 32 | kFilter, 33 | kSelectingHouseList, 34 | kProtectedHouseList, 35 | kAddAll, 36 | kClearAll, 37 | kCount, 38 | }; 39 | 40 | constexpr const char8_t* GetGuiStrZh(GuiLabel label) { 41 | switch (label) { 42 | case GuiLabel::kTitle: return u8"辅助工具"; 43 | case GuiLabel::kState: return u8"状态"; 44 | case GuiLabel::kStateOk: return u8"游戏运行中"; 45 | case GuiLabel::kStateIdle: return u8"未检测到游戏"; 46 | case GuiLabel::kMoney: return u8"钱"; 47 | case GuiLabel::kAssist: return u8"功能"; 48 | case GuiLabel::kFilter: return u8"过滤"; 49 | case GuiLabel::kSelectingHouseList: return u8"选中阵营"; 50 | case GuiLabel::kProtectedHouseList: return u8"保护阵营"; 51 | case GuiLabel::kAddAll: return u8"添加全部"; 52 | case GuiLabel::kClearAll: return u8"删除全部"; 53 | default: return u8"unknown"; 54 | } 55 | } 56 | 57 | constexpr const char8_t* GetGuiStrEn(GuiLabel label) { 58 | switch (label) { 59 | case GuiLabel::kTitle: return u8"Assist Tool"; 60 | case GuiLabel::kState: return u8"State"; 61 | case GuiLabel::kStateOk: return u8"Game running"; 62 | case GuiLabel::kStateIdle: return u8"Game not running"; 63 | case GuiLabel::kMoney: return u8"Money"; 64 | case GuiLabel::kAssist: return u8"Assist"; 65 | case GuiLabel::kFilter: return u8"Filter"; 66 | case GuiLabel::kSelectingHouseList: return u8"Selecting house"; 67 | case GuiLabel::kProtectedHouseList: return u8"Protected house"; 68 | case GuiLabel::kAddAll: return u8"Add all"; 69 | case GuiLabel::kClearAll: return u8"Clear all"; 70 | default: return u8"unknown"; 71 | } 72 | } 73 | 74 | inline const char8_t* GetGuiStr(GuiLabel label, Lang lang) { 75 | if (lang == Lang::kZh) { 76 | return GetGuiStrZh(label); 77 | } else if (lang == Lang::kEn) { 78 | return GetGuiStrEn(label); 79 | } else { 80 | UNREACHABLE(); 81 | } 82 | } 83 | 84 | constexpr const char8_t* GetFnStrZh(FnLabel label) { 85 | switch (label) { 86 | case FnLabel::kApply: return u8"修改"; 87 | case FnLabel::kIAMWinner: return u8"立即胜利"; 88 | case FnLabel::kDeleteUnit: return u8"删除单位"; 89 | case FnLabel::kClearShroud: return u8"地图全开"; 90 | case FnLabel::kGiveMeABomb: return u8"核弹攻击"; 91 | case FnLabel::kUnitLevelUp: return u8"单位升级"; 92 | case FnLabel::kUnitSpeedUp: return u8"单位加速"; 93 | case FnLabel::kFastBuild: return u8"快速建造"; 94 | case FnLabel::kThisIsMine: return u8"这是我的"; 95 | case FnLabel::kGod: return u8"无敌"; 96 | case FnLabel::kInstBuild: return u8"瞬间建造"; 97 | case FnLabel::kUnlimitSuperWeapon: return u8"无限超武"; 98 | case FnLabel::kInstFire: return u8"极速攻击"; 99 | case FnLabel::kInstTurn: return u8"极速转身"; 100 | case FnLabel::kRangeToYourBase: return u8"远程攻击"; 101 | case FnLabel::kFireToYourBase: return u8"远程警戒"; 102 | case FnLabel::kFreezeGapGenerator: return u8"瘫痪裂缝产生器"; 103 | case FnLabel::kSellTheWorld: return u8"卖卖卖"; 104 | case FnLabel::kBuildEveryWhere: return u8"随意建筑"; 105 | case FnLabel::kAutoRepair: return u8"自动修理"; 106 | case FnLabel::kSocialismMajesty: return u8"社会主义万岁"; 107 | case FnLabel::kMakeCapturedMine: return u8"全是我的-工程师占领"; 108 | case FnLabel::kMakeGarrisonedMine: return u8"全是我的-房屋驻军"; 109 | case FnLabel::kInvadeMode: return u8"侵略模式"; 110 | case FnLabel::kUnlimitTech: return u8"全科技"; 111 | case FnLabel::kUnlimitFirePower: return u8"大量弹药-重新建造生效"; 112 | case FnLabel::kInstChrono: return u8"瞬间超时空"; 113 | case FnLabel::kSpySpy: return u8"无间道"; 114 | case FnLabel::kAdjustGameSpeed: return u8"任务调速"; 115 | default: return u8"unknown"; 116 | } 117 | } 118 | 119 | constexpr const char8_t* GetFnStrEn(FnLabel label) { 120 | switch (label) { 121 | case FnLabel::kApply: return u8"Apply"; 122 | case FnLabel::kIAMWinner: return u8"I am winner"; 123 | case FnLabel::kDeleteUnit: return u8"Delete unit"; 124 | case FnLabel::kClearShroud: return u8"Clear shroud"; 125 | case FnLabel::kGiveMeABomb: return u8"Give me a bomb"; 126 | case FnLabel::kUnitLevelUp: return u8"Selected units level up"; 127 | case FnLabel::kUnitSpeedUp: return u8"Selected units speed up"; 128 | case FnLabel::kFastBuild: return u8"Fast build"; 129 | case FnLabel::kThisIsMine: return u8"This is mine"; 130 | case FnLabel::kGod: return u8"I am god"; 131 | case FnLabel::kInstBuild: return u8"Instant build"; 132 | case FnLabel::kUnlimitSuperWeapon: return u8"No super weapon cd"; 133 | case FnLabel::kInstFire: return u8"No fire cd"; 134 | case FnLabel::kInstTurn: return u8"Instant turn"; 135 | case FnLabel::kRangeToYourBase: return u8"Range to your base"; 136 | case FnLabel::kFireToYourBase: return u8"Fire to your base"; 137 | case FnLabel::kFreezeGapGenerator: return u8"Disable gap generator"; 138 | case FnLabel::kSellTheWorld: return u8"Sell the world"; 139 | case FnLabel::kBuildEveryWhere: return u8"Build everywhere"; 140 | case FnLabel::kAutoRepair: return u8"Auto repair"; 141 | case FnLabel::kSocialismMajesty: return u8"Socialism majesty"; 142 | case FnLabel::kMakeCapturedMine: return u8"Make captured mine"; 143 | case FnLabel::kMakeGarrisonedMine: return u8"Make garrisoned mine"; 144 | case FnLabel::kInvadeMode: return u8"Invade mode"; 145 | case FnLabel::kUnlimitTech: return u8"Unlimit technology"; 146 | case FnLabel::kUnlimitFirePower: return u8"Unlimit gun power"; 147 | case FnLabel::kInstChrono: return u8"Instant chrono"; 148 | case FnLabel::kSpySpy: return u8"Spy spy"; 149 | case FnLabel::kAdjustGameSpeed: return u8"Enable game speed adjustment"; 150 | default: return u8"unknown"; 151 | } 152 | } 153 | 154 | inline const char8_t* GetFnStr(FnLabel label, Lang lang) { 155 | if (lang == Lang::kZh) { 156 | return GetFnStrZh(label); 157 | } else if (lang == Lang::kEn) { 158 | return GetFnStrEn(label); 159 | } else { 160 | UNREACHABLE(); 161 | } 162 | } 163 | 164 | } // namespace frontend 165 | } // namespace yrtr 166 | -------------------------------------------------------------------------------- /src/protocol/server.cc: -------------------------------------------------------------------------------- 1 | #include "protocol/server.h" 2 | 3 | #include "base/logging.h" 4 | #include "base/thread.h" 5 | #include "nlohmann/json.hpp" 6 | using json = nlohmann::json; 7 | #ifdef YRTR_ENABLE_WEB 8 | #include "protocol/main_page.h" 9 | #endif 10 | 11 | namespace yrtr { 12 | 13 | namespace { 14 | static constexpr std::string_view kEmptyPageHtml = R"( 15 | 16 | 17 | 18 | YRTR Assist Tool 19 | 20 | 21 | Web frontend disabled. 22 | 23 | 24 | )"; 25 | } // namespace 26 | 27 | Server::Server(backend::hook::ITrainer* trainer, uint16_t port) 28 | : trainer_(trainer) { 29 | game_loop_ch_.SetThreadId(GetGameLoopThreadId()); 30 | trainer_->set_on_state_updated( 31 | std::bind_front(&Server::OnStateUpdated, this)); 32 | svr_.set_open_handshake_timeout(kConnTimeoutMilliseconds); 33 | svr_.set_close_handshake_timeout(kConnTimeoutMilliseconds); 34 | svr_.set_pong_timeout(kConnTimeoutMilliseconds * 4); 35 | svr_.set_access_channels(websocketpp::log::alevel::all); 36 | svr_.clear_access_channels(websocketpp::log::alevel::frame_payload); 37 | svr_.init_asio(); 38 | svr_.set_open_handler( 39 | [this](websocketpp::connection_hdl hdl) { OnOpenConn(hdl); }); 40 | svr_.set_close_handler( 41 | [this](websocketpp::connection_hdl hdl) { OnCloseConn(hdl); }); 42 | svr_.set_message_handler([this](websocketpp::connection_hdl hdl, 43 | WebsocketServer::message_ptr msg) { 44 | OnMessage(svr_, hdl, msg); 45 | }); 46 | svr_.set_http_handler([this](websocketpp::connection_hdl hdl){ 47 | OnHttpRequest(svr_, hdl); 48 | }); 49 | try { 50 | svr_.listen("0.0.0.0", std::to_string(port)); 51 | svr_.start_accept(); 52 | } catch (const std::exception& e) { 53 | LOG_F(ERROR, "Server start listen with err={}", e.what()); 54 | } 55 | if (svr_.is_listening()) { 56 | LOG_F(INFO, "Server start listen on port={}", port); 57 | evloop_ = std::thread([this]() { 58 | SetupNetThreadOnce(); 59 | try { 60 | svr_.run(); 61 | } catch (const std::exception& e) { 62 | LOG_F(ERROR, "Server exit with err={}", e.what()); 63 | } 64 | DLOG_F(INFO, "Server exit"); 65 | }); 66 | } 67 | } 68 | 69 | void Server::Stop() { 70 | try { 71 | svr_.stop(); 72 | } catch (const std::exception& e) { 73 | LOG_F(ERROR, "Server stop with err={}", e.what()); 74 | } 75 | // Check if event loop had started. 76 | if (evloop_.joinable()) { 77 | evloop_.join(); 78 | } 79 | } 80 | 81 | void Server::Update() { 82 | DCHECK(IsWithinGameLoopThread()); 83 | game_loop_ch_.ExecuteTasks(); 84 | } 85 | 86 | void Server::OnOpenConn(websocketpp::connection_hdl hdl) { 87 | DCHECK(IsWithinNetThread()); 88 | conns_.insert(hdl); 89 | } 90 | 91 | void Server::OnCloseConn(websocketpp::connection_hdl hdl) { 92 | DCHECK(IsWithinNetThread()); 93 | conns_.erase(hdl); 94 | } 95 | 96 | void Server::OnMessage(WebsocketServer& /*svr*/, 97 | websocketpp::connection_hdl hdl, 98 | WebsocketServer::message_ptr msg) { 99 | DCHECK(IsWithinNetThread()); 100 | // Handle event message. 101 | std::string payload = msg->get_payload(); 102 | try { 103 | json raw_data = json::parse(payload); 104 | std::string type = raw_data.at("type"); 105 | if (type == "get_state") { 106 | OnGetStateEvent(hdl); 107 | } else if (type == "input") { 108 | OnPostInputEvent(ParseInputEvent(raw_data)); 109 | } else if (type == "button") { 110 | OnPostButtonEvent(ParseButtonEvent(raw_data)); 111 | } else if (type == "checkbox") { 112 | OnPostCheckboxEvent(ParseCheckboxEvent(raw_data)); 113 | } else if (type == "protected_list") { 114 | OnPostProtectedListEvent(ParseProtectedListEvent(raw_data)); 115 | } else { 116 | LOG_F(ERROR, "Unknown event type={} from data={}", type, payload); 117 | } 118 | } catch (const std::exception& e) { 119 | LOG_F(ERROR, "Failed to parse json data={} error={}", payload, e.what()); 120 | } 121 | } 122 | 123 | void Server::OnHttpRequest(WebsocketServer& /*svr*/, 124 | websocketpp::connection_hdl hdl) { 125 | websocketpp::lib::error_code ec; 126 | WebsocketServer::connection_ptr conn = svr_.get_con_from_hdl(hdl, ec); 127 | if (ec) { 128 | LOG_F(ERROR, "Failed to send state data with error=[{}]{}", ec.value(), 129 | ec.message()); 130 | return; 131 | } 132 | #ifdef YRTR_ENABLE_WEB 133 | conn->set_body(std::string(kMainPageHtml)); 134 | #else 135 | conn->set_body(std::string(kEmptyPageHtml)); 136 | #endif 137 | conn->set_status(websocketpp::http::status_code::ok); 138 | } 139 | 140 | void Server::OnStateUpdated(State state) { 141 | DCHECK(IsWithinGameLoopThread()); 142 | boost::asio::dispatch(svr_.get_io_service(), [this, state = state]() { 143 | DCHECK(IsWithinNetThread()); 144 | // Broadcast state to clients. 145 | for (auto conn : conns_) { 146 | State state_copy = state; 147 | SendState(std::move(state_copy), conn); 148 | } 149 | }); 150 | } 151 | 152 | void Server::OnGetStateEvent(websocketpp::connection_hdl hdl) { 153 | DCHECK(IsWithinNetThread()); 154 | game_loop_ch_.ExecuteOrScheduleTask([this, hdl = hdl]() { 155 | DCHECK(IsWithinGameLoopThread()); 156 | State state = trainer_->state(); 157 | boost::asio::dispatch( 158 | svr_.get_io_service(), 159 | [this, hdl = hdl, state = std::move(state)]() mutable { 160 | DCHECK(IsWithinNetThread()); 161 | SendState(std::move(state), hdl); 162 | }); 163 | }); 164 | } 165 | 166 | void Server::SendState(State&& state, websocketpp::connection_hdl hdl) { 167 | DCHECK(IsWithinNetThread()); 168 | if (hdl.expired()) [[unlikely]] { 169 | conns_.erase(hdl); 170 | return; 171 | } 172 | std::string data = MakeGetStateEvent(std::move(state)); 173 | websocketpp::lib::error_code ec; 174 | svr_.send(hdl, data, websocketpp::frame::opcode::value::text, ec); 175 | if (ec) { 176 | LOG_F(ERROR, "Failed to send state data with error=[{}]{}", ec.value(), 177 | ec.message()); 178 | } 179 | } 180 | 181 | void Server::OnPostInputEvent(Event&& event) { 182 | DCHECK(IsWithinNetThread()); 183 | game_loop_ch_.ExecuteOrScheduleTask([this, event = std::move(event)]() { 184 | trainer_->OnInputEvent(event.label, event.val); 185 | }); 186 | } 187 | 188 | void Server::OnPostButtonEvent(Event&& event) { 189 | DCHECK(IsWithinNetThread()); 190 | game_loop_ch_.ExecuteOrScheduleTask([this, event = std::move(event)]() { 191 | trainer_->OnButtonEvent(event.label); 192 | }); 193 | } 194 | 195 | void Server::OnPostCheckboxEvent(Event&& event) { 196 | DCHECK(IsWithinNetThread()); 197 | game_loop_ch_.ExecuteOrScheduleTask([this, event = std::move(event)]() { 198 | trainer_->OnCheckboxEvent(event.label, event.val); 199 | }); 200 | } 201 | 202 | void Server::OnPostProtectedListEvent(Event&& event) { 203 | DCHECK(IsWithinNetThread()); 204 | game_loop_ch_.ExecuteOrScheduleTask( 205 | [this, event = std::move(event)]() mutable { 206 | trainer_->OnProtectedListEvent(std::move(event.val)); 207 | }); 208 | } 209 | 210 | } // namespace yrtr 211 | -------------------------------------------------------------------------------- /scripts/tech.txt: -------------------------------------------------------------------------------- 1 | Aircraft [1]-ORCA-Intruder-入侵者战机 2 | Aircraft [2]-HORNET-Hornet-大黄蜂 3 | Aircraft [7]-BEAG-Black Eagle-黑鹰战机 4 | Building [0]-GAPOWR-Allied Power Plant-盟军发电厂 5 | Building [1]-GAREFN-Allied Ore Refinery-盟军矿厂 6 | Building [2]-GACNST-Allied Construction Yard-盟军建造场 7 | Building [3]-GAPILE-Allied Barracks-盟军兵营 8 | Building [4]-GASAND-Sandbags-沙墙 9 | Building [5]-GADEPT-Allied Service Depot-盟军维修厂 10 | Building [6]-GATECH-Allied Battle Lab-盟军作战实验室 11 | Building [7]-GAWEAP-Allied War Factory-盟军战车工厂 12 | Building [8]-CALAB-Einstein's Lab-爱因斯坦实验室 13 | Building [9]-NAPOWR-Soviet Tesla Reactor-磁能反应炉 14 | Building [10]-NATECH-Soviet Battle Lab-苏军作战实验室 15 | Building [11]-NAHAND-Soviet Barracks-苏军兵营 16 | Building [12]-GAWALL-Allied Wall-盟军围墙 17 | Building [13]-NARADR-Soviet Radar Tower-苏军雷达 18 | Building [14]-NAWEAP-Soviet War Factory-苏军战车工厂 19 | Building [15]-NAREFN-Soviet Ore Refinery-苏军矿厂 20 | Building [16]-NAWALL-Soviet Wall-苏军围墙 21 | Building [18]-NAPSIS-Psychic Sensor-心灵感应器 22 | Building [20]-NALASR-Soviet Sentry Gun-哨戒炮 23 | Building [21]-NASAM-Allied Patriot Missile-爱国者飞弹 24 | Building [22]-CASYDN02-Sydney Kangaroo Burger-悉尼袋鼠堡 25 | Building [23]-GAYARD-Allied Shipyard-盟军船厂 26 | Building [24]-NAIRON-Soviet Iron Curtain Device-铁幕装置 27 | Building [25]-NACNST-Soviet Construction Yard-苏军建造场 28 | Building [26]-NADEPT-Soviet Service Depot-苏军维修厂 29 | Building [27]-GACSPH-Allied Chrono Sphere-超时空传送仪 30 | Building [29]-GAWEAT-Allied Weather Controller-天气控制器 31 | Building [30]-CABHUT-Bridge repair hut-桥梁维修小屋 32 | Building [47]-CAHOSP-Old Civilian Hospital-市民医院 33 | Building [53]-TESLA-Soviet Tesla Coil-磁暴线圈 34 | Building [54]-NAMISL-Soviet Nuclear Missile Silo-核弹发射井 35 | Building [55]-ATESLA-Allied Prism Cannon-光棱塔 36 | Building [56]-CAMACH-Tech Machine Shop-科技机器商店 37 | Building [58]-CASYDN03-Sydney Opera House-悉尼歌剧院 38 | Building [59]-AMMOCRAT-Ammo Crates-弹药箱 39 | Building [61]-NAYARD-Soviet Shipyard-苏军造船厂 40 | Building [62]-GASPYSAT-Allied SpySat Uplink-间谍卫星 41 | Building [63]-GAGAP-Allied Gap Generator-裂缝产生器 42 | Building [64]-GTGCAN-Allied Grand Cannon-巨炮 43 | Building [65]-NANRCT-Soviet Nuclear Reactor-核子反应堆 44 | Building [66]-GAPILL-Allied Pill Box-机枪碉堡 45 | Building [67]-NAFLAK-Soviet Flak Cannon-防空炮 46 | Building [68]-CAOUTP-Tech Outpost-科技前哨站 47 | Building [69]-CATHOSP-Tech Hospital-科技医院 48 | Building [70]-CAAIRP-Tech Airport-科技机场 49 | Building [71]-CAOILD-Tech Oil Derrick-科技钻油厂 50 | Building [72]-NACLON-Cloning Vats-复制中心 51 | Building [73]-GAOREP-Allied Ore Processor-矿石精炼器 52 | Building [79]-CANEWY04-Statue of Liberty-自由女神像 53 | Building [80]-CANEWY05-ZZZ World Trade Center-世贸中心 54 | Building [82]-CATECH01-Communications-通讯中心 55 | Building [85]-CAWASH01-White House-白宫 56 | Building [92]-CAMISC01-Barrels-油桶1 57 | Building [93]-CAMISC02-Barrel-油桶2 58 | Building [104]-CAPARS01-Paris Tower-埃菲尔铁塔 59 | Building [105]-GAAIRC-Allied Airforce Command Headquarters-盟军空指部 60 | Building [107]-CAFRMB-Farm Outhouse-移动式厕所 61 | Building [135]-AMRADR-Allied American Airforce Command Headquarters-美国空指部 62 | Building [137]-CAGARD01-Guard Shack-警卫哨 63 | Building [143]-MAYAN-Mayan Prism Pyramid-玛雅金字塔 64 | Building [147]-CAMEX01-Mayan Prism Pyramid-玛雅金字塔 65 | Building [160]-CARUS03-Russain Kremlin Palace-克里姆林宫 66 | Building [179]-CAPARS11-Paris Arc de Triumphe-巴黎凯旋门 67 | Building [181]-CAFARM06-Lighthouse-灯塔 68 | Building [183]-NAPSYB-Psychic Beacon-心灵信标 69 | Building [184]-NAPSYA-Psychic Amplifier-心灵增幅器 70 | Building [186]-CACOLO01-Air Force Academy Chapel Colorado-空军学院礼拜堂 71 | Building [289]-CAMSC13-Derelict Tank-废弃猛犸坦克 72 | Building [313]-CASLAB-Tech Secret Lab-秘密科技实验室 73 | Building [320]-CATRAN03-TRANS FORTRESS-尤里要塞 74 | Building [369]-CAPOWR-Tech Civilian Power Plant-科技电厂 75 | Building [398]-CABUNK03-Concrete Bunker 03-科技混凝土碉堡 76 | Building [399]-CABUNK04-Concrete Bunker 04-科技混凝土碉堡 77 | Building [402]-YACNST-YACNST-尤里建造场 78 | Building [403]-YAPOWR-YAPOWR-生化反应室 79 | Building [404]-YAREFN-YAREFN-奴隶矿场 80 | Building [405]-YABRCK-YABRCK-尤里兵营 81 | Building [406]-YATECH-YATECH-尤里作战实验室 82 | Building [407]-YAWEAP-YAWEAP-尤里战车工厂 83 | Building [408]-NABNKR-NABNKR-战斗碉堡 84 | Building [409]-YAGGUN-YAGGUN-盖特机炮 85 | Building [410]-YAPSYT-YAPSYT-心灵控制塔 86 | Building [411]-NATBNK-NATBNK-坦克碉堡 87 | Building [412]-GAFWLL-GAFWLL-尤里围墙 88 | Building [413]-YAYARD-YAYARD-尤里船坞 89 | Infantry [0]-E1-GI-美国大兵 90 | Infantry [1]-E2-Conscript-动员兵 91 | Infantry [2]-SHK-Shock Trooper-磁暴步兵 92 | Infantry [3]-ENGINEER-Engineer-工程师 93 | Infantry [4]-JUMPJET-Rocketeer-火箭飞行兵 94 | Infantry [5]-GHOST-SEAL-海豹部队 95 | Infantry [6]-YURI-Yuri-尤里 96 | Infantry [7]-IVAN-Crazy Ivan-疯狂伊文 97 | Infantry [8]-DESO-Desolater-辐射工兵 98 | Infantry [9]-DOG-Attack Dog-苏联警犬 99 | Infantry [13]-CTECH-Technician-技师 100 | Infantry [15]-CLEG-Chrono Legionnaire-超时空军团兵 101 | Infantry [16]-SPY-Spy-间谍 102 | Infantry [17]-CCOMAND-Chrono Commando-超时空突击队 103 | Infantry [18]-PTROOP-Psi-Corp Trooper-尤里复制人 104 | Infantry [19]-CIVAN-Chrono Ivan-超时空伊文 105 | Infantry [20]-YURIPR-Yuri Prime-尤里改 106 | Infantry [21]-SNIPE-Sniper-狙击手 107 | Infantry [22]-COW-Animal Cow-奶牛 108 | Infantry [23]-ALL-Animal Alligator-鳄鱼 109 | Infantry [24]-TANY-Tanya-谭雅 110 | Infantry [25]-FLAKT-Flak Trooper-防空步兵 111 | Infantry [26]-TERROR-Terrorist-恐怖分子 112 | Infantry [27]-SENGINEER-Soviet Engineer-苏军工程师 113 | Infantry [28]-ADOG-Allied Attack Dog-盟军警犬 114 | Infantry [29]-VLADIMIR-Vladimir-弗拉基米尔 115 | Infantry [30]-PENTGEN-General Pentagon-五角大楼将军 116 | Infantry [31]-PRES-President-总统 117 | Infantry [32]-SSRV-Secret Service-秘密保镖 118 | Infantry [43]-POLARB-Animal Polar Bear-北极熊 119 | Infantry [44]-JOSH-Animal Monkey-猴子 120 | Infantry [51]-CLNT-Cowboy-Westwood之星 121 | Infantry [52]-ARND-Hero-终结者 122 | Infantry [53]-STLN-Bodybuilder-兰博 123 | Infantry [54]-CAML-Animal Camel-骆驼 124 | Infantry [55]-EINS-Albert Einstein-爱因斯坦 125 | Infantry [56]-MUMY-Evil Mummy-木乃伊 126 | Infantry [57]-RMNV-Romanov-罗曼诺夫 127 | Infantry [58]-LUNR-Lunar Infantry-登月飞行兵 128 | Infantry [59]-DNOA-Animal T-Rex-暴龙 129 | Infantry [60]-DNOB-Animal Bront-恐龙 130 | Infantry [62]-WWLF-Werewolf-狼人 131 | Infantry [65]-BRUTE-BRUTE-狂兽人 132 | Unit [0]-AMCV-Allied Construction Vehicle-盟军移动基地车 133 | Unit [1]-HARV-War Miner-武装采矿车 134 | Unit [2]-APOC-Apocalypse-灾厄坦克 135 | Unit [3]-HTNK-Rhino Heavy Tank-犀牛坦克 136 | Unit [4]-SAPC-Armored Transport-野牛运输艇 137 | Unit [9]-MTNK-Grizzly Battle Tank-灰熊坦克 138 | Unit [10]-HORV-War Miner-武装采矿车 139 | Unit [13]-CARRIER-Aircraft Carrier-航空母舰 140 | Unit [14]-V3-V3 Launcher-V3导弹车 141 | Unit [15]-ZEP-Kirov Airship-基洛夫空艇 142 | Unit [16]-DRON-Terror Drone-恐怖机器人 143 | Unit [17]-HTK-Flak Track-防空履带车 144 | Unit [18]-DEST-Destroyer-驱逐舰 145 | Unit [19]-SUB-Typhoon Attack Sub-台风攻击潜艇 146 | Unit [20]-AEGIS-Aegis Cruiser-神盾巡洋舰 147 | Unit [21]-LCRF-Landing Craft-旅行者运输艇 148 | Unit [22]-DRED-Dreadnought-无畏级导弹舰 149 | Unit [23]-SHAD-BlackHawk Transport-黑鹰直升机 150 | Unit [24]-SQD-Giant Squid-巨型乌贼 151 | Unit [25]-DLPH-Dolphin-海豚 152 | Unit [26]-SMCV-Soviet Construction Vehicle-苏军移动基地车 153 | Unit [27]-TNKD-Tank Destroyer-坦克杀手 154 | Unit [28]-HOWI-Howitzer-西风火炮 155 | Unit [29]-TTNK-Tesla Tank-磁暴坦克 156 | Unit [30]-HIND-Hind Transport-飞碟 157 | Unit [31]-LTNK-Light Tank-轻坦克 158 | Unit [32]-CMON-Chrono Miner(noback)-超时空采矿车 159 | Unit [33]-CMIN-Chrono Miner-超时空采矿车 160 | Unit [34]-SREF-Prism Tank-光棱坦克 161 | Unit [36]-HYD-Sea Scorpion-海蝎 162 | Unit [37]-MGTK-Mirage Tank-幻影坦克 163 | Unit [38]-FV-IFV-多功能步兵车 164 | Unit [40]-VLAD-Vladimir's Dreadnought-弗拉基米尔指挥舰 165 | Unit [41]-DTRUCK-Demolitions Truck-自爆卡车 166 | Unit [52]-CRUISE-Cruise Ship-游轮 167 | Unit [73]-YDUM-YDUM 168 | Unit [83]-PCV-PCV-尤里移动基地车 169 | -------------------------------------------------------------------------------- /src/backend/hook/memory_api.cc: -------------------------------------------------------------------------------- 1 | #include "backend/hook/memory_api.h" 2 | 3 | #include "base/windows_shit.h" 4 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 5 | #include "base/logging.h" 6 | 7 | namespace yrtr { 8 | namespace backend { 9 | namespace hook { 10 | 11 | class HandleGuard { 12 | public: 13 | HandleGuard(HANDLE handle) : handle_(handle) {} 14 | ~HandleGuard() { CloseHandle(handle_); } 15 | HandleGuard(HandleGuard&&) = delete; 16 | HandleGuard& operator=(HandleGuard&&) = delete; 17 | 18 | HANDLE handle() const { return handle_; } 19 | 20 | private: 21 | HANDLE handle_; 22 | 23 | DISALLOW_COPY_AND_ASSIGN(HandleGuard); 24 | }; 25 | 26 | class AllocGuard { 27 | public: 28 | AllocGuard(HANDLE handle, LPVOID mem) : handle_(handle), mem_(mem) {} 29 | AllocGuard(AllocGuard&&) = delete; 30 | AllocGuard& operator=(AllocGuard&&) = delete; 31 | 32 | LPVOID data() const { return mem_; } 33 | 34 | ~AllocGuard() { 35 | if (!VirtualFreeEx(handle_, mem_, 0, MEM_RELEASE)) { 36 | PLOG(WARNING) << "VirtualFreeEx failed"; 37 | } 38 | } 39 | 40 | private: 41 | HANDLE handle_; 42 | LPVOID mem_; 43 | 44 | DISALLOW_COPY_AND_ASSIGN(AllocGuard); 45 | }; 46 | 47 | std::unique_ptr MemoryAPI::inst_; 48 | 49 | void MemoryAPI::Init() { 50 | inst_ = std::make_unique(); 51 | } 52 | 53 | void MemoryAPI::Destroy() { 54 | inst_.reset(); 55 | } 56 | 57 | MemoryAPI::MemoryAPI() { 58 | HANDLE hProcess = GetCurrentProcess(); 59 | PCHECK(hProcess != NULL); 60 | handle_ = std::make_unique(hProcess); 61 | } 62 | 63 | MemoryAPI::~MemoryAPI() { 64 | RestoreAllHooks(); 65 | } 66 | 67 | bool MemoryAPI::CheckHandle() const { return handle_ != nullptr; } 68 | 69 | #define CHECK_HANDLE_OR_RETURN_FALSE() \ 70 | if (!CheckHandle()) { \ 71 | LOG_F(WARNING, "Invalid handle"); \ 72 | return false; \ 73 | } 74 | 75 | bool MemoryAPI::ReadMemory(uint32_t address, std::span data) const { 76 | CHECK_HANDLE_OR_RETURN_FALSE(); 77 | DWORD oldprotect; 78 | if (!VirtualProtectEx(handle_->handle(), (void*)address, data.size(), 79 | PAGE_EXECUTE_READWRITE, &oldprotect)) { 80 | PLOG_F(WARNING, "VirtualProtectEx failed on addr={}", (void*)address); 81 | return false; 82 | } 83 | if (!ReadProcessMemory(handle_->handle(), (void*)address, data.data(), 84 | data.size(), nullptr)) { 85 | PLOG_F(WARNING, "ReadProcessMemory failed on addr={}", (void*)address); 86 | return false; 87 | } 88 | if (!VirtualProtectEx(handle_->handle(), (void*)address, data.size(), 89 | oldprotect, &oldprotect)) { 90 | PLOG_F(WARNING, "VirtualProtectEx failed on addr={}", (void*)address); 91 | return false; 92 | } 93 | return true; 94 | } 95 | 96 | bool MemoryAPI::ReadAddress(uint32_t address, uint32_t* data) const { 97 | CHECK_HANDLE_OR_RETURN_FALSE(); 98 | CHECK(data != nullptr); 99 | return ReadMemory( 100 | address, 101 | std::span(reinterpret_cast(data), sizeof(uint32_t*))); 102 | } 103 | 104 | bool MemoryAPI::WriteMemory(uint32_t address, 105 | std::span data) const { 106 | CHECK_HANDLE_OR_RETURN_FALSE(); 107 | DWORD oldprotect; 108 | if (!VirtualProtectEx(handle_->handle(), (void*)address, data.size(), 109 | PAGE_EXECUTE_READWRITE, &oldprotect)) { 110 | PLOG_F(WARNING, "VirtualProtectEx failed on addr={}", (void*)address); 111 | return false; 112 | } 113 | if (!WriteProcessMemory(handle_->handle(), (void*)address, data.data(), 114 | data.size(), nullptr)) { 115 | PLOG_F(WARNING, "WriteProcessMemory failed on addr={}", (void*)address); 116 | return false; 117 | } 118 | if (!VirtualProtectEx(handle_->handle(), (void*)address, data.size(), 119 | oldprotect, &oldprotect)) { 120 | PLOG_F(WARNING, "VirtualProtectEx failed on addr={}", (void*)address); 121 | return false; 122 | } 123 | return true; 124 | } 125 | 126 | bool MemoryAPI::HasHook(const HookPoint hook_point) const { 127 | return hooks_.contains(hook_point.first); 128 | } 129 | 130 | bool MemoryAPI::HookJump(const HookPoint hook_point, void* dest) { 131 | CHECK_HANDLE_OR_RETURN_FALSE(); 132 | uint32_t addr = hook_point.first; 133 | uint32_t asm_len = hook_point.second; 134 | if (hooks_.contains(addr)) { 135 | return true; 136 | } 137 | // jmp 138 | if (asm_len < 5) { 139 | asm_len = 5; 140 | } 141 | absl::InlinedVector asm_code(asm_len); 142 | asm_code[0] = static_cast(0xE9); // jmp 143 | *reinterpret_cast(&asm_code[1]) = 144 | static_cast(reinterpret_cast(dest) - (addr + 5)); 145 | for (size_t i = 5; i < asm_len; i++) { 146 | asm_code[i] = static_cast(0x90); // nop 147 | } 148 | return WriteHook(addr, asm_code); 149 | } 150 | 151 | bool MemoryAPI::HookNop(const HookPoint hook_point) { 152 | CHECK_HANDLE_OR_RETURN_FALSE(); 153 | uint32_t addr = hook_point.first; 154 | uint32_t asm_len = hook_point.second; 155 | if (hooks_.contains(addr)) { 156 | return true; 157 | } 158 | absl::InlinedVector asm_code(asm_len, 0x90); 159 | return WriteHook(addr, asm_code); 160 | } 161 | 162 | bool MemoryAPI::RestoreHook(const HookPoint hook_point) { 163 | uint32_t addr = hook_point.first; 164 | auto it = hooks_.find(addr); 165 | if (it == hooks_.end()) { 166 | return true; 167 | } 168 | const absl::InlinedVector& original_code = it->second; 169 | #ifdef YRTR_DEBUG 170 | uint32_t asm_len = hook_point.second; 171 | DCHECK_EQ(asm_len, original_code.size()); 172 | #endif 173 | if (!WriteMemory(addr, original_code)) { 174 | return false; 175 | } 176 | DLOG_F(INFO, "RestoreHook at={:08X} len={}", addr, original_code.size()); 177 | hooks_.erase(it); 178 | return true; 179 | } 180 | 181 | bool MemoryAPI::AutoAssemble(std::string_view script, bool activate) const { 182 | CHECK_HANDLE_OR_RETURN_FALSE(); 183 | // return _autoassemble(handle_->handle(), std::string(script), 184 | // static_cast(activate)); 185 | // Auto assemble is nice, but it's difficult to plug in complex filters writen 186 | // in cpp. MSVC inline asm is more "spaghetti" but easier to interact with 187 | // cpp. Sincerely, it's a tough trade-off. 188 | UNREFERENCED_PARAMETER(script); 189 | UNREFERENCED_PARAMETER(activate); 190 | NOT_IMPLEMENTED(); 191 | } 192 | 193 | #undef CHECK_HANDLE_OR_RETURN_FALSE 194 | 195 | bool MemoryAPI::WriteHook(uint32_t addr, std::span code) { 196 | absl::InlinedVector original_code(code.size(), 0); 197 | if (!ReadMemory(addr, /*out*/ original_code)) { 198 | return false; 199 | } 200 | if (!WriteMemory(addr, code)) { 201 | return false; 202 | } 203 | DLOG_F(INFO, "Hook at={:08X} len={}", addr, code.size()); 204 | hooks_.emplace(addr, std::move(original_code)); 205 | return true; 206 | } 207 | 208 | void MemoryAPI::RestoreAllHooks() { 209 | for (const auto& [addr, original_code] : hooks_) { 210 | DLOG_F(INFO, "RestoreHook at={:08X} len={}", addr, original_code.size()); 211 | WriteMemory(addr, original_code); 212 | } 213 | hooks_.clear(); 214 | } 215 | 216 | } // namespace hook 217 | } // namespace backend 218 | } // namespace yrtr 219 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | set(CMAKE_CXX_STANDARD 23) 3 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 4 | set(CMAKE_CXX_SCAN_FOR_MODULES OFF) 5 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 6 | 7 | project(ra2_trainer) 8 | 9 | option(YRTR_BUILD_TEST "Build unit tests." ON) 10 | option(YRTR_MSVC_STATIC_RUNTIME "Link static runtime libraries" ON) 11 | option(YRTR_FORCE_DCHECK "Force enable debug check." OFF) 12 | option(YRTR_ENABLE_WEB "Enable web as frontend" ON) 13 | 14 | set(YRTR_DEPS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps) 15 | set(YRTR_DEPS_OUTPUT_DIR ${YRTR_DEPS_DIR}/out/${CMAKE_BUILD_TYPE}) 16 | set(YRTR_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) 17 | 18 | if(YRTR_MSVC_STATIC_RUNTIME) 19 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 20 | else() 21 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") 22 | endif() 23 | 24 | # Abseil log reports: warning C4127: conditional expression is constant 25 | add_compile_options(/Zc:__cplusplus /MP /utf-8 /EHsc $<$:/Gy> /wd4127) 26 | add_compile_definitions( 27 | NOMINMAX # Disable min/max macros in minwindef.h. They are sucks! 28 | _SILENCE_ALL_CXX23_DEPRECATION_WARNINGS 29 | $<$:YRTR_DEBUG> # facilitates searching 30 | ) 31 | add_link_options($<$:/OPT:REF> $<$:/INCREMENTAL:NO>) 32 | 33 | # Add dependencies. 34 | find_package(absl REQUIRED) 35 | find_package(Freetype REQUIRED) 36 | find_package(glfw3 REQUIRED) 37 | find_package(Microsoft.GSL REQUIRED) 38 | find_package(plutosvg REQUIRED) 39 | find_package(tomlplusplus REQUIRED) 40 | find_package(websocketpp REQUIRED) 41 | 42 | set(IMGUI_PROJ_DIR ${YRTR_DEPS_DIR}/imgui-1.91.9b-docking) 43 | set(IMGUI_INCLUDES ${IMGUI_PROJ_DIR} 44 | ${IMGUI_PROJ_DIR}/backends) 45 | set(IMGUI_SOURCES ${IMGUI_PROJ_DIR}/imgui.cpp 46 | ${IMGUI_PROJ_DIR}/imgui_demo.cpp 47 | ${IMGUI_PROJ_DIR}/imgui_draw.cpp 48 | ${IMGUI_PROJ_DIR}/imgui_tables.cpp 49 | ${IMGUI_PROJ_DIR}/imgui_widgets.cpp 50 | ${IMGUI_PROJ_DIR}/misc/freetype/imgui_freetype.cpp 51 | ${IMGUI_PROJ_DIR}/backends/imgui_impl_glfw.cpp 52 | ${IMGUI_PROJ_DIR}/backends/imgui_impl_opengl3.cpp 53 | ${IMGUI_PROJ_DIR}/backends/imgui_impl_win32.cpp) 54 | add_library(libimgui STATIC ${IMGUI_SOURCES}) 55 | target_include_directories(libimgui PUBLIC ${IMGUI_INCLUDES}) 56 | target_compile_definitions(libimgui PUBLIC 57 | IMGUI_DISABLE_DEFAULT_ALLOCATORS # use custom malloc/free across dlls 58 | IMGUI_USE_WCHAR32 IMGUI_ENABLE_FREETYPE IMGUI_ENABLE_FREETYPE_PLUTOSVG # support emoji 59 | ) 60 | target_link_libraries(libimgui PRIVATE Freetype::Freetype glfw plutosvg::plutosvg) 61 | 62 | add_library(wsock32 SHARED 63 | ${YRTR_SOURCE_DIR}/wsock32/export_table.def 64 | ${YRTR_SOURCE_DIR}/wsock32/wsock32.cc 65 | ${YRTR_SOURCE_DIR}/base/logging.cc 66 | ) 67 | target_include_directories(wsock32 PUBLIC ${YRTR_SOURCE_DIR}) 68 | target_compile_definitions(wsock32 PRIVATE _WINSOCK_DEPRECATED_NO_WARNINGS) 69 | target_compile_options(wsock32 PRIVATE /W4 $<$:/WX>) 70 | target_link_libraries(wsock32 71 | PRIVATE absl::log 72 | PRIVATE absl::log_initialize 73 | PRIVATE mswsock 74 | PRIVATE ws2_32 75 | ) 76 | 77 | add_library(yrtr_base STATIC 78 | ${YRTR_SOURCE_DIR}/base/debug.cc 79 | ${YRTR_SOURCE_DIR}/base/logging.cc 80 | ${YRTR_SOURCE_DIR}/base/task_queue.cc 81 | ${YRTR_SOURCE_DIR}/base/thread.cc 82 | ) 83 | target_include_directories(yrtr_base 84 | PUBLIC $ 85 | PUBLIC $ 86 | PUBLIC $ 87 | ) 88 | target_link_libraries(yrtr_base 89 | PUBLIC absl::check 90 | PUBLIC absl::flat_hash_map 91 | PUBLIC absl::flat_hash_set 92 | PUBLIC absl::inlined_vector 93 | PUBLIC absl::log 94 | PUBLIC absl::log_initialize 95 | PUBLIC absl::synchronization 96 | ) 97 | 98 | add_library(yrtr_frontend STATIC 99 | ${YRTR_SOURCE_DIR}/frontend/desktop/config.cc 100 | ${YRTR_SOURCE_DIR}/frontend/desktop/timer.cc 101 | ${YRTR_SOURCE_DIR}/frontend/desktop/gui.cc 102 | ${YRTR_SOURCE_DIR}/frontend/desktop/gui_context.cc 103 | ${YRTR_SOURCE_DIR}/protocol/client.cc 104 | ) 105 | target_include_directories(yrtr_frontend 106 | PUBLIC $ 107 | PUBLIC $ 108 | PUBLIC $ 109 | ) 110 | target_compile_definitions(yrtr_frontend 111 | PRIVATE $<$:YRTR_GL_DEBUG_CTX> 112 | ) 113 | target_compile_options(yrtr_frontend PRIVATE /W4 $<$:/WX>) 114 | target_link_libraries(yrtr_frontend 115 | PRIVATE yrtr_base 116 | PRIVATE glfw 117 | PRIVATE libimgui 118 | PRIVATE opengl32 119 | PRIVATE tomlplusplus::tomlplusplus 120 | ) 121 | 122 | add_executable(ra2_trainer WIN32 ${YRTR_SOURCE_DIR}/frontend/desktop/bin/main.cc) 123 | target_compile_options(ra2_trainer PRIVATE /W4 $<$:/WX>) 124 | target_link_libraries(ra2_trainer PRIVATE yrtr_frontend glfw) 125 | 126 | add_library(yrtr_backend STATIC 127 | ${YRTR_SOURCE_DIR}/backend/config.cc 128 | ${YRTR_SOURCE_DIR}/backend/record.cc 129 | ${YRTR_SOURCE_DIR}/backend/hook/game_loop.cc 130 | ${YRTR_SOURCE_DIR}/backend/hook/hook.cc 131 | ${YRTR_SOURCE_DIR}/backend/hook/logging.cc 132 | ${YRTR_SOURCE_DIR}/backend/hook/memory_api.cc 133 | ${YRTR_SOURCE_DIR}/backend/hook/trainer.cc 134 | ${YRTR_SOURCE_DIR}/protocol/server.cc 135 | ) 136 | target_include_directories(yrtr_backend 137 | PUBLIC $ 138 | PUBLIC $ 139 | PUBLIC $ 140 | PUBLIC $ 141 | PUBLIC $ 142 | ) 143 | if (YRTR_ENABLE_WEB) 144 | target_compile_definitions(yrtr_backend PRIVATE YRTR_ENABLE_WEB) 145 | endif() 146 | # warning C4740: flow in or out of inline asm code suppresses global optimization 147 | target_compile_options(yrtr_backend PRIVATE /W4 $<$:/WX> /wd4740) 148 | target_link_libraries(yrtr_backend 149 | PRIVATE yrtr_base 150 | PRIVATE tomlplusplus::tomlplusplus 151 | PRIVATE comctl32 152 | PRIVATE winmm 153 | ) 154 | 155 | add_library(ra2_trainer_backend SHARED ${YRTR_SOURCE_DIR}/backend/lib/dllmain.cc) 156 | target_compile_options(ra2_trainer_backend PRIVATE /W4 $<$:/WX>) 157 | target_link_libraries(ra2_trainer_backend 158 | PRIVATE yrtr_backend 159 | PRIVATE absl::failure_signal_handler 160 | ) 161 | 162 | if(CMAKE_GENERATOR MATCHES "Visual Studio") 163 | # Visual Studio-specific output directory 164 | set(EXE_OUTPUT_DIR ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}) 165 | else() 166 | # Default output directory for other generators 167 | set(EXE_OUTPUT_DIR ${CMAKE_BINARY_DIR}) 168 | endif() 169 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXE_OUTPUT_DIR}) 170 | add_custom_command(TARGET ra2_trainer_backend POST_BUILD 171 | COMMAND ${CMAKE_COMMAND} -E copy 172 | ${CMAKE_CURRENT_SOURCE_DIR}/scripts/ra2_trainer.toml 173 | ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} 174 | COMMAND ${CMAKE_COMMAND} -E copy 175 | ${CMAKE_CURRENT_SOURCE_DIR}/scripts/ra2_trainer_backend.toml 176 | ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} 177 | ) 178 | 179 | if (YRTR_BUILD_TEST) 180 | add_executable(test_server 181 | ${YRTR_SOURCE_DIR}/backend/bin/test_server.cc 182 | ${YRTR_SOURCE_DIR}/backend/hook/mock_trainer.cc 183 | ) 184 | target_link_libraries(test_server 185 | PRIVATE yrtr_base 186 | PRIVATE yrtr_backend 187 | PRIVATE absl::failure_signal_handler 188 | ) 189 | 190 | add_executable(test_json ${YRTR_SOURCE_DIR}/protocol/test_json.cc) 191 | target_link_libraries(test_json PRIVATE yrtr_base) 192 | endif() 193 | -------------------------------------------------------------------------------- /scripts/ra2_trainer_backend.toml: -------------------------------------------------------------------------------- 1 | [ra2_trainer] 2 | # Optional, only used for debugging. 3 | hotreload_directory = "ra2_trainer_hotreload" 4 | port = 35271 5 | # Auto record enabled checkboxes in last game. 6 | auto_record = true 7 | 8 | [tech] 9 | # Intruder-入侵者战机 10 | ORCA = true 11 | # Black Eagle-黑鹰战机 12 | BEAG = true 13 | # Allied Power Plant-盟军发电厂 14 | GAPOWR = true 15 | # Allied Ore Refinery-盟军矿厂 16 | GAREFN = true 17 | # Allied Construction Yard-盟军建造场 18 | GACNST = true 19 | # Allied Barracks-盟军兵营 20 | GAPILE = true 21 | # Allied Service Depot-盟军维修厂 22 | GADEPT = true 23 | # Allied Battle Lab-盟军作战实验室 24 | GATECH = true 25 | # Allied War Factory-盟军战车工厂 26 | GAWEAP = true 27 | # Einstein's Lab-爱因斯坦实验室 28 | CALAB = false 29 | # Soviet Tesla Reactor-磁能反应炉 30 | NAPOWR = true 31 | # Soviet Battle Lab-苏军作战实验室 32 | NATECH = true 33 | # Soviet Barracks-苏军兵营 34 | NAHAND = true 35 | # Allied Wall-盟军围墙 36 | GAWALL = true 37 | # Soviet Radar Tower-苏军雷达 38 | NARADR = true 39 | # Soviet War Factory-苏军战车工厂 40 | NAWEAP = true 41 | # Soviet Ore Refinery-苏军矿厂 42 | NAREFN = true 43 | # Soviet Wall-苏军围墙 44 | NAWALL = true 45 | # Psychic Sensor-心灵感应器 46 | NAPSIS = true 47 | # Soviet Sentry Gun-哨戒炮 48 | NALASR = true 49 | # Allied Patriot Missile-爱国者飞弹 50 | NASAM = true 51 | # Sydney Kangaroo Burger-悉尼袋鼠堡 52 | CASYDN02 = false 53 | # Allied Shipyard-盟军船厂 54 | GAYARD = true 55 | # Soviet Iron Curtain Device-铁幕装置 56 | NAIRON = true 57 | # Soviet Construction Yard-苏军建造场 58 | NACNST = true 59 | # Soviet Service Depot-苏军维修厂 60 | NADEPT = true 61 | # Allied Chrono Sphere-超时空传送仪 62 | GACSPH = true 63 | # Allied Weather Controller-天气控制器 64 | GAWEAT = true 65 | # Bridge repair hut-桥梁维修小屋 66 | CABHUT = false 67 | # Old Civilian Hospital-市民医院 68 | CAHOSP = false 69 | # Soviet Tesla Coil-磁暴线圈 70 | TESLA = true 71 | # Soviet Nuclear Missile Silo-核弹发射井 72 | NAMISL = true 73 | # Allied Prism Cannon-光棱塔 74 | ATESLA = true 75 | # Tech Machine Shop-科技机器商店 76 | CAMACH = false 77 | # Sydney Opera House-悉尼歌剧院 78 | CASYDN03 = false 79 | # Ammo Crates-弹药箱 80 | AMMOCRAT = false 81 | # Soviet Shipyard-苏军造船厂 82 | NAYARD = true 83 | # Allied SpySat Uplink-间谍卫星 84 | GASPYSAT = true 85 | # Allied Gap Generator-裂缝产生器 86 | GAGAP = true 87 | # Allied Grand Cannon-巨炮 88 | GTGCAN = true 89 | # Soviet Nuclear Reactor-核子反应堆 90 | NANRCT = true 91 | # Allied Pill Box-机枪碉堡 92 | GAPILL = true 93 | # Soviet Flak Cannon-防空炮 94 | NAFLAK = true 95 | # Tech Outpost-科技前哨站 96 | CAOUTP = false 97 | # Tech Hospital-科技医院 98 | CATHOSP = false 99 | # Tech Airport-科技机场 100 | CAAIRP = false 101 | # Tech Oil Derrick-科技钻油厂 102 | CAOILD = true 103 | # Cloning Vats-复制中心 104 | NACLON = true 105 | # Allied Ore Processor-矿石精炼器 106 | GAOREP = true 107 | # Statue of Liberty-自由女神像 108 | CANEWY04 = false 109 | # ZZZ World Trade Center-世贸中心 110 | CANEWY05 = false 111 | # Communications-通讯中心 112 | CATECH01 = false 113 | # White House-白宫 114 | CAWASH01 = false 115 | # Barrels-油桶1 116 | CAMISC01 = false 117 | # Barrel-油桶2 118 | CAMISC02 = false 119 | # Paris Tower-埃菲尔铁塔 120 | CAPARS01 = false 121 | # Allied Airforce Command Headquarters-盟军空指部 122 | GAAIRC = false 123 | # Farm Outhouse-移动式厕所 124 | CAFRMB = false 125 | # Allied American Airforce Command Headquarters-美国空指部 126 | AMRADR = true 127 | # Guard Shack-警卫哨 128 | CAGARD01 = false 129 | # Mayan Prism Pyramid-玛雅金字塔 130 | MAYAN = false 131 | # Mayan Prism Pyramid-玛雅金字塔 132 | CAMEX01 = false 133 | # Russain Kremlin Palace-克里姆林宫 134 | CARUS03 = false 135 | # Paris Arc de Triumphe-巴黎凯旋门 136 | CAPARS11 = false 137 | # Lighthouse-灯塔 138 | CAFARM06 = false 139 | # Psychic Beacon-心灵信标 140 | NAPSYB = true 141 | # Psychic Amplifier-心灵增幅器 142 | NAPSYA = false 143 | # Air Force Academy Chapel Colorado-空军学院礼拜堂 144 | CACOLO01 = false 145 | # Derelict Tank-废弃猛犸坦克 146 | CAMSC13 = false 147 | # Tech Secret Lab-秘密科技实验室 148 | CASLAB = false 149 | # TRANS FORTRESS-尤里要塞 150 | CATRAN03 = false 151 | # Tech Civilian Power Plant-科技电厂 152 | CAPOWR = false 153 | # Concrete Bunker 03-科技混凝土碉堡 154 | CABUNK03 = false 155 | # Concrete Bunker 04-科技混凝土碉堡 156 | CABUNK04 = false 157 | # YACNST-尤里建造场 158 | YACNST = true 159 | # YAPOWR-生化反应室 160 | YAPOWR = true 161 | # YAREFN-奴隶矿场 162 | YAREFN = true 163 | # YABRCK-尤里兵营 164 | YABRCK = true 165 | # YATECH-尤里作战实验室 166 | YATECH = true 167 | # YAWEAP-尤里战车工厂 168 | YAWEAP = true 169 | # NABNKR-战斗碉堡 170 | NABNKR = true 171 | # YAGGUN-盖特机炮 172 | YAGGUN = true 173 | # YAPSYT-心灵控制塔 174 | YAPSYT = true 175 | # NATBNK-坦克碉堡 176 | NATBNK = true 177 | # GAFWLL-尤里围墙 178 | GAFWLL = true 179 | # YAYARD-尤里船坞 180 | YAYARD = true 181 | # GI-美国大兵 182 | E1 = true 183 | # Conscript-动员兵 184 | E2 = true 185 | # Shock Trooper-磁暴步兵 186 | SHK = true 187 | # Engineer-工程师 188 | ENGINEER = true 189 | # Rocketeer-火箭飞行兵 190 | JUMPJET = true 191 | # SEAL-海豹部队 192 | GHOST = true 193 | # Yuri-尤里 194 | YURI = true 195 | # Crazy Ivan-疯狂伊文 196 | IVAN = true 197 | # Desolater-辐射工兵 198 | DESO = true 199 | # Attack Dog-苏联警犬 200 | DOG = true 201 | # Chrono Legionnaire-超时空军团兵 202 | CLEG = true 203 | # Spy-间谍 204 | SPY = true 205 | # Chrono Commando-超时空突击队 206 | CCOMAND = true 207 | # Psi-Corp Trooper-尤里复制人 208 | PTROOP = true 209 | # Chrono Ivan-超时空伊文 210 | CIVAN = true 211 | # Yuri Prime-尤里改 212 | YURIPR = true 213 | # Sniper-狙击手 214 | SNIPE = true 215 | # Animal Cow-奶牛 216 | COW = false 217 | # Animal Alligator-鳄鱼 218 | ALL = false 219 | # Tanya-谭雅 220 | TANY = true 221 | # Flak Trooper-防空步兵 222 | FLAKT = true 223 | # Terrorist-恐怖分子 224 | TERROR = true 225 | # Soviet Engineer-苏军工程师 226 | SENGINEER = true 227 | # Allied Attack Dog-盟军警犬 228 | ADOG = true 229 | # Vladimir-弗拉基米尔 230 | VLADIMIR = false 231 | # General Pentagon-五角大楼将军 232 | PENTGEN = false 233 | # President-总统 234 | PRES = false 235 | # Secret Service-秘密保镖 236 | SSRV = false 237 | # Animal Polar Bear-北极熊 238 | POLARB = false 239 | # Animal Monkey-猴子 240 | JOSH = false 241 | # Cowboy-Westwood之星 242 | CLNT = false 243 | # Hero-终结者 244 | ARND = false 245 | # Bodybuilder-兰博 246 | STLN = false 247 | # Animal Camel-骆驼 248 | CAML = false 249 | # Albert Einstein-爱因斯坦 250 | EINS = true 251 | # Evil Mummy-木乃伊 252 | MUMY = false 253 | # Romanov-罗曼诺夫 254 | RMNV = false 255 | # Lunar Infantry-登月飞行兵 256 | LUNR = false 257 | # Animal T-Rex-暴龙 258 | DNOA = false 259 | # Animal Bront-恐龙 260 | DNOB = false 261 | # Werewolf-狼人 262 | WWLF = false 263 | # BRUTE-狂兽人 264 | BRUTE = true 265 | # Allied Construction Vehicle-盟军移动基地车 266 | AMCV = true 267 | # War Miner-武装采矿车 268 | HARV = true 269 | # Apocalypse-灾厄坦克 270 | APOC = false 271 | # Rhino Heavy Tank-犀牛坦克 272 | HTNK = true 273 | # Armored Transport-野牛运输艇 274 | SAPC = true 275 | # Grizzly Battle Tank-灰熊坦克 276 | MTNK = true 277 | # War Miner-武装采矿车 278 | HORV = true 279 | # Aircraft Carrier-航空母舰 280 | CARRIER = true 281 | # V3 Launcher-V3导弹车 282 | V3 = true 283 | # Kirov Airship-基洛夫空艇 284 | ZEP = true 285 | # Terror Drone-恐怖机器人 286 | DRON = true 287 | # Flak Track-防空履带车 288 | HTK = true 289 | # Destroyer-驱逐舰 290 | DEST = true 291 | # Typhoon Attack Sub-台风攻击潜艇 292 | SUB = true 293 | # Aegis Cruiser-神盾巡洋舰 294 | AEGIS = true 295 | # Landing Craft-旅行者运输艇 296 | LCRF = true 297 | # Dreadnought-无畏级导弹舰 298 | DRED = true 299 | # BlackHawk Transport-黑鹰直升机 300 | SHAD = true 301 | # Giant Squid-巨型乌贼 302 | SQD = true 303 | # Dolphin-海豚 304 | DLPH = true 305 | # Soviet Construction Vehicle-苏军移动基地车 306 | SMCV = true 307 | # Tank Destroyer-坦克杀手 308 | TNKD = true 309 | # Howitzer-西风火炮 310 | HOWI = true 311 | # Tesla Tank-磁暴坦克 312 | TTNK = true 313 | # Hind Transport-飞碟 314 | HIND = true 315 | # Light Tank-轻坦克 316 | LTNK = false 317 | # Chrono Miner(noback)-超时空采矿车 318 | CMON = false 319 | # Chrono Miner-超时空采矿车 320 | CMIN = true 321 | # Prism Tank-光棱坦克 322 | SREF = true 323 | # Sea Scorpion-海蝎 324 | HYD = true 325 | # Mirage Tank-幻影坦克 326 | MGTK = true 327 | # IFV-多功能步兵车 328 | FV = true 329 | # Vladimir's Dreadnought-弗拉基米尔指挥舰 330 | VLAD = false 331 | # Demolitions Truck-自爆卡车 332 | DTRUCK = true 333 | # Cruise Ship-游轮 334 | CRUISE = false 335 | # PCV-尤里移动基地车 336 | PCV = true 337 | -------------------------------------------------------------------------------- /src/protocol/client.cc: -------------------------------------------------------------------------------- 1 | #include "protocol/client.h" 2 | 3 | #include 4 | 5 | #include "base/windows_shit.h" 6 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 7 | #include "base/logging.h" 8 | #include "base/macro.h" 9 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 10 | #include "absl/synchronization/mutex.h" 11 | __YRTR_END_THIRD_PARTY_HEADERS 12 | #include "base/thread.h" 13 | #include "gsl/util" 14 | 15 | namespace yrtr { 16 | 17 | #define BIND_BUTTON(fnname) \ 18 | gui.AddButtonListener(FnLabel::k##fnname, [this]() { \ 19 | boost::asio::dispatch(cli_.get_io_service(), \ 20 | [this]() { SendPostButton(FnLabel::k##fnname); }); \ 21 | }); 22 | #define BIND_CHECKBOX(fnname) \ 23 | gui.AddCheckboxListener(FnLabel::k##fnname, [this](bool activate) { \ 24 | boost::asio::dispatch(cli_.get_io_service(), [this, activate]() { \ 25 | SendPostCheckbox(FnLabel::k##fnname, activate); \ 26 | }); \ 27 | }); 28 | 29 | Client::Client(frontend::Gui& gui, uint16_t port) 30 | : uri_(std::format("ws://localhost:{}", port)), 31 | gui_(gui), 32 | conn_(nullptr), 33 | get_state_count_(0), 34 | stop_(false) { 35 | render_loop_ch_.SetThreadId(GetRendererThreadId()); 36 | cli_.set_open_handshake_timeout(kConnTimeoutMilliseconds); 37 | cli_.set_close_handshake_timeout(kConnTimeoutMilliseconds); 38 | cli_.set_pong_timeout(kConnTimeoutMilliseconds * 4); 39 | cli_.set_access_channels(websocketpp::log::alevel::all); 40 | cli_.clear_access_channels(websocketpp::log::alevel::frame_payload); 41 | cli_.init_asio(); 42 | cli_.set_message_handler([this](websocketpp::connection_hdl hdl, 43 | WebsocketClient::message_ptr msg) { 44 | OnMessage(cli_, hdl, msg); 45 | }); 46 | evloop_ = std::thread([&]() { 47 | SetupNetThreadOnce(); 48 | while (!stop_.load()) { 49 | GetOrCreateConn(); 50 | // Would block here. Exit if the remote endpoint resets the connection, 51 | // client should always retry. 52 | cli_.run(); 53 | } 54 | DLOG_F(INFO, "Client exit"); 55 | }); 56 | 57 | gui.AddHouseListListener([this](SideMap&& side_map) { 58 | boost::asio::dispatch(cli_.get_io_service(), 59 | [this, side_map = std::move(side_map)]() mutable { 60 | SendPostProtectedList(std::move(side_map)); 61 | }); 62 | }); 63 | gui.AddInputListener(FnLabel::kApply, [this](uint32_t val) { 64 | boost::asio::dispatch(cli_.get_io_service(), [this, val]() { 65 | SendPostInput(FnLabel::kApply, val); 66 | }); 67 | }); 68 | BIND_BUTTON(IAMWinner); 69 | BIND_BUTTON(DeleteUnit); 70 | BIND_BUTTON(ClearShroud); 71 | BIND_BUTTON(GiveMeABomb); 72 | BIND_BUTTON(UnitLevelUp); 73 | BIND_BUTTON(UnitSpeedUp); 74 | BIND_BUTTON(FastBuild); 75 | BIND_BUTTON(ThisIsMine); 76 | BIND_CHECKBOX(God); 77 | BIND_CHECKBOX(InstBuild); 78 | BIND_CHECKBOX(UnlimitSuperWeapon); 79 | BIND_CHECKBOX(InstFire); 80 | BIND_CHECKBOX(InstTurn); 81 | BIND_CHECKBOX(RangeToYourBase); 82 | BIND_CHECKBOX(FireToYourBase); 83 | BIND_CHECKBOX(FreezeGapGenerator); 84 | BIND_CHECKBOX(SellTheWorld); 85 | BIND_CHECKBOX(BuildEveryWhere); 86 | BIND_CHECKBOX(AutoRepair); 87 | BIND_CHECKBOX(SocialismMajesty); 88 | BIND_CHECKBOX(MakeCapturedMine); 89 | BIND_CHECKBOX(MakeGarrisonedMine); 90 | BIND_CHECKBOX(InvadeMode); 91 | BIND_CHECKBOX(UnlimitTech); 92 | BIND_CHECKBOX(UnlimitFirePower); 93 | BIND_CHECKBOX(InstChrono); 94 | BIND_CHECKBOX(SpySpy); 95 | BIND_CHECKBOX(AdjustGameSpeed); 96 | } 97 | 98 | #undef BIND_BUTTON 99 | #undef BIND_CHECKBOX 100 | 101 | Client::~Client() {} 102 | 103 | void Client::Stop() { 104 | stop_.store(true); 105 | boost::asio::dispatch(cli_.get_io_service(), [&]() { 106 | if (conn_ != nullptr && 107 | conn_->get_state() < websocketpp::session::state::value::closing) { 108 | websocketpp::lib::error_code ec; 109 | conn_->close(websocketpp::close::status::normal, "stop", ec); 110 | if (ec) { 111 | LOG_F(ERROR, "Failed to close connection err=[{}]{}", ec.value(), 112 | ec.message()); 113 | } 114 | } 115 | }); 116 | evloop_.join(); 117 | } 118 | 119 | void Client::Update() { 120 | DCHECK(IsWithinRendererThread()); 121 | render_loop_ch_.ExecuteTasks(); 122 | } 123 | 124 | void Client::GetState() { 125 | DCHECK(IsWithinRendererThread()); 126 | boost::asio::dispatch(cli_.get_io_service(), [&]() { SendGetState(); }); 127 | } 128 | 129 | Client::WebsocketClient::connection_ptr Client::GetOrCreateConn() { 130 | DCHECK(IsWithinNetThread()); 131 | if (conn_ != nullptr && 132 | conn_->get_state() < websocketpp::session::state::value::closing) { 133 | return conn_; 134 | } else { 135 | websocketpp::lib::error_code ec; 136 | // Drop expired dangling connection. 137 | if (conn_ != nullptr) { 138 | conn_->close(websocketpp::close::status::normal, "stop", ec); 139 | // Drop error code anyway. 140 | UNREFERENCED_PARAMETER(ec); 141 | } 142 | // Create new one. 143 | conn_ = cli_.get_connection(uri_, ec); 144 | if (ec) { 145 | LOG(ERROR) << "could not create connection because: " << ec.message(); 146 | return nullptr; 147 | } 148 | cli_.connect(conn_); 149 | return conn_; 150 | } 151 | } 152 | 153 | void Client::OnMessage(WebsocketClient& /*cli*/, 154 | websocketpp::connection_hdl /*hdl*/, 155 | WebsocketClient::message_ptr msg) { 156 | DCHECK(IsWithinNetThread()); 157 | std::string payload = msg->get_payload(); 158 | try { 159 | json raw_data = json::parse(payload); 160 | std::string type = raw_data.at("type"); 161 | if (type == "get_state") { 162 | OnGetStateEvent(ParseGetStateEvent(raw_data)); 163 | } else { 164 | LOG_F(ERROR, "Unknown event type={} from data={}", type, payload); 165 | } 166 | } catch (const std::exception& e) { 167 | LOG_F(ERROR, "Failed to parse json data={} error={}", payload, e.what()); 168 | } 169 | } 170 | 171 | void Client::SendGetState() { 172 | DCHECK(IsWithinNetThread()); 173 | auto _ = gsl::finally([&]() { get_state_count_.fetch_sub(1); }); 174 | if (get_state_count_.fetch_add(1) + 1 > kMaxGetState) { 175 | return; 176 | } 177 | SendData(kApiGetState, MakeGetStateEvent()); 178 | } 179 | 180 | void Client::OnGetStateEvent(Event&& event) { 181 | DCHECK(IsWithinNetThread()); 182 | render_loop_ch_.ExecuteOrScheduleTask([this, state = std::move(event.val)]() { 183 | DCHECK(IsWithinRendererThread()); 184 | gui_.UpdateState(state); 185 | }); 186 | } 187 | 188 | void Client::SendPostInput(FnLabel label, uint32_t val) { 189 | DCHECK(IsWithinNetThread()); 190 | SendData(kApiPostEvent, MakeInputEvent(label, val)); 191 | } 192 | 193 | void Client::SendPostButton(FnLabel label) { 194 | DCHECK(IsWithinNetThread()); 195 | SendData(kApiPostEvent, MakeButtonEvent(label)); 196 | } 197 | 198 | void Client::SendPostCheckbox(FnLabel label, bool activate) { 199 | DCHECK(IsWithinNetThread()); 200 | SendData(kApiPostEvent, MakeCheckboxEvent(label, activate)); 201 | } 202 | 203 | void Client::SendPostProtectedList(SideMap&& side_map) { 204 | DCHECK(IsWithinNetThread()); 205 | SendData(kApiPostEvent, MakeProtectedListEvent(std::move(side_map))); 206 | } 207 | 208 | void Client::SendData(std::string_view /*path*/, std::string&& data) { 209 | DCHECK(IsWithinNetThread()); 210 | WebsocketClient::connection_ptr conn = GetOrCreateConn(); 211 | if (conn == nullptr) { 212 | DLOG_F(WARNING, "Failed to send post data, connection not established"); 213 | return; 214 | } 215 | if (conn->get_state() != websocketpp::session::state::open) { 216 | // The connection is not prepared. 217 | DLOG_F(WARNING, 218 | "Failed to send post data, connection not prepared at state={}", 219 | static_cast(conn->get_state())); 220 | return; 221 | } 222 | websocketpp::lib::error_code ec = 223 | conn->send(data, websocketpp::frame::opcode::text); 224 | if (ec) { 225 | LOG_F(ERROR, "Failed to send post data with error=[{}]{}", ec.value(), 226 | ec.message()); 227 | } 228 | } 229 | 230 | } // namespace yrtr 231 | -------------------------------------------------------------------------------- /src/frontend/web/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | BtnFnLabelFirst, BtnFnLabelLast, 3 | CheckboxFnLabelFirst, CheckboxFnLabelLast, 4 | FnLabel, strFnLabel 5 | } from './protocol'; 6 | import { YRTRClient } from './client'; 7 | import { Localization } from './localization'; 8 | 9 | var client = undefined; 10 | var selectingHouseMap = new Map(); 11 | var protectedHouseMap = new Map(); 12 | var localization = new Localization(); 13 | 14 | function initTab() { 15 | document.querySelectorAll('.tab').forEach(tab => { 16 | tab.addEventListener('click', () => { 17 | // Remove active class from all tabs and content 18 | document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); 19 | document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); 20 | // Add active class to clicked tab and corresponding content 21 | tab.classList.add('active'); 22 | const tabId = tab.getAttribute('data-tab'); 23 | document.getElementById(tabId).classList.add('active'); 24 | }); 25 | }); 26 | } 27 | 28 | function initFilterList() { 29 | // Source list functionality 30 | const sourceList = document.getElementById('source-list'); 31 | const destinationList = document.getElementById('destination-list'); 32 | const addAllBtn = document.getElementById('kAddAll'); 33 | const clearAllBtn = document.getElementById('kClearAll'); 34 | // Add all items to destination list 35 | addAllBtn.addEventListener('click', () => { 36 | sourceList.querySelectorAll('.selectable').forEach(item => { 37 | const houseId = parseInt(item.id); 38 | const house = selectingHouseMap.get(houseId); 39 | protectedHouseMap.set(houseId, house); 40 | updateProtectedHouseList(protectedHouseMap); 41 | }); 42 | }); 43 | // Clear all items from destination list 44 | clearAllBtn.addEventListener('click', () => { 45 | destinationList.innerHTML = ''; 46 | protectedHouseMap.clear(); 47 | updateProtectedHouseList(protectedHouseMap); 48 | }); 49 | } 50 | 51 | function initButton() { 52 | // Bind apply button with heading input. 53 | const apply_btn = document.getElementById('kApply'); 54 | apply_btn.addEventListener('click', () => { 55 | const amount = document.getElementById('money-input').value; 56 | if (amount) { 57 | onTriggerBtn(FnLabel.kApply, parseInt(amount)); 58 | } else { 59 | onTriggerBtn(FnLabel.kApply, 23333); 60 | } 61 | }); 62 | // Add rest buttons. 63 | let btnList = document.getElementById('btn-list'); 64 | for (let i = BtnFnLabelFirst; i <= BtnFnLabelLast; i++) { 65 | const btn = document.createElement('button'); 66 | btn.id = `btn${i}`; 67 | btn.textContent = localization.getFnStr(`k${strFnLabel(i)}`); 68 | btn.addEventListener('click', () => onTriggerBtn(i, /*val*/ undefined)); 69 | btnList.appendChild(btn); 70 | } 71 | } 72 | 73 | function createCheckbox(id, onChange) { 74 | const group = document.createElement('div'); 75 | group.className = 'checkbox-group'; 76 | const label = document.createElement('label'); 77 | label.className = 'checkbox-label'; 78 | const input = document.createElement('input'); 79 | input.type = 'checkbox'; 80 | input.id = `checkbox${id}`; 81 | input.addEventListener('change', (e) => { 82 | const checked = input.checked; 83 | // Only allowed to chagnge state from this script. 84 | input.checked = !input.checked; 85 | onChange(checked); 86 | }); 87 | const text = document.createElement('span'); 88 | text.textContent = localization.getFnStr(`k${strFnLabel(id)}`); 89 | label.appendChild(input); 90 | label.appendChild(text); 91 | group.appendChild(label); 92 | return group; 93 | } 94 | 95 | function initCheckbox() { 96 | let checkboxList = document.getElementById('checkbox-list'); 97 | for (let i = CheckboxFnLabelFirst; i <= CheckboxFnLabelLast; i++) { 98 | const checkbox = createCheckbox(i, (checked) => { 99 | onTriggerCheckbox(i, checked); 100 | }); 101 | checkboxList.appendChild(checkbox); 102 | } 103 | } 104 | 105 | function updateSelectingHouseList(houses) { 106 | // Do not clear all by set innerHTML and add all here, frequently changing 107 | // items even nothing changed causes click event randomly missing. 108 | const list = document.getElementById('source-list'); 109 | // Update selecting map. 110 | selectingHouseMap.clear(); 111 | houses.forEach(element => { 112 | const houseId = element[0]; 113 | const house = element[1]; 114 | selectingHouseMap.set(houseId, house); 115 | }); 116 | // Remove items not in the map. 117 | let existHouseIds = new Set(); 118 | list.querySelectorAll('.selectable').forEach(item => { 119 | const existHouseId = parseInt(item.id); 120 | existHouseIds.add(existHouseId); 121 | if (!selectingHouseMap.has(existHouseId)) { 122 | list.removeChild(item); 123 | } 124 | }); 125 | // Add items not in the list. 126 | selectingHouseMap.forEach((house, houseId) => { 127 | if (!existHouseIds.has(houseId)) { 128 | const houseName = house.name; 129 | const item = document.createElement('div'); 130 | item.id = houseId; 131 | item.className = 'selectable'; 132 | item.textContent = houseName; 133 | item.addEventListener('click', () => { 134 | protectedHouseMap.set(houseId, house); 135 | updateProtectedHouseList(protectedHouseMap); 136 | }); 137 | list.appendChild(item); 138 | } 139 | }); 140 | } 141 | 142 | function updateProtectedHouseList(houses) { 143 | const list = document.getElementById('destination-list'); 144 | list.innerHTML = ''; 145 | houses.forEach((house, houseId) => { 146 | const houseName = house.name; 147 | const item = document.createElement('div'); 148 | item.id = houseId; 149 | item.className = 'selectable'; 150 | item.textContent = houseName; 151 | item.addEventListener('click', () => { 152 | protectedHouseMap.delete(houseId); 153 | updateProtectedHouseList(protectedHouseMap); 154 | }); 155 | list.appendChild(item); 156 | }); 157 | onUpdateProtectedHouseList(houses); 158 | } 159 | 160 | function onStateUpdate(state) { 161 | // Update your GUI with the new state 162 | console.debug('Received state update:', state); 163 | // Update checkboxes 164 | if (state.ckbox_states) { 165 | state.ckbox_states.forEach(element => { 166 | const label = element[0]; 167 | const checkboxState = element[1]; 168 | const checkbox = document.getElementById(`checkbox${label}`); 169 | if (checkbox) { 170 | checkbox.checked = checkboxState.activate; 171 | checkbox.disabled = !checkboxState.enable; 172 | } 173 | }); 174 | } 175 | // Update house lists 176 | if (state.selecting_houses) { 177 | updateSelectingHouseList(state.selecting_houses); 178 | } 179 | } 180 | 181 | function initClient() { 182 | client = new YRTRClient(`ws://${window.location.hostname}:${window.location.port}`, 183 | onStateUpdate); 184 | // Connect to server 185 | client.connect(); 186 | // Clean up on page unload 187 | window.addEventListener('beforeunload', function () { 188 | client.disconnect(); 189 | }); 190 | } 191 | 192 | function onTriggerBtn(fnLabel, val) { 193 | console.log(`Button [${fnLabel}]${strFnLabel(fnLabel)} clicked`); 194 | if (client) { 195 | if (fnLabel == FnLabel.kApply) { 196 | client.sendInput(FnLabel.kApply, val); 197 | } else { 198 | client.sendButton(fnLabel); 199 | } 200 | } else { 201 | console.warn(`Client not connecting to the server`); 202 | } 203 | } 204 | 205 | function onTriggerCheckbox(fnLabel, checked) { 206 | console.log(`Checkbox [${fnLabel}]${strFnLabel(fnLabel)} is now ${checked}`); 207 | if (client) { 208 | client.sendCheckbox(fnLabel, checked); 209 | } else { 210 | console.warn(`Client not connecting to the server`); 211 | } 212 | } 213 | 214 | function onUpdateProtectedHouseList(houses) { 215 | let sideMap = Array.from(houses); 216 | client.sendProtectedList(sideMap); 217 | } 218 | 219 | function applyLocalization() { 220 | const fn_labels = [ 221 | "kApply", 222 | ]; 223 | const gui_labels = [ 224 | "kMoney", 225 | "kAssist", 226 | "kFilter", 227 | "kSelectingHouseList", 228 | "kProtectedHouseList", 229 | "kAddAll", 230 | "kClearAll", 231 | ]; 232 | fn_labels.forEach(label => { 233 | let element = document.getElementById(label); 234 | if (element) { 235 | element.textContent = localization.getFnStr(label); 236 | } 237 | }); 238 | gui_labels.forEach(label => { 239 | let element = document.getElementById(label); 240 | if (element) { 241 | element.textContent = localization.getGuiStr(label); 242 | } 243 | }); 244 | } 245 | 246 | document.addEventListener('DOMContentLoaded', function () { 247 | initTab(); 248 | initFilterList(); 249 | initButton(); 250 | initCheckbox(); 251 | applyLocalization(); 252 | initClient(); 253 | }); 254 | -------------------------------------------------------------------------------- /src/backend/tech.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace yrtr { 6 | namespace backend { 7 | 8 | enum class Tech { 9 | kUnknown = -1, 10 | ORCA, 11 | HORNET, 12 | BEAG, 13 | GAPOWR, 14 | GAREFN, 15 | GACNST, 16 | GAPILE, 17 | GASAND, 18 | GADEPT, 19 | GATECH, 20 | GAWEAP, 21 | CALAB, 22 | NAPOWR, 23 | NATECH, 24 | NAHAND, 25 | GAWALL, 26 | NARADR, 27 | NAWEAP, 28 | NAREFN, 29 | NAWALL, 30 | NAPSIS, 31 | NALASR, 32 | NASAM, 33 | CASYDN02, 34 | GAYARD, 35 | NAIRON, 36 | NACNST, 37 | NADEPT, 38 | GACSPH, 39 | GAWEAT, 40 | CABHUT, 41 | CAHOSP, 42 | TESLA, 43 | NAMISL, 44 | ATESLA, 45 | CAMACH, 46 | CASYDN03, 47 | AMMOCRAT, 48 | NAYARD, 49 | GASPYSAT, 50 | GAGAP, 51 | GTGCAN, 52 | NANRCT, 53 | GAPILL, 54 | NAFLAK, 55 | CAOUTP, 56 | CATHOSP, 57 | CAAIRP, 58 | CAOILD, 59 | NACLON, 60 | GAOREP, 61 | CANEWY04, 62 | CANEWY05, 63 | CATECH01, 64 | CAWASH01, 65 | CAMISC01, 66 | CAMISC02, 67 | CAPARS01, 68 | GAAIRC, 69 | CAFRMB, 70 | AMRADR, 71 | CAGARD01, 72 | MAYAN, 73 | CAMEX01, 74 | CARUS03, 75 | CAPARS11, 76 | CAFARM06, 77 | NAPSYB, 78 | NAPSYA, 79 | CACOLO01, 80 | CAMSC13, 81 | CASLAB, 82 | CATRAN03, 83 | CAPOWR, 84 | CABUNK03, 85 | CABUNK04, 86 | YACNST, 87 | YAPOWR, 88 | YAREFN, 89 | YABRCK, 90 | YATECH, 91 | YAWEAP, 92 | NABNKR, 93 | YAGGUN, 94 | YAPSYT, 95 | NATBNK, 96 | GAFWLL, 97 | YAYARD, 98 | E1, 99 | E2, 100 | SHK, 101 | ENGINEER, 102 | JUMPJET, 103 | GHOST, 104 | YURI, 105 | IVAN, 106 | DESO, 107 | DOG, 108 | CTECH, 109 | CLEG, 110 | SPY, 111 | CCOMAND, 112 | PTROOP, 113 | CIVAN, 114 | YURIPR, 115 | SNIPE, 116 | COW, 117 | ALL, 118 | TANY, 119 | FLAKT, 120 | TERROR, 121 | SENGINEER, 122 | ADOG, 123 | VLADIMIR, 124 | PENTGEN, 125 | PRES, 126 | SSRV, 127 | POLARB, 128 | JOSH, 129 | CLNT, 130 | ARND, 131 | STLN, 132 | CAML, 133 | EINS, 134 | MUMY, 135 | RMNV, 136 | LUNR, 137 | DNOA, 138 | DNOB, 139 | WWLF, 140 | BRUTE, 141 | AMCV, 142 | HARV, 143 | APOC, 144 | HTNK, 145 | SAPC, 146 | MTNK, 147 | HORV, 148 | CARRIER, 149 | V3, 150 | ZEP, 151 | DRON, 152 | HTK, 153 | DEST, 154 | SUB, 155 | AEGIS, 156 | LCRF, 157 | DRED, 158 | SHAD, 159 | SQD, 160 | DLPH, 161 | SMCV, 162 | TNKD, 163 | HOWI, 164 | TTNK, 165 | HIND, 166 | LTNK, 167 | CMON, 168 | CMIN, 169 | SREF, 170 | HYD, 171 | MGTK, 172 | FV, 173 | VLAD, 174 | DTRUCK, 175 | CRUISE, 176 | YDUM, 177 | PCV, 178 | kCount, 179 | }; 180 | 181 | inline Tech GetTech(std::string_view tech_id) { 182 | static const std::unordered_map kTechTable{ 183 | {"ORCA", Tech::ORCA}, 184 | {"HORNET", Tech::HORNET}, 185 | {"BEAG", Tech::BEAG}, 186 | {"GAPOWR", Tech::GAPOWR}, 187 | {"GAREFN", Tech::GAREFN}, 188 | {"GACNST", Tech::GACNST}, 189 | {"GAPILE", Tech::GAPILE}, 190 | {"GASAND", Tech::GASAND}, 191 | {"GADEPT", Tech::GADEPT}, 192 | {"GATECH", Tech::GATECH}, 193 | {"GAWEAP", Tech::GAWEAP}, 194 | {"CALAB", Tech::CALAB}, 195 | {"NAPOWR", Tech::NAPOWR}, 196 | {"NATECH", Tech::NATECH}, 197 | {"NAHAND", Tech::NAHAND}, 198 | {"GAWALL", Tech::GAWALL}, 199 | {"NARADR", Tech::NARADR}, 200 | {"NAWEAP", Tech::NAWEAP}, 201 | {"NAREFN", Tech::NAREFN}, 202 | {"NAWALL", Tech::NAWALL}, 203 | {"NAPSIS", Tech::NAPSIS}, 204 | {"NALASR", Tech::NALASR}, 205 | {"NASAM", Tech::NASAM}, 206 | {"CASYDN02", Tech::CASYDN02}, 207 | {"GAYARD", Tech::GAYARD}, 208 | {"NAIRON", Tech::NAIRON}, 209 | {"NACNST", Tech::NACNST}, 210 | {"NADEPT", Tech::NADEPT}, 211 | {"GACSPH", Tech::GACSPH}, 212 | {"GAWEAT", Tech::GAWEAT}, 213 | {"CABHUT", Tech::CABHUT}, 214 | {"CAHOSP", Tech::CAHOSP}, 215 | {"TESLA", Tech::TESLA}, 216 | {"NAMISL", Tech::NAMISL}, 217 | {"ATESLA", Tech::ATESLA}, 218 | {"CAMACH", Tech::CAMACH}, 219 | {"CASYDN03", Tech::CASYDN03}, 220 | {"AMMOCRAT", Tech::AMMOCRAT}, 221 | {"NAYARD", Tech::NAYARD}, 222 | {"GASPYSAT", Tech::GASPYSAT}, 223 | {"GAGAP", Tech::GAGAP}, 224 | {"GTGCAN", Tech::GTGCAN}, 225 | {"NANRCT", Tech::NANRCT}, 226 | {"GAPILL", Tech::GAPILL}, 227 | {"NAFLAK", Tech::NAFLAK}, 228 | {"CAOUTP", Tech::CAOUTP}, 229 | {"CATHOSP", Tech::CATHOSP}, 230 | {"CAAIRP", Tech::CAAIRP}, 231 | {"CAOILD", Tech::CAOILD}, 232 | {"NACLON", Tech::NACLON}, 233 | {"GAOREP", Tech::GAOREP}, 234 | {"CANEWY04", Tech::CANEWY04}, 235 | {"CANEWY05", Tech::CANEWY05}, 236 | {"CATECH01", Tech::CATECH01}, 237 | {"CAWASH01", Tech::CAWASH01}, 238 | {"CAMISC01", Tech::CAMISC01}, 239 | {"CAMISC02", Tech::CAMISC02}, 240 | {"CAPARS01", Tech::CAPARS01}, 241 | {"GAAIRC", Tech::GAAIRC}, 242 | {"CAFRMB", Tech::CAFRMB}, 243 | {"AMRADR", Tech::AMRADR}, 244 | {"CAGARD01", Tech::CAGARD01}, 245 | {"MAYAN", Tech::MAYAN}, 246 | {"CAMEX01", Tech::CAMEX01}, 247 | {"CARUS03", Tech::CARUS03}, 248 | {"CAPARS11", Tech::CAPARS11}, 249 | {"CAFARM06", Tech::CAFARM06}, 250 | {"NAPSYB", Tech::NAPSYB}, 251 | {"NAPSYA", Tech::NAPSYA}, 252 | {"CACOLO01", Tech::CACOLO01}, 253 | {"CAMSC13", Tech::CAMSC13}, 254 | {"CASLAB", Tech::CASLAB}, 255 | {"CATRAN03", Tech::CATRAN03}, 256 | {"CAPOWR", Tech::CAPOWR}, 257 | {"CABUNK03", Tech::CABUNK03}, 258 | {"CABUNK04", Tech::CABUNK04}, 259 | {"YACNST", Tech::YACNST}, 260 | {"YAPOWR", Tech::YAPOWR}, 261 | {"YAREFN", Tech::YAREFN}, 262 | {"YABRCK", Tech::YABRCK}, 263 | {"YATECH", Tech::YATECH}, 264 | {"YAWEAP", Tech::YAWEAP}, 265 | {"NABNKR", Tech::NABNKR}, 266 | {"YAGGUN", Tech::YAGGUN}, 267 | {"YAPSYT", Tech::YAPSYT}, 268 | {"NATBNK", Tech::NATBNK}, 269 | {"GAFWLL", Tech::GAFWLL}, 270 | {"YAYARD", Tech::YAYARD}, 271 | {"E1", Tech::E1}, 272 | {"E2", Tech::E2}, 273 | {"SHK", Tech::SHK}, 274 | {"ENGINEER", Tech::ENGINEER}, 275 | {"JUMPJET", Tech::JUMPJET}, 276 | {"GHOST", Tech::GHOST}, 277 | {"YURI", Tech::YURI}, 278 | {"IVAN", Tech::IVAN}, 279 | {"DESO", Tech::DESO}, 280 | {"DOG", Tech::DOG}, 281 | {"CTECH", Tech::CTECH}, 282 | {"CLEG", Tech::CLEG}, 283 | {"SPY", Tech::SPY}, 284 | {"CCOMAND", Tech::CCOMAND}, 285 | {"PTROOP", Tech::PTROOP}, 286 | {"CIVAN", Tech::CIVAN}, 287 | {"YURIPR", Tech::YURIPR}, 288 | {"SNIPE", Tech::SNIPE}, 289 | {"COW", Tech::COW}, 290 | {"ALL", Tech::ALL}, 291 | {"TANY", Tech::TANY}, 292 | {"FLAKT", Tech::FLAKT}, 293 | {"TERROR", Tech::TERROR}, 294 | {"SENGINEER", Tech::SENGINEER}, 295 | {"ADOG", Tech::ADOG}, 296 | {"VLADIMIR", Tech::VLADIMIR}, 297 | {"PENTGEN", Tech::PENTGEN}, 298 | {"PRES", Tech::PRES}, 299 | {"SSRV", Tech::SSRV}, 300 | {"POLARB", Tech::POLARB}, 301 | {"JOSH", Tech::JOSH}, 302 | {"CLNT", Tech::CLNT}, 303 | {"ARND", Tech::ARND}, 304 | {"STLN", Tech::STLN}, 305 | {"CAML", Tech::CAML}, 306 | {"EINS", Tech::EINS}, 307 | {"MUMY", Tech::MUMY}, 308 | {"RMNV", Tech::RMNV}, 309 | {"LUNR", Tech::LUNR}, 310 | {"DNOA", Tech::DNOA}, 311 | {"DNOB", Tech::DNOB}, 312 | {"WWLF", Tech::WWLF}, 313 | {"BRUTE", Tech::BRUTE}, 314 | {"AMCV", Tech::AMCV}, 315 | {"HARV", Tech::HARV}, 316 | {"APOC", Tech::APOC}, 317 | {"HTNK", Tech::HTNK}, 318 | {"SAPC", Tech::SAPC}, 319 | {"MTNK", Tech::MTNK}, 320 | {"HORV", Tech::HORV}, 321 | {"CARRIER", Tech::CARRIER}, 322 | {"V3", Tech::V3}, 323 | {"ZEP", Tech::ZEP}, 324 | {"DRON", Tech::DRON}, 325 | {"HTK", Tech::HTK}, 326 | {"DEST", Tech::DEST}, 327 | {"SUB", Tech::SUB}, 328 | {"AEGIS", Tech::AEGIS}, 329 | {"LCRF", Tech::LCRF}, 330 | {"DRED", Tech::DRED}, 331 | {"SHAD", Tech::SHAD}, 332 | {"SQD", Tech::SQD}, 333 | {"DLPH", Tech::DLPH}, 334 | {"SMCV", Tech::SMCV}, 335 | {"TNKD", Tech::TNKD}, 336 | {"HOWI", Tech::HOWI}, 337 | {"TTNK", Tech::TTNK}, 338 | {"HIND", Tech::HIND}, 339 | {"LTNK", Tech::LTNK}, 340 | {"CMON", Tech::CMON}, 341 | {"CMIN", Tech::CMIN}, 342 | {"SREF", Tech::SREF}, 343 | {"HYD", Tech::HYD}, 344 | {"MGTK", Tech::MGTK}, 345 | {"FV", Tech::FV}, 346 | {"VLAD", Tech::VLAD}, 347 | {"DTRUCK", Tech::DTRUCK}, 348 | {"CRUISE", Tech::CRUISE}, 349 | {"YDUM", Tech::YDUM}, 350 | {"PCV", Tech::PCV}, 351 | }; 352 | if (auto it = kTechTable.find(tech_id); it != kTechTable.end()) { 353 | return it->second; 354 | } else { 355 | return Tech::kUnknown; 356 | } 357 | } 358 | 359 | using TechList = std::array(Tech::kCount)>; 360 | 361 | } // namespace backend 362 | } // namespace yrtr 363 | -------------------------------------------------------------------------------- /src/frontend/desktop/gui.cc: -------------------------------------------------------------------------------- 1 | #include "frontend/desktop/gui.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "base/windows_shit.h" 8 | #define EAT_SHIT_FIRST // prevent linter move windows shit down 9 | #include "base/logging.h" 10 | #include "base/thread.h" 11 | #include "frontend/desktop/config.h" 12 | #include "imgui.h" 13 | 14 | // I'm gonna kick all c_str()s' ass. 15 | namespace ImGui { 16 | template 17 | void Format(const std::format_string& fmt_str, TArgs... args) { 18 | Text(std::format(fmt_str, std::forward(args)...).c_str()); 19 | } 20 | 21 | void Text(const std::string& cpp_str) { 22 | Text(cpp_str.c_str()); 23 | } 24 | 25 | bool Begin(const std::string& name, bool* p_open = (bool*)0, 26 | ImGuiWindowFlags flags = 0) { 27 | return Begin(name.c_str(), p_open, flags); 28 | } 29 | 30 | bool BeginTabItem(const std::string& label, bool* p_open = (bool*)0, 31 | ImGuiTabItemFlags flags = 0) { 32 | return BeginTabItem(label.c_str(), p_open, flags); 33 | } 34 | 35 | bool Button(const std::string& label, const ImVec2& size = ImVec2(0, 0)) { 36 | return Button(label.c_str(), size); 37 | } 38 | 39 | bool Checkbox(const std::string& label, bool* v) { 40 | return Checkbox(label.c_str(), v); 41 | } 42 | 43 | bool Selectable(const std::string& label, bool selected = false, 44 | ImGuiSelectableFlags flags = 0, 45 | const ImVec2& size = ImVec2(0, 0)) { 46 | return Selectable(label.c_str(), selected, flags, size); 47 | } 48 | } // namespace ImGui 49 | 50 | namespace yrtr { 51 | namespace frontend { 52 | 53 | namespace { 54 | inline std::string_view get_log_header() { return "Gui "; } 55 | 56 | static void PushStyle() { 57 | // Customize window style here. 58 | // ImGui::PushStyleColor(id, col); 59 | // ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); 60 | } 61 | 62 | static void PopStyle() { 63 | // Customize window style here. 64 | // ImGui::PopStyleColor(1); 65 | // ImGui::PopStyleVar(1); 66 | } 67 | } // namespace 68 | 69 | Gui::Gui(Lang lang) : lang_(lang) {} 70 | 71 | Gui::~Gui() {} 72 | 73 | void Gui::UpdateState(const State& state) { 74 | DCHECK(IsWithinRendererThread()); 75 | // This should be the only point writing to state. 76 | state_ = state; 77 | } 78 | 79 | void Gui::Render() { 80 | DCHECK(IsWithinRendererThread()); 81 | PushStyle(); 82 | RenderClientArea(); 83 | PopStyle(); 84 | } 85 | 86 | void Gui::RenderClientArea() { 87 | ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); 88 | ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); 89 | 90 | ImGui::Begin(GetGuiStr(GuiLabel::kTitle), nullptr, 91 | ImGuiWindowFlags_NoMove | 92 | ImGuiWindowFlags_NoResize | 93 | ImGuiWindowFlags_NoCollapse | 94 | ImGuiWindowFlags_NoTitleBar | 95 | ImGuiWindowFlags_AlwaysAutoResize); 96 | if (ImGui::BeginTabBar("Assist Tool Tabs")) { 97 | if (ImGui::BeginTabItem(GetGuiStr(GuiLabel::kFilter))) { 98 | RenderTabFilters(); 99 | ImGui::EndTabItem(); 100 | } 101 | if (ImGui::BeginTabItem(GetGuiStr(GuiLabel::kAssist))) { 102 | RenderTabAssists(); 103 | ImGui::EndTabItem(); 104 | } 105 | ImGui::EndTabBar(); 106 | } 107 | ImGui::End(); 108 | } 109 | 110 | void Gui::RenderTabAssists() { 111 | ImGui::Text(GetGuiStr(GuiLabel::kMoney)); 112 | ImGui::SameLine(); // Keep the following item on the same line 113 | ImGui::SetNextItemWidth(100); 114 | static char input[128] = ""; 115 | // Triggered by press enter. 116 | bool triggered = ImGui::InputText( 117 | "##input", input, IM_ARRAYSIZE(input), 118 | ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue); 119 | char* end = nullptr; 120 | uint32_t input_val = std::strtoul(input, &end, 10); 121 | ImGui::SameLine(); 122 | if (ImGui::Button(GetFnStr(FnLabel::kApply)) || triggered) { 123 | auto it = input_cbs_.find(FnLabel::kApply); 124 | if (it != input_cbs_.end()) { 125 | it->second(input_val); 126 | } else { 127 | HLOG_F(WARNING, "Not found handler for label=Apply"); 128 | } 129 | } 130 | for (auto& [label, handler] : btn_cbs_) { 131 | if (ImGui::Button(GetFnStr(label))) { 132 | if (handler != nullptr) { 133 | handler(); 134 | } else { 135 | HLOG_F(WARNING, "Not found handler for label={}", StrFnLabel(label)); 136 | } 137 | } 138 | } 139 | const CheckboxStateMap& ckbox_states = state_.ckbox_states; 140 | for (auto& [label, handler] : ckbox_cbs_) { 141 | bool enable = true; 142 | bool activate = false; 143 | auto it = ckbox_states.find(label); 144 | if (it != ckbox_states.end()) { 145 | enable = it->second.enable; 146 | activate = it->second.activate; 147 | } 148 | ImGui::BeginDisabled(!enable); 149 | if (ImGui::Checkbox(GetFnStr(label), &activate)) { 150 | if (handler != nullptr) { 151 | handler(activate); 152 | } else { 153 | HLOG_F(WARNING, "Not found handler for label={}", StrFnLabel(label)); 154 | } 155 | } 156 | ImGui::EndDisabled(); 157 | } 158 | } 159 | 160 | void Gui::RenderTabFilters() { 161 | float client_width = 162 | ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2; 163 | // Readonly. 164 | const SideMap& selecting_houses = state_.selecting_houses; 165 | // Read and write. 166 | SideMap protected_houses = state_.protected_houses; 167 | ImGui::BeginGroup(); 168 | ImGui::Text(GetGuiStr(GuiLabel::kSelectingHouseList)); 169 | ImGui::SameLine(); 170 | if (ImGui::Button(GetGuiStr(GuiLabel::kAddAll))) { 171 | for (auto& [uniq_id, desc] : selecting_houses) { 172 | protected_houses.try_emplace(uniq_id, desc); 173 | } 174 | } 175 | ImGui::BeginChild("Source", ImVec2(client_width / 2, 0), true, 176 | ImGuiWindowFlags_None); 177 | if (ImGui::BeginListBox("##ListBox", ImVec2(-FLT_MIN, -FLT_MIN))) { 178 | for (auto& [uniq_id, desc] : selecting_houses) { 179 | if (ImGui::Selectable(desc.item_name())) { 180 | protected_houses.try_emplace(uniq_id, desc); 181 | } 182 | } 183 | ImGui::EndListBox(); 184 | } 185 | ImGui::EndChild(); 186 | ImGui::EndGroup(); 187 | 188 | ImGui::SameLine(); 189 | 190 | // Right panel: Destination ListBox 191 | ImGui::BeginGroup(); 192 | ImGui::Text(GetGuiStr(GuiLabel::kProtectedHouseList)); 193 | ImGui::SameLine(); 194 | if (ImGui::Button(GetGuiStr(GuiLabel::kClearAll))) { 195 | protected_houses.clear(); 196 | } 197 | 198 | ImGui::BeginChild("Destination", ImVec2(client_width / 2, 0), true, 199 | ImGuiWindowFlags_None); 200 | if (ImGui::BeginListBox("##ListBox", ImVec2(-FLT_MIN, -FLT_MIN))) { 201 | absl::InlinedVector erased_houses; 202 | for (const SideDesc& desc : std::views::values(protected_houses)) { 203 | if (ImGui::Selectable(desc.item_name())) { 204 | erased_houses.emplace_back(desc); 205 | } 206 | } 207 | for (const SideDesc& desc : erased_houses) { 208 | protected_houses.erase(desc.uniq_id); 209 | } 210 | ImGui::EndListBox(); 211 | } 212 | ImGui::EndChild(); 213 | ImGui::EndGroup(); 214 | // Trigger update data if modified any item. 215 | if (!AreEqual(protected_houses, state_.protected_houses)) { 216 | if (house_list_cb_ != nullptr) { 217 | house_list_cb_(std::move(protected_houses)); 218 | } else { 219 | HLOG_F(WARNING, "Not found handler for house list update"); 220 | } 221 | } 222 | } 223 | 224 | void Gui::Trigger(FnLabel label) const { 225 | DCHECK(IsWithinRendererThread()); 226 | if (label == FnLabel::kInvalid) { 227 | HLOG_F(WARNING, "Invalid label"); 228 | return; 229 | } 230 | // There's have only one input widget now. 231 | DCHECK_LE(input_cbs_.size(), 1u); 232 | auto it_input = input_cbs_.find(label); 233 | if (it_input != input_cbs_.end()) { 234 | // Set credit. 235 | it_input->second(233333); 236 | return; 237 | } 238 | auto it_btn = btn_cbs_.find(label); 239 | if (it_btn != btn_cbs_.end()) { 240 | it_btn->second(); 241 | return; 242 | } 243 | const CheckboxStateMap& ckbox_states = state_.ckbox_states; 244 | auto it_ckbox_state = ckbox_states.find(label); 245 | auto it_ckbox = ckbox_cbs_.find(label); 246 | if (it_ckbox_state != ckbox_states.end() && it_ckbox != ckbox_cbs_.end()) { 247 | bool enable = it_ckbox_state->second.enable; 248 | if (enable) { 249 | bool activate = it_ckbox_state->second.activate; 250 | it_ckbox->second(!activate); 251 | } 252 | return; 253 | } 254 | // Report errors. 255 | if (it_input == input_cbs_.end() && it_btn == btn_cbs_.end() && 256 | it_ckbox == ckbox_cbs_.end()) { 257 | HLOG_F(WARNING, "Not found handler for label={}", StrFnLabel(label)); 258 | return; 259 | } 260 | if (it_ckbox_state == ckbox_states.end()) { 261 | HLOG_F(WARNING, "Not found state for label={}", StrFnLabel(label)); 262 | return; 263 | } 264 | UNREACHABLE(); 265 | } 266 | 267 | void Gui::AddButtonListener(FnLabel label, ButtonHandler cb) { 268 | btn_cbs_.emplace(label, std::move(cb)); 269 | } 270 | 271 | void Gui::AddInputListener(FnLabel label, InputHandler cb) { 272 | input_cbs_.emplace(label, std::move(cb)); 273 | } 274 | 275 | void Gui::AddCheckboxListener(FnLabel label, CheckboxHandler cb) { 276 | ckbox_cbs_.emplace(label, std::move(cb)); 277 | } 278 | 279 | void Gui::AddHouseListListener(HouseListHandler cb) { 280 | house_list_cb_ = std::move(cb); 281 | } 282 | 283 | std::string Gui::GetGuiStr(GuiLabel label) { 284 | const char8_t* gui_str = yrtr::frontend::GetGuiStr(label, lang_); 285 | return std::string(reinterpret_cast(gui_str)); 286 | } 287 | 288 | std::string Gui::GetFnStr(FnLabel label) { 289 | Config* config = Config::instance(); 290 | DCHECK_NOTNULL(config); 291 | return config->GetFnStrWithKey(label); 292 | } 293 | 294 | } // namespace frontend 295 | } // namespace yrtr 296 | -------------------------------------------------------------------------------- /src/protocol/model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #include "base/logging.h" 7 | #include "base/macro.h" 8 | __YRTR_BEGIN_THIRD_PARTY_HEADERS 9 | #include "absl/container/btree_map.h" 10 | #include "absl/container/flat_hash_map.h" 11 | #include "absl/synchronization/mutex.h" 12 | __YRTR_END_THIRD_PARTY_HEADERS 13 | #include "nlohmann/json.hpp" 14 | using json = nlohmann::json; 15 | 16 | namespace yrtr { 17 | // MVC -- model. 18 | 19 | enum class FnLabel { 20 | kInvalid = -1, 21 | // Button 22 | kApply, 23 | kIAMWinner, 24 | kDeleteUnit, 25 | kClearShroud, 26 | kGiveMeABomb, 27 | kUnitLevelUp, 28 | kUnitSpeedUp, 29 | kFastBuild, 30 | kThisIsMine, 31 | // Checkbox 32 | kGod, 33 | kInstBuild, 34 | kUnlimitSuperWeapon, 35 | kInstFire, 36 | kInstTurn, 37 | kRangeToYourBase, 38 | kFireToYourBase, 39 | kFreezeGapGenerator, 40 | kSellTheWorld, 41 | kBuildEveryWhere, 42 | kAutoRepair, 43 | kSocialismMajesty, 44 | kMakeCapturedMine, 45 | kMakeGarrisonedMine, 46 | kInvadeMode, 47 | kUnlimitTech, 48 | kUnlimitFirePower, 49 | kInstChrono, 50 | kSpySpy, 51 | kAdjustGameSpeed, 52 | kCount, 53 | }; 54 | 55 | constexpr std::string_view StrFnLabel(FnLabel label) { 56 | switch (label) { 57 | case FnLabel::kInvalid: return "Invalid"; 58 | // Button 59 | case FnLabel::kApply: return "Apply"; 60 | case FnLabel::kIAMWinner: return "IAMWinner"; 61 | case FnLabel::kDeleteUnit: return "DeleteUnit"; 62 | case FnLabel::kClearShroud: return "ClearShroud"; 63 | case FnLabel::kGiveMeABomb: return "GiveMeABomb"; 64 | case FnLabel::kUnitLevelUp: return "UnitLevelUp"; 65 | case FnLabel::kUnitSpeedUp: return "UnitSpeedUp"; 66 | case FnLabel::kFastBuild: return "FastBuild"; 67 | case FnLabel::kThisIsMine: return "ThisIsMine"; 68 | // Checkbox 69 | case FnLabel::kGod: return "God"; 70 | case FnLabel::kInstBuild: return "InstBuild"; 71 | case FnLabel::kUnlimitSuperWeapon: return "UnlimitSuperWeapon"; 72 | case FnLabel::kInstFire: return "InstFire"; 73 | case FnLabel::kInstTurn: return "InstTurn"; 74 | case FnLabel::kRangeToYourBase: return "RangeToYourBase"; 75 | case FnLabel::kFireToYourBase: return "FireToYourBase"; 76 | case FnLabel::kFreezeGapGenerator: return "FreezeGapGenerator"; 77 | case FnLabel::kSellTheWorld: return "SellTheWorld"; 78 | case FnLabel::kBuildEveryWhere: return "BuildEveryWhere"; 79 | case FnLabel::kAutoRepair: return "AutoRepair"; 80 | case FnLabel::kSocialismMajesty: return "SocialismMajesty"; 81 | case FnLabel::kMakeCapturedMine: return "MakeCapturedMine"; 82 | case FnLabel::kMakeGarrisonedMine: return "MakeGarrisonedMine"; 83 | case FnLabel::kInvadeMode: return "InvadeMode"; 84 | case FnLabel::kUnlimitTech: return "UnlimitTech"; 85 | case FnLabel::kUnlimitFirePower: return "UnlimitFirePower"; 86 | case FnLabel::kInstChrono: return "InstChrono"; 87 | case FnLabel::kSpySpy: return "SpySpy"; 88 | case FnLabel::kAdjustGameSpeed: return "AdjustGameSpeed"; 89 | case FnLabel::kCount: return "Count"; 90 | default: return "unknown"; 91 | } 92 | } 93 | 94 | static constexpr FnLabel StrToFnLabel(std::string_view str) { 95 | // Button 96 | if (str == "kApply") return FnLabel::kApply; 97 | if (str == "kIAMWinner") return FnLabel::kIAMWinner; 98 | if (str == "kDeleteUnit") return FnLabel::kDeleteUnit; 99 | if (str == "kClearShroud") return FnLabel::kClearShroud; 100 | if (str == "kGiveMeABomb") return FnLabel::kGiveMeABomb; 101 | if (str == "kUnitLevelUp") return FnLabel::kUnitLevelUp; 102 | if (str == "kUnitSpeedUp") return FnLabel::kUnitSpeedUp; 103 | if (str == "kFastBuild") return FnLabel::kFastBuild; 104 | if (str == "kThisIsMine") return FnLabel::kThisIsMine; 105 | // Checkbox 106 | if (str == "kGod") return FnLabel::kGod; 107 | if (str == "kInstBuild") return FnLabel::kInstBuild; 108 | if (str == "kUnlimitSuperWeapon") return FnLabel::kUnlimitSuperWeapon; 109 | if (str == "kInstFire") return FnLabel::kInstFire; 110 | if (str == "kInstTurn") return FnLabel::kInstTurn; 111 | if (str == "kRangeToYourBase") return FnLabel::kRangeToYourBase; 112 | if (str == "kFireToYourBase") return FnLabel::kFireToYourBase; 113 | if (str == "kFreezeGapGenerator") return FnLabel::kFreezeGapGenerator; 114 | if (str == "kSellTheWorld") return FnLabel::kSellTheWorld; 115 | if (str == "kBuildEveryWhere") return FnLabel::kBuildEveryWhere; 116 | if (str == "kAutoRepair") return FnLabel::kAutoRepair; 117 | if (str == "kSocialismMajesty") return FnLabel::kSocialismMajesty; 118 | if (str == "kMakeCapturedMine") return FnLabel::kMakeCapturedMine; 119 | if (str == "kMakeGarrisonedMine") return FnLabel::kMakeGarrisonedMine; 120 | if (str == "kInvadeMode") return FnLabel::kInvadeMode; 121 | if (str == "kUnlimitTech") return FnLabel::kUnlimitTech; 122 | if (str == "kUnlimitFirePower") return FnLabel::kUnlimitFirePower; 123 | if (str == "kInstChrono") return FnLabel::kInstChrono; 124 | if (str == "kSpySpy") return FnLabel::kSpySpy; 125 | if (str == "kAdjustGameSpeed") return FnLabel::kAdjustGameSpeed; 126 | return FnLabel::kInvalid; 127 | } 128 | 129 | struct CheckboxState { 130 | bool enable = true; 131 | bool activate = false; 132 | NLOHMANN_DEFINE_TYPE_INTRUSIVE(CheckboxState, enable, activate); 133 | }; 134 | 135 | using UniqId = uint32_t; 136 | 137 | struct SideDesc { 138 | UniqId uniq_id; 139 | std::string name; 140 | NLOHMANN_DEFINE_TYPE_INTRUSIVE(SideDesc, uniq_id, name); 141 | 142 | std::string item_name() const { return name; } 143 | }; 144 | 145 | using CheckboxStateMap = std::unordered_map; 146 | using SideMap = std::map; 147 | 148 | // Only compare keys. 149 | inline bool AreEqual(const SideMap& lhs, const SideMap& rhs) { 150 | return lhs.size() == rhs.size() && 151 | std::equal(lhs.begin(), lhs.end(), rhs.begin(), 152 | [](auto a, auto b) { return a.first == b.first; }); 153 | } 154 | 155 | // Deprecated in websocket. 156 | static constexpr std::string_view kApiGetState = "/state"; 157 | // Deprecated in websocket. 158 | static constexpr std::string_view kApiPostEvent = "/event"; 159 | 160 | // Stored in backend. 161 | struct State { 162 | // Write by controller, read by view. 163 | // Export checkbox states to controller to bind them with the game state 164 | // instead of gui state. 165 | CheckboxStateMap ckbox_states; 166 | // "House" emm..., fine, classic westwood naming convention. Use map to drop 167 | // duplications. 168 | SideMap selecting_houses; 169 | // Read by controller, write by view. 170 | SideMap protected_houses; 171 | NLOHMANN_DEFINE_TYPE_INTRUSIVE(State, ckbox_states, selecting_houses, 172 | protected_houses); 173 | }; 174 | 175 | template 176 | struct Event { 177 | std::string type; 178 | FnLabel label; 179 | T val; 180 | NLOHMANN_DEFINE_TYPE_INTRUSIVE(Event, type, label, val); 181 | }; 182 | 183 | inline std::string MakeGetStateEvent() { 184 | Event event{ 185 | .type = "get_state", 186 | .label = FnLabel::kInvalid, 187 | .val = State{}, 188 | }; 189 | return json(event).dump(); 190 | } 191 | 192 | inline std::string MakeGetStateEvent(State&& state) { 193 | Event event{ 194 | .type = "get_state", 195 | .label = FnLabel::kInvalid, 196 | .val = std::move(state), 197 | }; 198 | return json(event).dump(); 199 | } 200 | 201 | inline Event ParseGetStateEvent(const json& data) { 202 | auto event = data.get>(); 203 | DCHECK_EQ(event.type, "get_state"); 204 | DCHECK_EQ(static_cast(event.label), static_cast(FnLabel::kInvalid)); 205 | return event; 206 | } 207 | 208 | inline std::string MakeInputEvent(FnLabel label, uint32_t val) { 209 | Event event{ 210 | .type = "input", 211 | .label = label, 212 | .val = val, 213 | }; 214 | return json(event).dump(); 215 | } 216 | 217 | inline Event ParseInputEvent(const json& data) { 218 | auto event = data.get>(); 219 | DCHECK_EQ(event.type, "input"); 220 | DCHECK_NE(static_cast(event.label), static_cast(FnLabel::kInvalid)); 221 | return event; 222 | } 223 | 224 | inline std::string MakeButtonEvent(FnLabel label) { 225 | Event event{ 226 | .type = "button", 227 | .label = label, 228 | .val = -1, 229 | }; 230 | return json(event).dump(); 231 | } 232 | 233 | inline Event ParseButtonEvent(const json& data) { 234 | auto event = data.get>(); 235 | DCHECK_EQ(event.type, "button"); 236 | DCHECK_NE(static_cast(event.label), static_cast(FnLabel::kInvalid)); 237 | return event; 238 | } 239 | 240 | inline std::string MakeCheckboxEvent(FnLabel label, bool activate) { 241 | Event event{ 242 | .type = "checkbox", 243 | .label = label, 244 | .val = activate, 245 | }; 246 | return json(event).dump(); 247 | } 248 | 249 | inline Event ParseCheckboxEvent(const json& data) { 250 | auto event = data.get>(); 251 | DCHECK_EQ(event.type, "checkbox"); 252 | DCHECK_NE(static_cast(event.label), static_cast(FnLabel::kInvalid)); 253 | return event; 254 | } 255 | 256 | inline std::string MakeProtectedListEvent(SideMap&& side_map) { 257 | Event event{ 258 | .type = "protected_list", 259 | .label = FnLabel::kInvalid, 260 | .val = std::move(side_map), 261 | }; 262 | return json(event).dump(); 263 | } 264 | 265 | inline Event ParseProtectedListEvent(const json& data) { 266 | auto event = data.get>(); 267 | DCHECK_EQ(event.type, "protected_list"); 268 | DCHECK_EQ(static_cast(event.label), static_cast(FnLabel::kInvalid)); 269 | return event; 270 | } 271 | 272 | } // namespace yrtr 273 | -------------------------------------------------------------------------------- /src/frontend/desktop/gui_context.cc: -------------------------------------------------------------------------------- 1 | #include "frontend/desktop/gui_context.h" 2 | 3 | #include "base/thread.h" 4 | #include "frontend/desktop/config.h" 5 | #include "frontend/desktop/context.h" 6 | #include "frontend/desktop/glfw.h" 7 | #include "imgui.h" 8 | #include "imgui_impl_glfw.h" 9 | #include "imgui_impl_opengl3.h" 10 | #include "imgui_impl_win32.h" 11 | #include "misc/freetype/imgui_freetype.h" 12 | 13 | namespace yrtr { 14 | namespace frontend { 15 | 16 | namespace { 17 | inline std::string_view get_log_header() { return "ImGuiWindow "; } 18 | 19 | void* MemAllocFunc(size_t sz, void*) { return malloc(sz); } 20 | void MemFreeFunc(void* ptr, void*) { free(ptr); } 21 | 22 | // scarlet-light style from ImThemes 23 | static void SetupStyle() { 24 | ImGuiStyle& style = ImGui::GetStyle(); 25 | style.Alpha = 0.9f; 26 | style.DisabledAlpha = 0.6000000238418579f; 27 | style.WindowPadding = ImVec2(8.0f, 8.0f); 28 | style.WindowRounding = 0.0f; 29 | style.WindowBorderSize = 1.0f; 30 | style.WindowMinSize = ImVec2(20.0f, 20.0f); 31 | style.WindowTitleAlign = ImVec2(0.0f, 0.5f); 32 | style.WindowMenuButtonPosition = ImGuiDir_Right; 33 | style.ChildRounding = 0.0f; 34 | style.ChildBorderSize = 1.0f; 35 | style.PopupRounding = 0.0f; 36 | style.PopupBorderSize = 1.0f; 37 | style.FramePadding = ImVec2(5.199999809265137f, 4.099999904632568f); 38 | style.FrameRounding = 0.0f; 39 | style.FrameBorderSize = 0.0f; 40 | style.ItemSpacing = ImVec2(8.0f, 7.599999904632568f); 41 | style.ItemInnerSpacing = ImVec2(8.399999618530273f, 4.300000190734863f); 42 | style.CellPadding = ImVec2(4.300000190734863f, 2.299999952316284f); 43 | style.IndentSpacing = 20.0f; 44 | style.ColumnsMinSpacing = 6.0f; 45 | style.ScrollbarSize = 20.0f; 46 | style.ScrollbarRounding = 0.0f; 47 | style.GrabMinSize = 15.89999961853027f; 48 | style.GrabRounding = 0.0f; 49 | style.TabRounding = 0.0f; 50 | style.TabBorderSize = 0.0f; 51 | style.ColorButtonPosition = ImGuiDir_Right; 52 | style.ButtonTextAlign = ImVec2(0.5f, 0.5f); 53 | style.SelectableTextAlign = ImVec2(0.0f, 0.0f); 54 | 55 | style.Colors[ImGuiCol_Text] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); 56 | style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.6000000238418579f, 0.6000000238418579f, 0.6000000238418579f, 1.0f); 57 | style.Colors[ImGuiCol_WindowBg] = ImVec4(0.9450980424880981f, 0.9411764740943909f, 0.9411764740943909f, 1.0f); 58 | style.Colors[ImGuiCol_ChildBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); 59 | style.Colors[ImGuiCol_PopupBg] = ImVec4(1.0f, 1.0f, 1.0f, 0.9803921580314636f); 60 | style.Colors[ImGuiCol_Border] = ImVec4(0.0f, 0.0f, 0.0f, 0.300000011920929f); 61 | style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); 62 | style.Colors[ImGuiCol_FrameBg] = ImVec4(0.6666666865348816f, 0.6313725709915161f, 0.6313725709915161f, 0.4000000059604645f); 63 | style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 0.4000000059604645f); 64 | style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 0.4000000059604645f); 65 | style.Colors[ImGuiCol_TitleBg] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 0.8078431487083435f); 66 | style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 0.8111587762832642f); 67 | style.Colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.9921568632125854f, 0.929411768913269f, 0.929411768913269f, 0.5098039507865906f); 68 | style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.8588235378265381f, 0.8588235378265381f, 0.8588235378265381f, 1.0f); 69 | style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.9764705896377563f, 0.9764705896377563f, 0.9764705896377563f, 0.5299999713897705f); 70 | style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.686274528503418f, 0.686274528503418f, 0.686274528503418f, 0.800000011920929f); 71 | style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.4862745106220245f, 0.4862745106220245f, 0.4862745106220245f, 0.800000011920929f); 72 | style.Colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.4862745106220245f, 0.4862745106220245f, 0.4862745106220245f, 1.0f); 73 | style.Colors[ImGuiCol_CheckMark] = ImVec4(0.2470588237047195f, 0.01568627543747425f, 0.01568627543747425f, 0.4000000059604645f); 74 | style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.4627451002597809f, 0.05490196123719215f, 0.05490196123719215f, 0.4000000059604645f); 75 | style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.4156862795352936f, 0.1372549086809158f, 0.04313725605607033f, 1.0f); 76 | style.Colors[ImGuiCol_Button] = ImVec4(0.6666666865348816f, 0.6313725709915161f, 0.6313725709915161f, 0.4000000059604645f); 77 | style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 1.0f); 78 | style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 1.0f); 79 | style.Colors[ImGuiCol_Header] = ImVec4(0.4627451002597809f, 0.05490196123719215f, 0.05490196123719215f, 0.1630901098251343f); 80 | style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 0.800000011920929f); 81 | style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 1.0f); 82 | style.Colors[ImGuiCol_Separator] = ImVec4(0.3882353007793427f, 0.3882353007793427f, 0.3882353007793427f, 0.6200000047683716f); 83 | style.Colors[ImGuiCol_SeparatorHovered] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 0.7803921699523926f); 84 | style.Colors[ImGuiCol_SeparatorActive] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 1.0f); 85 | style.Colors[ImGuiCol_ResizeGrip] = ImVec4(0.3490196168422699f, 0.3490196168422699f, 0.3490196168422699f, 0.1700000017881393f); 86 | style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 0.6705882549285889f); 87 | style.Colors[ImGuiCol_ResizeGripActive] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 0.9490196108818054f); 88 | style.Colors[ImGuiCol_Tab] = ImVec4(0.9843137264251709f, 0.6745098233222961f, 0.7764706015586853f, 0.929411768913269f); 89 | style.Colors[ImGuiCol_TabHovered] = ImVec4(0.9764705896377563f, 0.5098039507865906f, 0.4235294163227081f, 0.800000011920929f); 90 | style.Colors[ImGuiCol_TabActive] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 1.0f); 91 | style.Colors[ImGuiCol_TabUnfocused] = ImVec4(0.9176470637321472f, 0.9254902005195618f, 0.9333333373069763f, 0.9861999750137329f); 92 | style.Colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.7411764860153198f, 0.8196078538894653f, 0.9137254953384399f, 1.0f); 93 | style.Colors[ImGuiCol_PlotLines] = ImVec4(0.3882353007793427f, 0.3882353007793427f, 0.3882353007793427f, 1.0f); 94 | style.Colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.0f, 0.4274509847164154f, 0.3490196168422699f, 1.0f); 95 | style.Colors[ImGuiCol_PlotHistogram] = ImVec4(0.8745098114013672f, 0.1450980454683304f, 0.1450980454683304f, 1.0f); 96 | style.Colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.0f, 0.4470588266849518f, 0.0f, 1.0f); 97 | style.Colors[ImGuiCol_TableHeaderBg] = ImVec4(0.9686274528503418f, 0.501960813999176f, 0.501960813999176f, 1.0f); 98 | style.Colors[ImGuiCol_TableBorderStrong] = ImVec4(0.5686274766921997f, 0.5686274766921997f, 0.6392157077789307f, 1.0f); 99 | style.Colors[ImGuiCol_TableBorderLight] = ImVec4(0.6784313917160034f, 0.6784313917160034f, 0.7372549176216125f, 1.0f); 100 | style.Colors[ImGuiCol_TableRowBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); 101 | style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.2980392277240753f, 0.2980392277240753f, 0.2980392277240753f, 0.09000000357627869f); 102 | style.Colors[ImGuiCol_TextSelectedBg] = ImVec4(0.5372549295425415f, 0.47843137383461f, 0.47843137383461f, 0.3490196168422699f); 103 | style.Colors[ImGuiCol_DragDropTarget] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 0.9490196108818054f); 104 | style.Colors[ImGuiCol_NavHighlight] = ImVec4(0.95686274766922f, 0.1607843190431595f, 0.1607843190431595f, 0.800000011920929f); 105 | style.Colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.6980392336845398f, 0.6980392336845398f, 0.6980392336845398f, 0.699999988079071f); 106 | style.Colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.2000000029802322f, 0.2000000029802322f, 0.2000000029802322f, 0.2000000029802322f); 107 | style.Colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.2000000029802322f, 0.2000000029802322f, 0.2000000029802322f, 0.3499999940395355f); 108 | } 109 | } // namespace 110 | 111 | ImGuiWindow::ImGuiWindow(__YRTR_WINDOW_TYPE window) 112 | : window_(window), 113 | hdpi_scale_factor_(1.0f) { 114 | ImGui::SetAllocatorFunctions(MemAllocFunc, MemFreeFunc); 115 | // Setup high dpi scale factor. 116 | if (Config::instance()->enable_dpi_awareness()) { 117 | ImGui_ImplWin32_EnableDpiAwareness(); 118 | hdpi_scale_factor_ = ImGui_ImplWin32_GetDpiScaleForHwnd(GetDesktopWindow()); 119 | hdpi_scale_factor_ = std::max(hdpi_scale_factor_, 0.01f); 120 | } 121 | const float font_size_pixels = 15.0f * hdpi_scale_factor_; 122 | HLOG_F(INFO, "High dpi scale factor={:.2f}", hdpi_scale_factor_); 123 | // Setup Dear ImGui context. 124 | IMGUI_CHECKVERSION(); 125 | ImGui::CreateContext(); 126 | ImGuiIO& io = ImGui::GetIO(); 127 | // Disable imgui.ini 128 | io.IniFilename = NULL; 129 | // Enable Keyboard Controls. 130 | io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; 131 | // Enable Gamepad Controls. 132 | io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; 133 | // Enable Docking. 134 | // io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; 135 | // Allow popup windows outside of the main window. 136 | // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; 137 | DCHECK_NOTNULL(Config::instance()); 138 | bool merge_mode = false; 139 | const fs::path& font_path = Config::instance()->font_path(); 140 | if (font_path != "") { 141 | static const ImWchar ranges[] = { 142 | // 0x0020, 0x00FF, // Basic Latin + Latin Supplement 143 | 0x2000, 0x206F, // General Punctuation 144 | 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana 145 | 0x31F0, 0x31FF, // Katakana Phonetic Extensions 146 | 0xFF00, 0xFFEF, // Half-width characters 147 | 0xFFFD, 0xFFFD, // Invalid 148 | 0x4e00, 0x9FAF, // CJK Ideograms 149 | 0, 150 | }; 151 | ImFont* font = io.Fonts->AddFontFromFileTTF( 152 | font_path.string().c_str(), font_size_pixels, nullptr, ranges); 153 | CHECK_NOTNULL(font); 154 | merge_mode = true; 155 | } 156 | const fs::path& fontex_path = Config::instance()->fontex_path(); 157 | static const ImWchar rangesex[] = {0x1, 0x1FFFF, 0}; 158 | static ImFontConfig cfg; 159 | cfg.OversampleH = cfg.OversampleV = 1; 160 | cfg.MergeMode = merge_mode; 161 | cfg.FontBuilderFlags |= ImGuiFreeTypeBuilderFlags_LoadColor; 162 | ImFont* fontemj = io.Fonts->AddFontFromFileTTF( 163 | fontex_path.string().data(), font_size_pixels, &cfg, rangesex); 164 | CHECK_NOTNULL(fontemj); 165 | io.Fonts->Build(); 166 | // Setup Dear ImGui style 167 | SetupStyle(); 168 | // When viewports are enabled we tweak WindowRounding/WindowBg so platform 169 | // windows can look identical to regular ones. 170 | ImGuiStyle& style = ImGui::GetStyle(); 171 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { 172 | style.WindowRounding = 0.0f; 173 | style.Colors[ImGuiCol_WindowBg].w = 1.0f; 174 | } 175 | style.ScaleAllSizes(hdpi_scale_factor_); 176 | // Setup Platform/Renderer backends 177 | ImGui_ImplGlfw_InitForOpenGL(window, /*install_callbacks*/ true); 178 | ImGui_ImplOpenGL3_Init("#version 130"); 179 | } 180 | 181 | ImGuiWindow::~ImGuiWindow() { 182 | ImGui_ImplOpenGL3_Shutdown(); 183 | ImGui_ImplGlfw_Shutdown(); 184 | ImGui::DestroyContext(); 185 | } 186 | 187 | void ImGuiWindow::UpdateViewport(int width, int height) { 188 | UNREFERENCED_PARAMETER(width); 189 | UNREFERENCED_PARAMETER(height); 190 | hdpi_scale_factor_ = ImGui_ImplWin32_GetDpiScaleForMonitor( 191 | MonitorFromWindow(glfwGetWin32Window(window_), MONITOR_DEFAULTTONEAREST)); 192 | LOG_F(INFO, "High dpi scale factor={:.2f}", hdpi_scale_factor_); 193 | } 194 | 195 | void ImGuiWindow::BeginFrame() { 196 | DCHECK(IsWithinRendererThread()); 197 | // Start the Dear ImGui frame 198 | ImGui_ImplOpenGL3_NewFrame(); 199 | ImGui_ImplGlfw_NewFrame(); 200 | ImGui::NewFrame(); 201 | } 202 | 203 | void ImGuiWindow::EndFrame() { 204 | DCHECK(IsWithinRendererThread()); 205 | ImGui::EndFrame(); 206 | } 207 | 208 | void ImGuiWindow::Render() { 209 | DCHECK(IsWithinRendererThread()); 210 | ImGui::Render(); 211 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 212 | // Update platform. 213 | ImGuiIO& io = ImGui::GetIO(); 214 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { 215 | ImGui::UpdatePlatformWindows(); 216 | ImGui::RenderPlatformWindowsDefault(); 217 | } 218 | } 219 | 220 | } // namespace frontend 221 | } // namespace yrtr 222 | --------------------------------------------------------------------------------