gemba 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. checksums.yaml +4 -4
  2. data/THIRD_PARTY_NOTICES +37 -2
  3. data/assets/placeholder_boxart.png +0 -0
  4. data/bin/gemba +2 -2
  5. data/ext/gemba/extconf.rb +23 -1
  6. data/ext/gemba/gemba_ext.c +436 -2
  7. data/ext/gemba/gemba_ext.h +2 -0
  8. data/gemba.gemspec +5 -3
  9. data/lib/gemba/achievements/achievement.rb +23 -0
  10. data/lib/gemba/achievements/backend.rb +186 -0
  11. data/lib/gemba/achievements/cache.rb +70 -0
  12. data/lib/gemba/achievements/credentials_presenter.rb +142 -0
  13. data/lib/gemba/achievements/fake_backend.rb +205 -0
  14. data/lib/gemba/achievements/null_backend.rb +11 -0
  15. data/lib/gemba/achievements/offline_backend.rb +168 -0
  16. data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
  19. data/lib/gemba/achievements.rb +19 -0
  20. data/lib/gemba/achievements_window.rb +556 -0
  21. data/lib/gemba/app_controller.rb +1015 -0
  22. data/lib/gemba/bios.rb +54 -0
  23. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  24. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  25. data/lib/gemba/boxart_fetcher.rb +79 -0
  26. data/lib/gemba/bus_emitter.rb +13 -0
  27. data/lib/gemba/child_window.rb +24 -1
  28. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  29. data/lib/gemba/cli/commands/decode.rb +154 -0
  30. data/lib/gemba/cli/commands/patch.rb +78 -0
  31. data/lib/gemba/cli/commands/play.rb +78 -0
  32. data/lib/gemba/cli/commands/record.rb +114 -0
  33. data/lib/gemba/cli/commands/replay.rb +161 -0
  34. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  35. data/lib/gemba/cli/commands/version.rb +22 -0
  36. data/lib/gemba/cli.rb +52 -364
  37. data/lib/gemba/config.rb +134 -1
  38. data/lib/gemba/data/gb_games.json +1 -0
  39. data/lib/gemba/data/gb_md5.json +1 -0
  40. data/lib/gemba/data/gba_games.json +1 -0
  41. data/lib/gemba/data/gba_md5.json +1 -0
  42. data/lib/gemba/data/gbc_games.json +1 -0
  43. data/lib/gemba/data/gbc_md5.json +1 -0
  44. data/lib/gemba/emulator_frame.rb +1060 -0
  45. data/lib/gemba/event_bus.rb +48 -0
  46. data/lib/gemba/frame_stack.rb +60 -0
  47. data/lib/gemba/game_index.rb +84 -0
  48. data/lib/gemba/game_picker_frame.rb +268 -0
  49. data/lib/gemba/gamepad_map.rb +103 -0
  50. data/lib/gemba/headless.rb +6 -5
  51. data/lib/gemba/headless_player.rb +33 -3
  52. data/lib/gemba/help_window.rb +61 -0
  53. data/lib/gemba/hotkey_map.rb +3 -1
  54. data/lib/gemba/input_recorder.rb +107 -0
  55. data/lib/gemba/input_replayer.rb +119 -0
  56. data/lib/gemba/keyboard_map.rb +90 -0
  57. data/lib/gemba/locales/en.yml +97 -5
  58. data/lib/gemba/locales/ja.yml +97 -5
  59. data/lib/gemba/main_window.rb +56 -0
  60. data/lib/gemba/modal_stack.rb +81 -0
  61. data/lib/gemba/patcher_window.rb +223 -0
  62. data/lib/gemba/platform/gb.rb +21 -0
  63. data/lib/gemba/platform/gba.rb +21 -0
  64. data/lib/gemba/platform/gbc.rb +23 -0
  65. data/lib/gemba/platform.rb +20 -0
  66. data/lib/gemba/platform_open.rb +19 -0
  67. data/lib/gemba/recorder.rb +4 -3
  68. data/lib/gemba/replay_player.rb +691 -0
  69. data/lib/gemba/rom_info.rb +57 -0
  70. data/lib/gemba/rom_info_window.rb +16 -3
  71. data/lib/gemba/rom_library.rb +106 -0
  72. data/lib/gemba/rom_overrides.rb +47 -0
  73. data/lib/gemba/rom_patcher/bps.rb +161 -0
  74. data/lib/gemba/rom_patcher/ips.rb +101 -0
  75. data/lib/gemba/rom_patcher/ups.rb +118 -0
  76. data/lib/gemba/rom_patcher.rb +109 -0
  77. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  78. data/lib/gemba/runtime.rb +59 -26
  79. data/lib/gemba/save_state_manager.rb +4 -7
  80. data/lib/gemba/save_state_picker.rb +17 -4
  81. data/lib/gemba/session_logger.rb +64 -0
  82. data/lib/gemba/settings/audio_tab.rb +77 -0
  83. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  84. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  85. data/lib/gemba/settings/paths.rb +11 -0
  86. data/lib/gemba/settings/recording_tab.rb +83 -0
  87. data/lib/gemba/settings/save_states_tab.rb +91 -0
  88. data/lib/gemba/settings/system_tab.rb +362 -0
  89. data/lib/gemba/settings/video_tab.rb +318 -0
  90. data/lib/gemba/settings_window.rb +162 -1036
  91. data/lib/gemba/version.rb +1 -1
  92. data/lib/gemba/virtual_keyboard.rb +19 -0
  93. data/lib/gemba.rb +2 -12
  94. data/test/achievements_window/test_bulk_sync.rb +218 -0
  95. data/test/achievements_window/test_bus_events.rb +125 -0
  96. data/test/achievements_window/test_close_confirmation.rb +201 -0
  97. data/test/achievements_window/test_initial_state.rb +164 -0
  98. data/test/achievements_window/test_sorting.rb +227 -0
  99. data/test/achievements_window/test_tree_rendering.rb +133 -0
  100. data/test/fixtures/fake_bios.bin +0 -0
  101. data/test/fixtures/pong.gba +0 -0
  102. data/test/fixtures/test.gb +0 -0
  103. data/test/fixtures/test.gbc +0 -0
  104. data/test/fixtures/test_quicksave.ss +0 -0
  105. data/test/screenshots/no_focus.png +0 -0
  106. data/test/shared/teek_test_worker.rb +17 -1
  107. data/test/shared/tk_test_helper.rb +91 -4
  108. data/test/support/achievements_window_helpers.rb +18 -0
  109. data/test/support/fake_core.rb +25 -0
  110. data/test/support/fake_ra_runtime.rb +74 -0
  111. data/test/support/fake_requester.rb +68 -0
  112. data/test/support/player_helpers.rb +20 -5
  113. data/test/test_achievement.rb +32 -0
  114. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  115. data/test/test_bios.rb +123 -0
  116. data/test/test_boxart_fetcher.rb +150 -0
  117. data/test/test_cli.rb +17 -265
  118. data/test/test_cli_config.rb +64 -0
  119. data/test/test_cli_decode.rb +97 -0
  120. data/test/test_cli_patch.rb +58 -0
  121. data/test/test_cli_play.rb +213 -0
  122. data/test/test_cli_ra.rb +175 -0
  123. data/test/test_cli_record.rb +69 -0
  124. data/test/test_cli_replay.rb +72 -0
  125. data/test/test_cli_sync_requester.rb +152 -0
  126. data/test/test_cli_version.rb +27 -0
  127. data/test/test_config.rb +2 -3
  128. data/test/test_config_ra.rb +69 -0
  129. data/test/test_core.rb +62 -1
  130. data/test/test_credentials_presenter.rb +192 -0
  131. data/test/test_event_bus.rb +100 -0
  132. data/test/test_fake_backend_achievements.rb +130 -0
  133. data/test/test_fake_backend_auth.rb +68 -0
  134. data/test/test_game_index.rb +77 -0
  135. data/test/test_game_picker_frame.rb +310 -0
  136. data/test/test_gamepad_map.rb +1 -3
  137. data/test/test_headless_player.rb +17 -3
  138. data/test/test_help_window.rb +82 -0
  139. data/test/test_hotkey_map.rb +22 -1
  140. data/test/test_input_recorder.rb +179 -0
  141. data/test/test_input_replay_determinism.rb +113 -0
  142. data/test/test_input_replayer.rb +162 -0
  143. data/test/test_keyboard_map.rb +1 -3
  144. data/test/test_libretro_backend.rb +41 -0
  145. data/test/test_locale.rb +1 -1
  146. data/test/test_logging.rb +123 -0
  147. data/test/test_null_backend.rb +42 -0
  148. data/test/test_offline_backend.rb +116 -0
  149. data/test/test_overlay_renderer.rb +1 -1
  150. data/test/test_platform.rb +149 -0
  151. data/test/test_ra_backend.rb +313 -0
  152. data/test/test_ra_backend_unlock_gate.rb +56 -0
  153. data/test/test_recorder.rb +0 -3
  154. data/test/test_replay_player.rb +316 -0
  155. data/test/test_rom_info.rb +149 -0
  156. data/test/test_rom_overrides.rb +86 -0
  157. data/test/test_rom_patcher.rb +382 -0
  158. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  159. data/test/test_save_state_manager.rb +2 -4
  160. data/test/test_settings_audio.rb +107 -0
  161. data/test/test_settings_hotkeys.rb +83 -66
  162. data/test/test_settings_recording.rb +49 -0
  163. data/test/test_settings_save_states.rb +97 -0
  164. data/test/test_settings_system.rb +133 -0
  165. data/test/test_settings_video.rb +450 -0
  166. data/test/test_settings_window.rb +76 -507
  167. data/test/test_tip_service.rb +6 -6
  168. data/test/test_toast_overlay.rb +1 -1
  169. data/test/test_virtual_events.rb +156 -0
  170. data/test/test_virtual_keyboard.rb +1 -1
  171. data/vendor/rcheevos/CHANGELOG.md +495 -0
  172. data/vendor/rcheevos/LICENSE +21 -0
  173. data/vendor/rcheevos/Package.swift +33 -0
  174. data/vendor/rcheevos/README.md +67 -0
  175. data/vendor/rcheevos/include/module.modulemap +70 -0
  176. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  177. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  178. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  179. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  180. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  181. data/vendor/rcheevos/include/rc_client.h +877 -0
  182. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  183. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  184. data/vendor/rcheevos/include/rc_error.h +59 -0
  185. data/vendor/rcheevos/include/rc_export.h +100 -0
  186. data/vendor/rcheevos/include/rc_hash.h +200 -0
  187. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  188. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  189. data/vendor/rcheevos/include/rc_util.h +51 -0
  190. data/vendor/rcheevos/include/rcheevos.h +8 -0
  191. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  192. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  193. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  194. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  197. data/vendor/rcheevos/src/rc_client.c +6941 -0
  198. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  199. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  200. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  201. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  202. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  203. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  204. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  205. data/vendor/rcheevos/src/rc_compat.c +251 -0
  206. data/vendor/rcheevos/src/rc_compat.h +121 -0
  207. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  208. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  209. data/vendor/rcheevos/src/rc_util.c +199 -0
  210. data/vendor/rcheevos/src/rc_version.c +11 -0
  211. data/vendor/rcheevos/src/rc_version.h +32 -0
  212. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  213. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  214. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  215. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  216. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  217. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  218. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  219. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  220. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  221. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  222. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  223. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  224. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  225. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  226. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  227. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  228. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  229. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  230. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  231. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  232. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  233. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  234. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  235. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  236. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  237. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  238. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  239. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  240. data/vendor/rcheevos/test/libretro.h +205 -0
  241. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  242. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  243. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  244. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  246. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  247. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  248. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  249. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  250. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  251. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  252. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  253. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  254. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  255. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  256. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  257. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  258. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  259. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  260. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  261. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  262. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  263. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  264. data/vendor/rcheevos/test/rhash/data.c +657 -0
  265. data/vendor/rcheevos/test/rhash/data.h +32 -0
  266. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  267. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  268. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  269. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  270. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  271. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  272. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  273. data/vendor/rcheevos/test/test.c +113 -0
  274. data/vendor/rcheevos/test/test_framework.h +205 -0
  275. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  276. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  277. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  278. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  279. data/vendor/rcheevos/test/test_types.natvis +9 -0
  280. data/vendor/rcheevos/validator/validator.c +658 -0
  281. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  282. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  283. metadata +274 -11
  284. data/lib/gemba/input_mappings.rb +0 -214
  285. data/lib/gemba/player.rb +0 -1525
@@ -0,0 +1,901 @@
1
+ #include "rc_api_runtime.h"
2
+ #include "rc_api_common.h"
3
+
4
+ #include "rc_runtime.h"
5
+ #include "rc_runtime_types.h"
6
+ #include "../rc_compat.h"
7
+ #include "../rhash/md5.h"
8
+
9
+ #include <stdlib.h>
10
+ #include <stdio.h>
11
+ #include <string.h>
12
+
13
+ /* --- Resolve Hash --- */
14
+
15
+ int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) {
16
+ return rc_api_init_resolve_hash_request_hosted(request, api_params, &g_host);
17
+ }
18
+
19
+ int rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request,
20
+ const rc_api_resolve_hash_request_t* api_params,
21
+ const rc_api_host_t* host) {
22
+ rc_api_url_builder_t builder;
23
+
24
+ rc_api_url_build_dorequest_url(request, host);
25
+
26
+ if (!api_params->game_hash || !*api_params->game_hash)
27
+ return RC_INVALID_STATE;
28
+
29
+ rc_url_builder_init(&builder, &request->buffer, 48);
30
+ rc_url_builder_append_str_param(&builder, "r", "gameid");
31
+ rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
32
+ request->post_data = rc_url_builder_finalize(&builder);
33
+ request->content_type = RC_CONTENT_TYPE_URLENCODED;
34
+
35
+ return builder.result;
36
+ }
37
+
38
+ int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) {
39
+ rc_api_server_response_t response_obj;
40
+
41
+ memset(&response_obj, 0, sizeof(response_obj));
42
+ response_obj.body = server_response;
43
+ response_obj.body_length = rc_json_get_object_string_length(server_response);
44
+
45
+ return rc_api_process_resolve_hash_server_response(response, &response_obj);
46
+ }
47
+
48
+ int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) {
49
+ int result;
50
+ rc_json_field_t fields[] = {
51
+ RC_JSON_NEW_FIELD("Success"),
52
+ RC_JSON_NEW_FIELD("Error"),
53
+ RC_JSON_NEW_FIELD("GameID")
54
+ };
55
+
56
+ memset(response, 0, sizeof(*response));
57
+ rc_buffer_init(&response->response.buffer);
58
+
59
+ result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
60
+ if (result != RC_OK)
61
+ return result;
62
+
63
+ rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID");
64
+ return RC_OK;
65
+ }
66
+
67
+ void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) {
68
+ rc_buffer_destroy(&response->response.buffer);
69
+ }
70
+
71
+ /* --- Fetch Game Data --- */
72
+
73
+ int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) {
74
+ return rc_api_init_fetch_game_data_request_hosted(request, api_params, &g_host);
75
+ }
76
+
77
+ int rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request,
78
+ const rc_api_fetch_game_data_request_t* api_params,
79
+ const rc_api_host_t* host) {
80
+ rc_api_url_builder_t builder;
81
+
82
+ rc_api_url_build_dorequest_url(request, host);
83
+
84
+ if (api_params->game_id == 0 && (!api_params->game_hash || !api_params->game_hash[0]))
85
+ return RC_INVALID_STATE;
86
+
87
+ rc_url_builder_init(&builder, &request->buffer, 48);
88
+ if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) {
89
+ if (api_params->game_id)
90
+ rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
91
+ else
92
+ rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
93
+
94
+ request->post_data = rc_url_builder_finalize(&builder);
95
+ request->content_type = RC_CONTENT_TYPE_URLENCODED;
96
+ }
97
+
98
+ return builder.result;
99
+ }
100
+
101
+ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) {
102
+ rc_api_server_response_t response_obj;
103
+
104
+ memset(&response_obj, 0, sizeof(response_obj));
105
+ response_obj.body = server_response;
106
+ response_obj.body_length = rc_json_get_object_string_length(server_response);
107
+
108
+ return rc_api_process_fetch_game_data_server_response(response, &response_obj);
109
+ }
110
+
111
+ static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) {
112
+ rc_json_iterator_t iterator;
113
+ const char* last_author = "";
114
+ const char* last_author_field = "";
115
+ size_t last_author_len = 0;
116
+ uint32_t timet;
117
+ size_t len;
118
+
119
+ rc_json_field_t achievement_fields[] = {
120
+ RC_JSON_NEW_FIELD("ID"),
121
+ RC_JSON_NEW_FIELD("Title"),
122
+ RC_JSON_NEW_FIELD("Description"),
123
+ RC_JSON_NEW_FIELD("Flags"),
124
+ RC_JSON_NEW_FIELD("Points"),
125
+ RC_JSON_NEW_FIELD("MemAddr"),
126
+ RC_JSON_NEW_FIELD("Author"),
127
+ RC_JSON_NEW_FIELD("BadgeName"),
128
+ RC_JSON_NEW_FIELD("Created"),
129
+ RC_JSON_NEW_FIELD("Modified"),
130
+ RC_JSON_NEW_FIELD("Type"),
131
+ RC_JSON_NEW_FIELD("Rarity"),
132
+ RC_JSON_NEW_FIELD("RarityHardcore"),
133
+ RC_JSON_NEW_FIELD("BadgeURL"),
134
+ RC_JSON_NEW_FIELD("BadgeLockedURL")
135
+ };
136
+
137
+ memset(&iterator, 0, sizeof(iterator));
138
+ iterator.json = array_field->value_start;
139
+ iterator.end = array_field->value_end;
140
+
141
+ while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) {
142
+ if (!rc_json_get_required_unum(&achievement->id, response, &achievement_fields[0], "ID"))
143
+ return RC_MISSING_VALUE;
144
+ if (!rc_json_get_required_string(&achievement->title, response, &achievement_fields[1], "Title"))
145
+ return RC_MISSING_VALUE;
146
+ if (!rc_json_get_required_string(&achievement->description, response, &achievement_fields[2], "Description"))
147
+ return RC_MISSING_VALUE;
148
+ if (!rc_json_get_required_unum(&achievement->category, response, &achievement_fields[3], "Flags"))
149
+ return RC_MISSING_VALUE;
150
+ if (!rc_json_get_required_unum(&achievement->points, response, &achievement_fields[4], "Points"))
151
+ return RC_MISSING_VALUE;
152
+ if (!rc_json_get_required_string(&achievement->definition, response, &achievement_fields[5], "MemAddr"))
153
+ return RC_MISSING_VALUE;
154
+ if (!rc_json_get_required_string(&achievement->badge_name, response, &achievement_fields[7], "BadgeName"))
155
+ return RC_MISSING_VALUE;
156
+
157
+ rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", "");
158
+ if (!achievement->badge_url[0])
159
+ achievement->badge_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name);
160
+
161
+ rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", "");
162
+ if (!achievement->badge_locked_url[0])
163
+ achievement->badge_locked_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name);
164
+
165
+ len = achievement_fields[6].value_end - achievement_fields[6].value_start;
166
+ if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) {
167
+ achievement->author = last_author;
168
+ }
169
+ else {
170
+ if (!rc_json_get_required_string(&achievement->author, response, &achievement_fields[6], "Author"))
171
+ return RC_MISSING_VALUE;
172
+
173
+ if (achievement->author == NULL) {
174
+ /* ensure we don't pass NULL out to client */
175
+ last_author = achievement->author = "";
176
+ last_author_len = 0;
177
+ } else {
178
+ last_author = achievement->author;
179
+ last_author_field = achievement_fields[6].value_start;
180
+ last_author_len = len;
181
+ }
182
+ }
183
+
184
+ if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created"))
185
+ return RC_MISSING_VALUE;
186
+ achievement->created = (time_t)timet;
187
+ if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified"))
188
+ return RC_MISSING_VALUE;
189
+ achievement->updated = (time_t)timet;
190
+
191
+ if (rc_json_field_string_matches(&achievement_fields[10], ""))
192
+ achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;
193
+ else if (rc_json_field_string_matches(&achievement_fields[10], "progression"))
194
+ achievement->type = RC_ACHIEVEMENT_TYPE_PROGRESSION;
195
+ else if (rc_json_field_string_matches(&achievement_fields[10], "missable"))
196
+ achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
197
+ else if (rc_json_field_string_matches(&achievement_fields[10], "win_condition"))
198
+ achievement->type = RC_ACHIEVEMENT_TYPE_WIN;
199
+ else
200
+ achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;
201
+
202
+ /* legacy support : if title contains[m], change type to missable and remove[m] from title */
203
+ if (memcmp(achievement->title, "[m]", 3) == 0) {
204
+ len = 3;
205
+ while (achievement->title[len] == ' ')
206
+ ++len;
207
+ achievement->title += len;
208
+ achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
209
+ }
210
+ else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) {
211
+ len = strlen(achievement->title) - 3;
212
+ while (achievement->title[len - 1] == ' ')
213
+ --len;
214
+ ((char*)achievement->title)[len] = '\0';
215
+ achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
216
+ }
217
+
218
+ rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0);
219
+ rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0);
220
+
221
+ ++achievement;
222
+ }
223
+
224
+ return RC_OK;
225
+ }
226
+
227
+ static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) {
228
+ rc_json_iterator_t iterator;
229
+ size_t len;
230
+ int result;
231
+ char format[16];
232
+
233
+ rc_json_field_t leaderboard_fields[] = {
234
+ RC_JSON_NEW_FIELD("ID"),
235
+ RC_JSON_NEW_FIELD("Title"),
236
+ RC_JSON_NEW_FIELD("Description"),
237
+ RC_JSON_NEW_FIELD("Mem"),
238
+ RC_JSON_NEW_FIELD("Format"),
239
+ RC_JSON_NEW_FIELD("LowerIsBetter"),
240
+ RC_JSON_NEW_FIELD("Hidden")
241
+ };
242
+
243
+ memset(&iterator, 0, sizeof(iterator));
244
+ iterator.json = array_field->value_start;
245
+ iterator.end = array_field->value_end;
246
+
247
+ while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) {
248
+ if (!rc_json_get_required_unum(&leaderboard->id, response, &leaderboard_fields[0], "ID"))
249
+ return RC_MISSING_VALUE;
250
+ if (!rc_json_get_required_string(&leaderboard->title, response, &leaderboard_fields[1], "Title"))
251
+ return RC_MISSING_VALUE;
252
+ if (!rc_json_get_required_string(&leaderboard->description, response, &leaderboard_fields[2], "Description"))
253
+ return RC_MISSING_VALUE;
254
+ if (!rc_json_get_required_string(&leaderboard->definition, response, &leaderboard_fields[3], "Mem"))
255
+ return RC_MISSING_VALUE;
256
+ rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0);
257
+ leaderboard->lower_is_better = (uint8_t)result;
258
+ rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0);
259
+ leaderboard->hidden = (uint8_t)result;
260
+
261
+ if (!leaderboard_fields[4].value_end)
262
+ return RC_MISSING_VALUE;
263
+ len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2;
264
+ if (len < sizeof(format) - 1) {
265
+ memcpy(format, leaderboard_fields[4].value_start + 1, len);
266
+ format[len] = '\0';
267
+ leaderboard->format = rc_parse_format(format);
268
+ }
269
+ else {
270
+ leaderboard->format = RC_FORMAT_VALUE;
271
+ }
272
+
273
+ ++leaderboard;
274
+ }
275
+
276
+ return RC_OK;
277
+ }
278
+
279
+ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) {
280
+ rc_json_field_t array_field;
281
+ size_t len;
282
+ int result;
283
+
284
+ rc_json_field_t fields[] = {
285
+ RC_JSON_NEW_FIELD("Success"),
286
+ RC_JSON_NEW_FIELD("Error"),
287
+ RC_JSON_NEW_FIELD("Code"),
288
+ RC_JSON_NEW_FIELD("PatchData") /* nested object */
289
+ };
290
+
291
+ rc_json_field_t patchdata_fields[] = {
292
+ RC_JSON_NEW_FIELD("ID"),
293
+ RC_JSON_NEW_FIELD("Title"),
294
+ RC_JSON_NEW_FIELD("ConsoleID"),
295
+ RC_JSON_NEW_FIELD("ImageIcon"),
296
+ RC_JSON_NEW_FIELD("ImageIconURL"),
297
+ RC_JSON_NEW_FIELD("RichPresencePatch"),
298
+ RC_JSON_NEW_FIELD("Achievements"), /* array */
299
+ RC_JSON_NEW_FIELD("Leaderboards"), /* array */
300
+ };
301
+
302
+ memset(response, 0, sizeof(*response));
303
+ rc_buffer_init(&response->response.buffer);
304
+
305
+ result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
306
+ if (result != RC_OK || !response->response.succeeded)
307
+ return result;
308
+
309
+ if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[3], "PatchData"))
310
+ return RC_MISSING_VALUE;
311
+
312
+ if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID"))
313
+ return RC_MISSING_VALUE;
314
+ if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title"))
315
+ return RC_MISSING_VALUE;
316
+ if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID"))
317
+ return RC_MISSING_VALUE;
318
+
319
+ /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */
320
+ rc_json_extract_filename(&patchdata_fields[3]);
321
+ rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", "");
322
+ rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", "");
323
+ if (!response->image_url[0])
324
+ response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name);
325
+
326
+ /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards.
327
+ determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)
328
+ and add space for the structures. */
329
+ len = patchdata_fields[5].value_end - patchdata_fields[5].value_start; /* rich presence */
330
+
331
+ len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* achievements */
332
+ patchdata_fields[6].array_size * (80 - sizeof(rc_api_achievement_definition_t));
333
+
334
+ len += (patchdata_fields[7].value_end - patchdata_fields[7].value_start) - /* leaderboards */
335
+ patchdata_fields[7].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));
336
+
337
+ rc_buffer_reserve(&response->response.buffer, len);
338
+ /* end estimation */
339
+
340
+ rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[5], "RichPresencePatch", "");
341
+ if (!response->rich_presence_script)
342
+ response->rich_presence_script = "";
343
+
344
+ if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[6], "Achievements"))
345
+ return RC_MISSING_VALUE;
346
+
347
+ if (response->num_achievements) {
348
+ response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t));
349
+ if (!response->achievements)
350
+ return RC_OUT_OF_MEMORY;
351
+
352
+ result = rc_api_process_fetch_game_data_achievements(&response->response, response->achievements, &array_field);
353
+ if (result != RC_OK)
354
+ return result;
355
+ }
356
+
357
+ if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[7], "Leaderboards"))
358
+ return RC_MISSING_VALUE;
359
+
360
+ if (response->num_leaderboards) {
361
+ response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));
362
+ if (!response->leaderboards)
363
+ return RC_OUT_OF_MEMORY;
364
+
365
+ result = rc_api_process_fetch_game_data_leaderboards(&response->response, response->leaderboards, &array_field);
366
+ if (result != RC_OK)
367
+ return result;
368
+ }
369
+
370
+ return RC_OK;
371
+ }
372
+
373
+ void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) {
374
+ rc_buffer_destroy(&response->response.buffer);
375
+ }
376
+
377
+ /* --- Fetch Game Sets --- */
378
+
379
+ int rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params) {
380
+ return rc_api_init_fetch_game_sets_request_hosted(request, api_params, &g_host);
381
+ }
382
+
383
+ int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request,
384
+ const rc_api_fetch_game_sets_request_t* api_params,
385
+ const rc_api_host_t* host) {
386
+ rc_api_url_builder_t builder;
387
+
388
+ rc_api_url_build_dorequest_url(request, host);
389
+
390
+ if (!api_params->game_id && (!api_params->game_hash || !api_params->game_hash[0]))
391
+ return RC_INVALID_STATE;
392
+
393
+ rc_url_builder_init(&builder, &request->buffer, 48);
394
+ if (rc_api_url_build_dorequest(&builder, "achievementsets", api_params->username, api_params->api_token)) {
395
+ if (api_params->game_id)
396
+ rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
397
+ else
398
+ rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
399
+
400
+ request->post_data = rc_url_builder_finalize(&builder);
401
+ request->content_type = RC_CONTENT_TYPE_URLENCODED;
402
+ }
403
+
404
+ return builder.result;
405
+ }
406
+
407
+ static int rc_api_process_fetch_game_sets_achievement_sets(rc_api_fetch_game_sets_response_t* response,
408
+ rc_api_achievement_set_definition_t* subset,
409
+ rc_json_field_t* subset_array_field) {
410
+ rc_json_iterator_t iterator;
411
+ rc_json_field_t array_field;
412
+ size_t len;
413
+ int result;
414
+
415
+ rc_json_field_t subset_fields[] = {
416
+ RC_JSON_NEW_FIELD("AchievementSetId"),
417
+ RC_JSON_NEW_FIELD("GameId"),
418
+ RC_JSON_NEW_FIELD("Title"),
419
+ RC_JSON_NEW_FIELD("Type"),
420
+ RC_JSON_NEW_FIELD("ImageIconUrl"),
421
+ RC_JSON_NEW_FIELD("Achievements"), /* array */
422
+ RC_JSON_NEW_FIELD("Leaderboards") /* array */
423
+ };
424
+
425
+ memset(&iterator, 0, sizeof(iterator));
426
+ iterator.json = subset_array_field->value_start;
427
+ iterator.end = subset_array_field->value_end;
428
+
429
+ while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) {
430
+ if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "AchievementSetId"))
431
+ return RC_MISSING_VALUE;
432
+ if (!rc_json_get_required_unum(&subset->game_id, &response->response, &subset_fields[1], "GameId"))
433
+ return RC_MISSING_VALUE;
434
+
435
+ if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[2], "Title"))
436
+ return RC_MISSING_VALUE;
437
+ if (!subset->title || !subset->title[0])
438
+ subset->title = response->title;
439
+
440
+ if (rc_json_field_string_matches(&subset_fields[3], "core"))
441
+ subset->type = RC_ACHIEVEMENT_SET_TYPE_CORE;
442
+ else if (rc_json_field_string_matches(&subset_fields[3], "bonus"))
443
+ subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS;
444
+ else if (rc_json_field_string_matches(&subset_fields[3], "specialty"))
445
+ subset->type = RC_ACHIEVEMENT_SET_TYPE_SPECIALTY;
446
+ else if (rc_json_field_string_matches(&subset_fields[3], "exclusive"))
447
+ subset->type = RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE;
448
+ else
449
+ subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS;
450
+
451
+ if (rc_json_field_string_matches(&subset_fields[4], response->image_url)) {
452
+ subset->image_url = response->image_url;
453
+ subset->image_name = response->image_name;
454
+ }
455
+ else {
456
+ if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[4], "ImageIconUrl"))
457
+ return RC_MISSING_VALUE;
458
+ rc_json_extract_filename(&subset_fields[4]);
459
+ rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[4], "ImageIconUrl", "");
460
+ }
461
+
462
+ /* estimate the amount of space necessary to store the achievements, and leaderboards.
463
+ determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)
464
+ and add space for the structures. */
465
+ len = (subset_fields[5].value_end - subset_fields[5].value_start) - /* achievements */
466
+ subset_fields[5].array_size * (80 - sizeof(rc_api_achievement_definition_t));
467
+ len += (subset_fields[6].value_end - subset_fields[6].value_start) - /* leaderboards */
468
+ subset_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));
469
+
470
+ rc_buffer_reserve(&response->response.buffer, len);
471
+ /* end estimation */
472
+
473
+ if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[5], "Achievements"))
474
+ return RC_MISSING_VALUE;
475
+
476
+ if (subset->num_achievements) {
477
+ subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t));
478
+ if (!subset->achievements)
479
+ return RC_OUT_OF_MEMORY;
480
+
481
+ result = rc_api_process_fetch_game_data_achievements(&response->response, subset->achievements, &array_field);
482
+ if (result != RC_OK)
483
+ return result;
484
+ }
485
+
486
+ if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[6], "Leaderboards"))
487
+ return RC_MISSING_VALUE;
488
+
489
+ if (subset->num_leaderboards) {
490
+ subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));
491
+ if (!subset->leaderboards)
492
+ return RC_OUT_OF_MEMORY;
493
+
494
+ result = rc_api_process_fetch_game_data_leaderboards(&response->response, subset->leaderboards, &array_field);
495
+ if (result != RC_OK)
496
+ return result;
497
+ }
498
+
499
+ ++subset;
500
+ }
501
+
502
+ return RC_OK;
503
+ }
504
+
505
+ int rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response) {
506
+ rc_json_field_t array_field;
507
+ int result;
508
+
509
+ rc_json_field_t fields[] = {
510
+ RC_JSON_NEW_FIELD("Success"),
511
+ RC_JSON_NEW_FIELD("Error"),
512
+ RC_JSON_NEW_FIELD("Code"),
513
+ RC_JSON_NEW_FIELD("GameId"),
514
+ RC_JSON_NEW_FIELD("Title"),
515
+ RC_JSON_NEW_FIELD("ConsoleId"),
516
+ RC_JSON_NEW_FIELD("ImageIconUrl"),
517
+ RC_JSON_NEW_FIELD("RichPresenceGameId"),
518
+ RC_JSON_NEW_FIELD("RichPresencePatch"),
519
+ RC_JSON_NEW_FIELD("Sets") /* array */
520
+ };
521
+
522
+ memset(response, 0, sizeof(*response));
523
+ rc_buffer_init(&response->response.buffer);
524
+
525
+ result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
526
+ if (result != RC_OK || !response->response.succeeded)
527
+ return result;
528
+
529
+ if (!rc_json_get_required_unum(&response->id, &response->response, &fields[3], "GameId"))
530
+ return RC_MISSING_VALUE;
531
+ if (!rc_json_get_required_string(&response->title, &response->response, &fields[4], "Title"))
532
+ return RC_MISSING_VALUE;
533
+ if (!rc_json_get_required_unum(&response->console_id, &response->response, &fields[5], "ConsoleId"))
534
+ return RC_MISSING_VALUE;
535
+
536
+ rc_json_get_required_string(&response->image_url, &response->response, &fields[6], "ImageIconUrl");
537
+ rc_json_extract_filename(&fields[6]);
538
+ rc_json_get_required_string(&response->image_name, &response->response, &fields[6], "ImageIconUrl");
539
+
540
+ rc_json_get_optional_unum(&response->session_game_id, &fields[7], "RichPresenceGameId", response->id);
541
+
542
+ rc_json_get_optional_string(&response->rich_presence_script, &response->response, &fields[8], "RichPresencePatch", "");
543
+ if (!response->rich_presence_script)
544
+ response->rich_presence_script = "";
545
+
546
+ rc_json_get_optional_array(&response->num_sets, &array_field, &fields[9], "Sets");
547
+ if (response->num_sets) {
548
+ response->sets = (rc_api_achievement_set_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_sets * sizeof(rc_api_achievement_set_definition_t));
549
+ if (!response->sets)
550
+ return RC_OUT_OF_MEMORY;
551
+
552
+ result = rc_api_process_fetch_game_sets_achievement_sets(response, response->sets, &array_field);
553
+ if (result != RC_OK)
554
+ return result;
555
+ }
556
+
557
+ return RC_OK;
558
+ }
559
+
560
+ void rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response) {
561
+ rc_buffer_destroy(&response->response.buffer);
562
+ }
563
+
564
+ /* --- Ping --- */
565
+
566
+ int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) {
567
+ return rc_api_init_ping_request_hosted(request, api_params, &g_host);
568
+ }
569
+
570
+ int rc_api_init_ping_request_hosted(rc_api_request_t* request,
571
+ const rc_api_ping_request_t* api_params,
572
+ const rc_api_host_t* host) {
573
+ rc_api_url_builder_t builder;
574
+
575
+ rc_api_url_build_dorequest_url(request, host);
576
+
577
+ if (api_params->game_id == 0)
578
+ return RC_INVALID_STATE;
579
+
580
+ rc_url_builder_init(&builder, &request->buffer, 48);
581
+ if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) {
582
+ rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
583
+
584
+ if (api_params->rich_presence && *api_params->rich_presence)
585
+ rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence);
586
+
587
+ if (api_params->game_hash && *api_params->game_hash) {
588
+ rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore);
589
+ rc_url_builder_append_str_param(&builder, "x", api_params->game_hash);
590
+ }
591
+
592
+ request->post_data = rc_url_builder_finalize(&builder);
593
+ request->content_type = RC_CONTENT_TYPE_URLENCODED;
594
+ }
595
+
596
+ return builder.result;
597
+ }
598
+
599
+ int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) {
600
+ rc_api_server_response_t response_obj;
601
+
602
+ memset(&response_obj, 0, sizeof(response_obj));
603
+ response_obj.body = server_response;
604
+ response_obj.body_length = rc_json_get_object_string_length(server_response);
605
+
606
+ return rc_api_process_ping_server_response(response, &response_obj);
607
+ }
608
+
609
+ int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) {
610
+ rc_json_field_t fields[] = {
611
+ RC_JSON_NEW_FIELD("Success"),
612
+ RC_JSON_NEW_FIELD("Error")
613
+ };
614
+
615
+ memset(response, 0, sizeof(*response));
616
+ rc_buffer_init(&response->response.buffer);
617
+
618
+ return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
619
+ }
620
+
621
+ void rc_api_destroy_ping_response(rc_api_ping_response_t* response) {
622
+ rc_buffer_destroy(&response->response.buffer);
623
+ }
624
+
625
+ /* --- Award Achievement --- */
626
+
627
+ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) {
628
+ return rc_api_init_award_achievement_request_hosted(request, api_params, &g_host);
629
+ }
630
+
631
+ int rc_api_init_award_achievement_request_hosted(rc_api_request_t* request,
632
+ const rc_api_award_achievement_request_t* api_params,
633
+ const rc_api_host_t* host) {
634
+ rc_api_url_builder_t builder;
635
+ char buffer[33];
636
+ md5_state_t md5;
637
+ md5_byte_t digest[16];
638
+
639
+ rc_api_url_build_dorequest_url(request, host);
640
+
641
+ if (api_params->achievement_id == 0)
642
+ return RC_INVALID_STATE;
643
+
644
+ rc_url_builder_init(&builder, &request->buffer, 96);
645
+ if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) {
646
+ rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id);
647
+ rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0);
648
+ if (api_params->game_hash && *api_params->game_hash)
649
+ rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
650
+ if (api_params->seconds_since_unlock)
651
+ rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_unlock);
652
+
653
+ /* Evaluate the signature. */
654
+ md5_init(&md5);
655
+ snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);
656
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
657
+ md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
658
+ snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0);
659
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
660
+ if (api_params->seconds_since_unlock) {
661
+ /* second achievement id is needed by delegated unlock. including it here allows overloading
662
+ * the hash generating code on the server */
663
+ snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);
664
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
665
+ snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_unlock);
666
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
667
+ }
668
+ md5_finish(&md5, digest);
669
+ rc_format_md5(buffer, digest);
670
+ rc_url_builder_append_str_param(&builder, "v", buffer);
671
+
672
+ request->post_data = rc_url_builder_finalize(&builder);
673
+ request->content_type = RC_CONTENT_TYPE_URLENCODED;
674
+ }
675
+
676
+ return builder.result;
677
+ }
678
+
679
+ int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) {
680
+ rc_api_server_response_t response_obj;
681
+
682
+ memset(&response_obj, 0, sizeof(response_obj));
683
+ response_obj.body = server_response;
684
+ response_obj.body_length = rc_json_get_object_string_length(server_response);
685
+
686
+ return rc_api_process_award_achievement_server_response(response, &response_obj);
687
+ }
688
+
689
+ int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) {
690
+ int result;
691
+ rc_json_field_t fields[] = {
692
+ RC_JSON_NEW_FIELD("Success"),
693
+ RC_JSON_NEW_FIELD("Error"),
694
+ RC_JSON_NEW_FIELD("Score"),
695
+ RC_JSON_NEW_FIELD("SoftcoreScore"),
696
+ RC_JSON_NEW_FIELD("AchievementID"),
697
+ RC_JSON_NEW_FIELD("AchievementsRemaining")
698
+ };
699
+
700
+ memset(response, 0, sizeof(*response));
701
+ rc_buffer_init(&response->response.buffer);
702
+
703
+ result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
704
+ if (result != RC_OK)
705
+ return result;
706
+
707
+ if (!response->response.succeeded) {
708
+ if (response->response.error_message &&
709
+ memcmp(response->response.error_message, "User already has", 16) == 0) {
710
+ /* not really an error, the achievement is unlocked, just not by the current call.
711
+ * hardcore: User already has hardcore and regular achievements awarded.
712
+ * non-hardcore: User already has this achievement awarded.
713
+ */
714
+ response->response.succeeded = 1;
715
+ } else {
716
+ return result;
717
+ }
718
+ }
719
+
720
+ rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0);
721
+ rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0);
722
+ rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0);
723
+ rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1);
724
+
725
+ return RC_OK;
726
+ }
727
+
728
+ void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) {
729
+ rc_buffer_destroy(&response->response.buffer);
730
+ }
731
+
732
+ /* --- Submit Leaderboard Entry --- */
733
+
734
+ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) {
735
+ return rc_api_init_submit_lboard_entry_request_hosted(request, api_params, &g_host);
736
+ }
737
+
738
+ int rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request,
739
+ const rc_api_submit_lboard_entry_request_t* api_params,
740
+ const rc_api_host_t* host) {
741
+ rc_api_url_builder_t builder;
742
+ char buffer[33];
743
+ md5_state_t md5;
744
+ md5_byte_t digest[16];
745
+
746
+ rc_api_url_build_dorequest_url(request, host);
747
+
748
+ if (api_params->leaderboard_id == 0)
749
+ return RC_INVALID_STATE;
750
+
751
+ rc_url_builder_init(&builder, &request->buffer, 96);
752
+ if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) {
753
+ rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id);
754
+ rc_url_builder_append_num_param(&builder, "s", api_params->score);
755
+
756
+ if (api_params->game_hash && *api_params->game_hash)
757
+ rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
758
+
759
+ if (api_params->seconds_since_completion)
760
+ rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_completion);
761
+
762
+ /* Evaluate the signature. */
763
+ md5_init(&md5);
764
+ snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id);
765
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
766
+ md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
767
+ snprintf(buffer, sizeof(buffer), "%d", api_params->score);
768
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
769
+ if (api_params->seconds_since_completion) {
770
+ snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_completion);
771
+ md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
772
+ }
773
+ md5_finish(&md5, digest);
774
+ rc_format_md5(buffer, digest);
775
+ rc_url_builder_append_str_param(&builder, "v", buffer);
776
+
777
+ request->post_data = rc_url_builder_finalize(&builder);
778
+ request->content_type = RC_CONTENT_TYPE_URLENCODED;
779
+ }
780
+
781
+ return builder.result;
782
+ }
783
+
784
+ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) {
785
+ rc_api_server_response_t response_obj;
786
+
787
+ memset(&response_obj, 0, sizeof(response_obj));
788
+ response_obj.body = server_response;
789
+ response_obj.body_length = rc_json_get_object_string_length(server_response);
790
+
791
+ return rc_api_process_submit_lboard_entry_server_response(response, &response_obj);
792
+ }
793
+
794
+ int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) {
795
+ rc_api_lboard_entry_t* entry;
796
+ rc_json_field_t array_field;
797
+ rc_json_iterator_t iterator;
798
+ const char* str;
799
+ int result;
800
+
801
+ rc_json_field_t fields[] = {
802
+ RC_JSON_NEW_FIELD("Success"),
803
+ RC_JSON_NEW_FIELD("Error"),
804
+ RC_JSON_NEW_FIELD("Response") /* nested object */
805
+ };
806
+
807
+ rc_json_field_t response_fields[] = {
808
+ RC_JSON_NEW_FIELD("Score"),
809
+ RC_JSON_NEW_FIELD("BestScore"),
810
+ RC_JSON_NEW_FIELD("RankInfo"), /* nested object */
811
+ RC_JSON_NEW_FIELD("TopEntries") /* array */
812
+ /* unused fields
813
+ RC_JSON_NEW_FIELD("LBData"), / * array * /
814
+ RC_JSON_NEW_FIELD("ScoreFormatted"),
815
+ RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * /
816
+ * unused fields */
817
+ };
818
+
819
+ /* unused fields
820
+ rc_json_field_t lbdata_fields[] = {
821
+ RC_JSON_NEW_FIELD("Format"),
822
+ RC_JSON_NEW_FIELD("LeaderboardID"),
823
+ RC_JSON_NEW_FIELD("GameID"),
824
+ RC_JSON_NEW_FIELD("Title"),
825
+ RC_JSON_NEW_FIELD("LowerIsBetter")
826
+ };
827
+ * unused fields */
828
+
829
+ rc_json_field_t entry_fields[] = {
830
+ RC_JSON_NEW_FIELD("User"),
831
+ RC_JSON_NEW_FIELD("Rank"),
832
+ RC_JSON_NEW_FIELD("Score")
833
+ /* unused fields
834
+ RC_JSON_NEW_FIELD("DateSubmitted")
835
+ * unused fields */
836
+ };
837
+
838
+ rc_json_field_t rank_info_fields[] = {
839
+ RC_JSON_NEW_FIELD("Rank"),
840
+ RC_JSON_NEW_FIELD("NumEntries")
841
+ /* unused fields
842
+ RC_JSON_NEW_FIELD("LowerIsBetter"),
843
+ RC_JSON_NEW_FIELD("UserRank")
844
+ * unused fields */
845
+ };
846
+
847
+ memset(response, 0, sizeof(*response));
848
+ rc_buffer_init(&response->response.buffer);
849
+
850
+ result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
851
+ if (result != RC_OK || !response->response.succeeded)
852
+ return result;
853
+
854
+ if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response"))
855
+ return RC_MISSING_VALUE;
856
+ if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score"))
857
+ return RC_MISSING_VALUE;
858
+ if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore"))
859
+ return RC_MISSING_VALUE;
860
+
861
+ if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo"))
862
+ return RC_MISSING_VALUE;
863
+ if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank"))
864
+ return RC_MISSING_VALUE;
865
+ if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries"))
866
+ return RC_MISSING_VALUE;
867
+ response->num_entries = (unsigned)atoi(str);
868
+
869
+ if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries"))
870
+ return RC_MISSING_VALUE;
871
+
872
+ if (response->num_top_entries) {
873
+ response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t));
874
+ if (!response->top_entries)
875
+ return RC_OUT_OF_MEMORY;
876
+
877
+ memset(&iterator, 0, sizeof(iterator));
878
+ iterator.json = array_field.value_start;
879
+ iterator.end = array_field.value_end;
880
+
881
+ entry = response->top_entries;
882
+ while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) {
883
+ if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User"))
884
+ return RC_MISSING_VALUE;
885
+
886
+ if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank"))
887
+ return RC_MISSING_VALUE;
888
+
889
+ if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score"))
890
+ return RC_MISSING_VALUE;
891
+
892
+ ++entry;
893
+ }
894
+ }
895
+
896
+ return RC_OK;
897
+ }
898
+
899
+ void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) {
900
+ rc_buffer_destroy(&response->response.buffer);
901
+ }