#include "poke.h" #include #include #include #include #include "games/nost/io.h" #include "cfg/api.h" #include "rawinput/rawinput.h" #include "misc/eamuse.h" #include "touch/native/inject.h" #include "touch/native/nativetouchhook.h" #include "util/logging.h" namespace games::nost::poke { static std::thread *THREAD = nullptr; static std::atomic THREAD_RUNNING { false }; // positions use game-client pixels, matching the legacy client-sized touch window static const std::unordered_map> NOST_POKE_KEYPAD { {EAM_IO_KEYPAD_0, {760, 440}}, {EAM_IO_KEYPAD_1, {760, 385}}, {EAM_IO_KEYPAD_2, {820, 385}}, {EAM_IO_KEYPAD_3, {880, 385}}, {EAM_IO_KEYPAD_4, {760, 325}}, {EAM_IO_KEYPAD_5, {820, 325}}, {EAM_IO_KEYPAD_6, {880, 325}}, {EAM_IO_KEYPAD_7, {760, 265}}, {EAM_IO_KEYPAD_8, {820, 265}}, {EAM_IO_KEYPAD_9, {880, 265}}, {EAM_IO_KEYPAD_00, {820, 440}}, // erase button {EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button }; static const std::unordered_map< games::nost::Buttons::Button, std::pair> NOST_POKE_BUTTONS { {games::nost::Buttons::Button::PokeConfirm, {1100, 525}}, {games::nost::Buttons::Button::PokeBack, {340, 100}}, {games::nost::Buttons::Button::PokeSong1, {450, 190}}, {games::nost::Buttons::Button::PokeSong2, {450, 330}}, {games::nost::Buttons::Button::PokeSong3, {450, 480}}, {games::nost::Buttons::Button::PokeSong4, {840, 190}}, {games::nost::Buttons::Button::PokeSong5, {840, 330}}, {games::nost::Buttons::Button::PokeSong6, {840, 480}}, {games::nost::Buttons::Button::PokeDifficulty1, {820, 240}}, {games::nost::Buttons::Button::PokeDifficulty2, {820, 320}}, {games::nost::Buttons::Button::PokeDifficulty3, {820, 410}}, {games::nost::Buttons::Button::PokeDifficulty4, {820, 490}}, }; static bool inject_poke_position(POINT position, bool down) { return nativetouch::inject::inject_synthetic_touch(position, down); } void enable() { // check if already running if (THREAD) { return; } if (!nativetouch::is_hooked()) { log_warning("poke", "Nostalgia poke requires native touch; disabled"); return; } // create new thread THREAD_RUNNING = true; THREAD = new std::thread([] { const int swipe_anim_total_frames = 6; const int swipe_anim_y = 300; const int swipe_next_page_x_begin = 1120; const int swipe_next_page_x_end = 1120 - 120; const int swipe_prev_page_x_begin = 280 - 120; const int swipe_prev_page_x_end = 280; int next_page_anim_index = -1; int prev_page_anim_index = -1; bool contact_active = false; bool release_pending = false; POINT last_position {}; std::unordered_map button_states; uint16_t last_keypad_state = 0; // log log_info("poke", "enabled"); // set variable to false to stop while (THREAD_RUNNING) { if (release_pending) { if (contact_active) { inject_poke_position(last_position, false); } contact_active = false; release_pending = false; } auto &buttons = games::nost::get_buttons(); std::optional touch_position; bool release_after_touch = false; const auto button_triggered = [&](games::nost::Buttons::Button button) { const auto pressed = GameAPI::Buttons::getState(RI_MGR, buttons.at(button)); const auto triggered = pressed && !button_states[button]; button_states[button] = pressed; return triggered; }; std::optional triggered_touch_position; for (const auto& it : NOST_POKE_BUTTONS) { if (button_triggered(it.first) && !triggered_touch_position.has_value()) { triggered_touch_position = POINT { it.second.first, it.second.second, }; } } const auto next_triggered = button_triggered(games::nost::Buttons::PokeNextPage); const auto prev_triggered = button_triggered(games::nost::Buttons::PokePrevPage); const auto keypad_state = eamuse_get_keypad_state(0); if (!triggered_touch_position.has_value()) { for (const auto& it : NOST_POKE_KEYPAD) { const auto mask = static_cast(1 << it.first); if ((keypad_state & mask) && !(last_keypad_state & mask)) { triggered_touch_position = POINT { it.second.first, it.second.second, }; break; } } } last_keypad_state = keypad_state; if (0 <= next_page_anim_index) { const auto delta = (swipe_next_page_x_end - swipe_next_page_x_begin) * (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames; touch_position = POINT { swipe_next_page_x_begin + delta, swipe_anim_y, }; next_page_anim_index--; if (next_page_anim_index < 0) { release_after_touch = true; } } else if (0 <= prev_page_anim_index) { const auto delta = (swipe_prev_page_x_end - swipe_prev_page_x_begin) * (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames; touch_position = POINT { swipe_prev_page_x_begin + delta, swipe_anim_y, }; prev_page_anim_index--; if (prev_page_anim_index < 0) { release_after_touch = true; } } else { touch_position = triggered_touch_position; release_after_touch = touch_position.has_value(); // start animations for next frame if (!touch_position.has_value()) { if (next_triggered) { next_page_anim_index = swipe_anim_total_frames; } if (prev_triggered) { prev_page_anim_index = swipe_anim_total_frames; } } } if (touch_position.has_value() && inject_poke_position(*touch_position, true)) { contact_active = true; last_position = *touch_position; } if (release_after_touch && contact_active) { release_pending = true; } // slow down std::this_thread::sleep_for(std::chrono::milliseconds(30)); } if (contact_active) { inject_poke_position(last_position, false); } return nullptr; }); } void disable() { if (!THREAD) { return; } // stop old thread THREAD_RUNNING = false; THREAD->join(); // delete thread delete THREAD; THREAD = nullptr; // log log_info("poke", "disabled"); } }