teleport_settings_store.cpp 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. // Runtime-safe movement settings persisted beside the module after every interface change.
  2. #include "teleport_settings_store.h"
  3. #include <Windows.h>
  4. #include <array>
  5. #include <cstddef>
  6. #include <cstdio>
  7. #include <cstdlib>
  8. #include <string_view>
  9. #include "../../core/filesystem/path.h"
  10. #include "../../core/logging/log.h"
  11. namespace sunrise::client::teleport {
  12. namespace {
  13. /** The module-owned configuration file, beside the generated settings and logs. */
  14. constexpr std::wstring_view kFileSuffix = L"\\teleport.json";
  15. /** The document is a few scalars, so one small buffer covers both reading and writing. */
  16. constexpr std::size_t kFileCapacity = 512;
  17. /** Longest scalar accepted from the file. Anything longer is malformed rather than large. */
  18. constexpr std::size_t kScalarCapacity = 32;
  19. /** Highest Windows virtual-key code, so a stored binding cannot name a key that cannot exist. */
  20. constexpr std::uint32_t kMaximumVirtualKey = 254;
  21. SRWLOCK g_lock{SRWLOCK_INIT};
  22. Settings g_settings{};
  23. core::path::Buffer g_path{};
  24. bool g_pathResolved{};
  25. /** @param settings Candidate configuration. @return True when every field is in range. */
  26. [[nodiscard]] bool valid(const Settings& settings) noexcept {
  27. return settings.distance >= kMinimumDistance && settings.distance <= kMaximumDistance
  28. && settings.virtualKey <= kMaximumVirtualKey
  29. && settings.noclipToggleKey <= kMaximumVirtualKey;
  30. }
  31. /** @param reason Key naming the step that failed. */
  32. void report_fail(const char* reason) noexcept {
  33. std::array<char, 96> line{};
  34. const int written = std::snprintf(
  35. line.data(), line.size(), "ev=teleport stage=store result=fail reason=%s", reason);
  36. if (written > 0) {
  37. core::log::write(core::log::Channel::client,
  38. core::log::Level::warn,
  39. {line.data(), static_cast<std::size_t>(written)});
  40. }
  41. }
  42. /**
  43. * Finds one key's raw scalar text.
  44. * @param text Whole document.
  45. * @param key Quoted key to locate.
  46. * @param output Receives the text between the colon and the next separator.
  47. * @return True when the key exists and carries a non-empty value.
  48. */
  49. [[nodiscard]] bool
  50. scalar_for(std::string_view text, std::string_view key, std::string_view& output) noexcept {
  51. const std::size_t at = text.find(key);
  52. if (at == std::string_view::npos) {
  53. return false;
  54. }
  55. const std::size_t colon = text.find(':', at + key.size());
  56. if (colon == std::string_view::npos) {
  57. return false;
  58. }
  59. std::size_t begin = colon + 1;
  60. while (begin < text.size() && (text[begin] == ' ' || text[begin] == '\t')) {
  61. ++begin;
  62. }
  63. std::size_t end = begin;
  64. while (end < text.size() && text[end] != ',' && text[end] != '}' && text[end] != '\n'
  65. && text[end] != '\r') {
  66. ++end;
  67. }
  68. output = text.substr(begin, end - begin);
  69. return !output.empty();
  70. }
  71. /**
  72. * Copies one scalar into null-terminated storage the C conversions require.
  73. * @param value Scalar text taken from the document.
  74. * @param output Receives the terminated copy.
  75. * @return True when the scalar fits.
  76. */
  77. [[nodiscard]] bool terminated(std::string_view value,
  78. std::array<char, kScalarCapacity>& output) noexcept {
  79. if (value.size() >= output.size()) {
  80. return false;
  81. }
  82. for (std::size_t index = 0; index < value.size(); ++index) {
  83. output[index] = value[index];
  84. }
  85. output[value.size()] = '\0';
  86. return true;
  87. }
  88. /**
  89. * Layers one document over the current defaults. A missing or malformed key keeps its default,
  90. * so a hand-edited file cannot stop the module loading.
  91. * @param text Whole document.
  92. * @param output Receives the parsed configuration.
  93. */
  94. void parse(std::string_view text, Settings& output) noexcept {
  95. std::string_view scalar;
  96. if (scalar_for(text, "\"enabled\"", scalar)) {
  97. output.enabled = scalar.starts_with("true");
  98. }
  99. std::array<char, kScalarCapacity> buffer{};
  100. if (scalar_for(text, "\"distance\"", scalar) && terminated(scalar, buffer)) {
  101. output.distance = std::strtof(buffer.data(), nullptr);
  102. }
  103. if (scalar_for(text, "\"virtual_key\"", scalar) && terminated(scalar, buffer)) {
  104. output.virtualKey = static_cast<std::uint32_t>(std::strtoul(buffer.data(), nullptr, 0));
  105. }
  106. if (scalar_for(text, "\"noclip_enabled\"", scalar)) {
  107. output.noclipEnabled = scalar.starts_with("true");
  108. }
  109. if (scalar_for(text, "\"noclip_toggle_key\"", scalar) && terminated(scalar, buffer)) {
  110. output.noclipToggleKey =
  111. static_cast<std::uint32_t>(std::strtoul(buffer.data(), nullptr, 0));
  112. }
  113. }
  114. /**
  115. * Writes the whole document. It is small enough that a complete rewrite is the simplest
  116. * correct save, which the shared settings file is not.
  117. * @param settings Configuration to store.
  118. * @return True when every byte reached the file.
  119. */
  120. [[nodiscard]] bool store(const Settings& settings) noexcept {
  121. if (!g_pathResolved) {
  122. return false;
  123. }
  124. std::array<char, kFileCapacity> document{};
  125. const int size = std::snprintf(document.data(),
  126. document.size(),
  127. "{\n \"enabled\": %s,\n \"distance\": %.3f,\n"
  128. " \"virtual_key\": %u,\n"
  129. " \"noclip_enabled\": %s,\n"
  130. " \"noclip_toggle_key\": %u\n}\n",
  131. settings.enabled ? "true" : "false",
  132. static_cast<double>(settings.distance),
  133. static_cast<unsigned>(settings.virtualKey),
  134. settings.noclipEnabled ? "true" : "false",
  135. static_cast<unsigned>(settings.noclipToggleKey));
  136. if (size <= 0) {
  137. return false;
  138. }
  139. const HANDLE file = CreateFileW(g_path.chars.data(),
  140. GENERIC_WRITE,
  141. 0,
  142. nullptr,
  143. CREATE_ALWAYS,
  144. FILE_ATTRIBUTE_NORMAL,
  145. nullptr);
  146. if (file == INVALID_HANDLE_VALUE) {
  147. return false;
  148. }
  149. DWORD written = 0;
  150. bool complete =
  151. WriteFile(file, document.data(), static_cast<DWORD>(size), &written, nullptr) != FALSE
  152. && written == static_cast<DWORD>(size);
  153. complete = CloseHandle(file) != FALSE && complete;
  154. return complete;
  155. }
  156. /** Reads the configuration file into the active settings when one exists. */
  157. void load() noexcept {
  158. const HANDLE file = CreateFileW(g_path.chars.data(),
  159. GENERIC_READ,
  160. FILE_SHARE_READ,
  161. nullptr,
  162. OPEN_EXISTING,
  163. FILE_ATTRIBUTE_NORMAL,
  164. nullptr);
  165. if (file == INVALID_HANDLE_VALUE) {
  166. return;
  167. }
  168. std::array<char, kFileCapacity> buffer{};
  169. DWORD read = 0;
  170. const bool readOk =
  171. ReadFile(file, buffer.data(), static_cast<DWORD>(buffer.size() - 1), &read, nullptr)
  172. != FALSE;
  173. (void)CloseHandle(file);
  174. if (!readOk || read == 0) {
  175. return;
  176. }
  177. Settings parsed{};
  178. parse(std::string_view(buffer.data(), read), parsed);
  179. if (!valid(parsed)) {
  180. report_fail("range");
  181. return;
  182. }
  183. g_settings = parsed;
  184. }
  185. } // namespace
  186. /** Resolves the configuration file and loads it when one exists. */
  187. void initialize(void* module) noexcept {
  188. AcquireSRWLockExclusive(&g_lock);
  189. g_settings = Settings{};
  190. g_pathResolved =
  191. core::path::artifact_directory(module, g_path) && core::path::append(g_path, kFileSuffix);
  192. if (g_pathResolved) {
  193. load();
  194. } else {
  195. report_fail("path");
  196. }
  197. ReleaseSRWLockExclusive(&g_lock);
  198. }
  199. /** Drops the runtime configuration and the resolved file path. */
  200. void shutdown() noexcept {
  201. AcquireSRWLockExclusive(&g_lock);
  202. g_settings = Settings{};
  203. g_path = core::path::Buffer{};
  204. g_pathResolved = false;
  205. ReleaseSRWLockExclusive(&g_lock);
  206. }
  207. /** @return One lock-consistent copy of the current configuration. */
  208. Settings get() noexcept {
  209. AcquireSRWLockShared(&g_lock);
  210. const Settings snapshot = g_settings;
  211. ReleaseSRWLockShared(&g_lock);
  212. return snapshot;
  213. }
  214. /** Publishes one configuration and writes it straight to disk. */
  215. bool publish(const Settings& settings) noexcept {
  216. if (!valid(settings)) {
  217. return false;
  218. }
  219. AcquireSRWLockExclusive(&g_lock);
  220. g_settings = settings;
  221. const bool stored = store(settings);
  222. ReleaseSRWLockExclusive(&g_lock);
  223. if (!stored) {
  224. report_fail("write");
  225. }
  226. return true;
  227. }
  228. } // namespace sunrise::client::teleport