movement_panel.cpp 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /**
  2. * The movement module's interface. Every control saves to disk at once, so a change made here
  3. * survives the next launch with no settings edit.
  4. */
  5. #include "movement_panel.h"
  6. #include <Windows.h>
  7. #include <array>
  8. #include <cstdio>
  9. #include <imgui.h>
  10. #include "../../../core/ui/components/toggle/ui_toggle_component.h"
  11. #include "../../movement/movement_settings_store.h"
  12. namespace sunrise::client::ui::movement {
  13. namespace {
  14. /** Lowest and highest virtual keys the picker scans. Zero is not a key. */
  15. constexpr int kFirstVirtualKey = 1;
  16. constexpr int kLastVirtualKey = 254;
  17. /** Mouse buttons are skipped so a click on the picker cannot bind itself. */
  18. constexpr int kLastMouseKey = 6;
  19. /** Longest key name Windows returns, plus the null. */
  20. constexpr std::size_t kKeyNameCapacity = 64;
  21. enum class CaptureTarget {
  22. none,
  23. teleport,
  24. noclip,
  25. swordSkate,
  26. };
  27. CaptureTarget g_capturing{CaptureTarget::none};
  28. /**
  29. * Names one virtual key for display.
  30. * @param virtualKey Key to name, or zero for no binding.
  31. * @param output Receives the name.
  32. */
  33. void key_name(std::uint32_t virtualKey, std::array<char, kKeyNameCapacity>& output) noexcept {
  34. if (virtualKey == client::movement::kNoKey) {
  35. (void)std::snprintf(output.data(), output.size(), "None");
  36. return;
  37. }
  38. const UINT scanCode = MapVirtualKeyW(virtualKey, MAPVK_VK_TO_VSC);
  39. std::array<wchar_t, kKeyNameCapacity> wide{};
  40. const int written = scanCode != 0 ? GetKeyNameTextW(static_cast<LONG>(scanCode << 16),
  41. wide.data(),
  42. static_cast<int>(wide.size()))
  43. : 0;
  44. if (written <= 0
  45. || WideCharToMultiByte(CP_UTF8,
  46. 0,
  47. wide.data(),
  48. written,
  49. output.data(),
  50. static_cast<int>(output.size() - 1),
  51. nullptr,
  52. nullptr)
  53. <= 0) {
  54. (void)std::snprintf(
  55. output.data(), output.size(), "Key 0x%02X", static_cast<unsigned>(virtualKey));
  56. }
  57. }
  58. /**
  59. * Takes the first key held while the picker is armed.
  60. * @param picked Receives the key, or zero when Escape clears the binding.
  61. * @return True when this frame ended the capture.
  62. */
  63. [[nodiscard]] bool capture_key(std::uint32_t& picked) noexcept {
  64. if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) {
  65. picked = client::movement::kNoKey;
  66. return true;
  67. }
  68. for (int key = kFirstVirtualKey; key <= kLastVirtualKey; ++key) {
  69. if (key <= kLastMouseKey) {
  70. continue;
  71. }
  72. if ((GetAsyncKeyState(key) & 0x8000) != 0) {
  73. picked = static_cast<std::uint32_t>(key);
  74. return true;
  75. }
  76. }
  77. return false;
  78. }
  79. /**
  80. * Draws one key picker while keeping capture ownership exclusive.
  81. * @param id ImGui identity for the button.
  82. * @param target Binding this picker captures.
  83. * @param virtualKey Binding value to display and update.
  84. * @param width Button width.
  85. * @return True when a new binding was captured.
  86. */
  87. [[nodiscard]] bool
  88. key_picker(const char* id, CaptureTarget target, std::uint32_t& virtualKey, float width) noexcept {
  89. ImGui::PushID(id);
  90. if (g_capturing == target) {
  91. if (ImGui::Button("...", ImVec2(width, 0.0F))) {
  92. g_capturing = CaptureTarget::none;
  93. }
  94. ImGui::PopID();
  95. std::uint32_t picked = client::movement::kNoKey;
  96. if (capture_key(picked)) {
  97. virtualKey = picked;
  98. g_capturing = CaptureTarget::none;
  99. return true;
  100. }
  101. return false;
  102. }
  103. std::array<char, kKeyNameCapacity> name{};
  104. key_name(virtualKey, name);
  105. const bool clicked = ImGui::Button(name.data(), ImVec2(width, 0.0F));
  106. ImGui::PopID();
  107. if (clicked) {
  108. g_capturing = target;
  109. }
  110. return false;
  111. }
  112. } // namespace
  113. /** Draws the movement module inside the active Core UI frame. */
  114. void draw() noexcept {
  115. client::movement::Settings settings = client::movement::get();
  116. bool changed = false;
  117. ImGui::TextUnformatted("Teleport");
  118. ImGui::Separator();
  119. ImGui::TextWrapped("Teleports you forward in the facing direction. "
  120. "Cancels vertical momentum.");
  121. ImGui::Spacing();
  122. changed =
  123. core::ui::components::toggle::control("Enabled##teleport", settings.enabled) || changed;
  124. ImGui::Spacing();
  125. // One label column and one control column, so the slider and key buttons share both edges.
  126. const float labelWidth =
  127. ImGui::CalcTextSize("Toggle key").x + ImGui::GetStyle().ItemSpacing.x * 2;
  128. const float controlWidth = ImGui::GetContentRegionAvail().x - labelWidth;
  129. ImGui::AlignTextToFramePadding();
  130. ImGui::TextUnformatted("Distance");
  131. ImGui::SameLine(labelWidth);
  132. ImGui::SetNextItemWidth(controlWidth);
  133. float distance = settings.distance;
  134. if (ImGui::SliderFloat("##distance",
  135. &distance,
  136. client::movement::kMinimumDistance,
  137. client::movement::kMaximumDistance,
  138. "%.0f units")) {
  139. settings.distance = distance;
  140. changed = true;
  141. }
  142. ImGui::Spacing();
  143. ImGui::AlignTextToFramePadding();
  144. ImGui::TextUnformatted("Key");
  145. ImGui::SameLine(labelWidth);
  146. changed = key_picker("teleport_key", CaptureTarget::teleport, settings.virtualKey, controlWidth)
  147. || changed;
  148. ImGui::Spacing();
  149. ImGui::Spacing();
  150. ImGui::TextUnformatted("Noclip");
  151. ImGui::Separator();
  152. ImGui::TextWrapped("Uses native horizontal rigid-body velocity while preserving the game's "
  153. "vertical movement. The bound key turns it on and off in game.");
  154. ImGui::Spacing();
  155. changed =
  156. core::ui::components::toggle::control("Enabled##noclip", settings.noclipEnabled) || changed;
  157. ImGui::Spacing();
  158. ImGui::AlignTextToFramePadding();
  159. ImGui::TextUnformatted("Toggle key");
  160. ImGui::SameLine(labelWidth);
  161. changed =
  162. key_picker("noclip_key", CaptureTarget::noclip, settings.noclipToggleKey, controlWidth)
  163. || changed;
  164. ImGui::Spacing();
  165. ImGui::Spacing();
  166. ImGui::TextUnformatted("Sword Skate Fix");
  167. ImGui::Separator();
  168. ImGui::TextWrapped("A sword's air attack throws you forward, and a glide started while that "
  169. "throw is still carrying you keeps the speed. The client refuses to start a "
  170. "glide during the throw. This clears that refusal on the tick you press "
  171. "jump, and the client's own glide runs from there.");
  172. ImGui::Spacing();
  173. changed =
  174. core::ui::components::toggle::control("Enabled##sword_skate", settings.swordSkateEnabled)
  175. || changed;
  176. ImGui::Spacing();
  177. ImGui::AlignTextToFramePadding();
  178. ImGui::TextUnformatted("Jump key");
  179. ImGui::SameLine(labelWidth);
  180. changed =
  181. key_picker(
  182. "sword_skate_key", CaptureTarget::swordSkate, settings.swordSkateJumpKey, controlWidth)
  183. || changed;
  184. ImGui::TextWrapped("Must match the key the game jumps on. Nothing happens on any other key.");
  185. if (changed && !client::movement::publish(settings)) {
  186. ImGui::Spacing();
  187. ImGui::TextUnformatted("value out of range, not saved");
  188. }
  189. }
  190. } // namespace sunrise::client::ui::movement