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,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+
6
+ class TestHelpWindow < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ # HelpWindow is wm transient to '.'. The TestWorker withdraws '.' after each
10
+ # test, so a transient child can't be shown unless we deiconify '.' first.
11
+ # app.show deiconifies the root window for tests that check visibility.
12
+
13
+ def test_visible_after_show
14
+ assert_tk_app("help window is visible after show") do
15
+ require "gemba/headless"
16
+
17
+ hotkeys = Struct.new(:m) { def key_for(a) = Gemba::HotkeyMap::DEFAULTS[a] }.new(nil)
18
+ win = Gemba::HelpWindow.new(app: app, hotkeys: hotkeys)
19
+ app.show
20
+ win.show
21
+ app.update # flush pending Tk map events before checking visibility
22
+
23
+ assert win.visible?, "help window should be visible after show"
24
+
25
+ win.hide
26
+ app.command(:destroy, Gemba::HelpWindow::TOP) rescue nil
27
+ end
28
+ end
29
+
30
+ def test_hidden_after_hide
31
+ assert_tk_app("help window is hidden after hide") do
32
+ require "gemba/headless"
33
+
34
+ hotkeys = Struct.new(:m) { def key_for(a) = Gemba::HotkeyMap::DEFAULTS[a] }.new(nil)
35
+ win = Gemba::HelpWindow.new(app: app, hotkeys: hotkeys)
36
+ app.show
37
+ win.show
38
+ win.hide
39
+
40
+ refute win.visible?, "help window should not be visible after hide"
41
+
42
+ app.command(:destroy, Gemba::HelpWindow::TOP) rescue nil
43
+ end
44
+ end
45
+
46
+ def test_rows_show_action_labels
47
+ assert_tk_app("help window rows show translated action labels") do
48
+ require "gemba/headless"
49
+
50
+ hotkeys = Struct.new(:m) { def key_for(a) = Gemba::HotkeyMap::DEFAULTS[a] }.new(nil)
51
+ win = Gemba::HelpWindow.new(app: app, hotkeys: hotkeys)
52
+ win.show
53
+
54
+ text = app.command("#{Gemba::HelpWindow::TOP}.f.row_pause.act", :cget, '-text')
55
+ assert_equal 'Pause', text, "pause row should show 'Pause' label"
56
+
57
+ win.hide
58
+ app.command(:destroy, Gemba::HelpWindow::TOP) rescue nil
59
+ end
60
+ end
61
+
62
+ def test_rows_show_key_display
63
+ assert_tk_app("help window rows show formatted key names") do
64
+ require "gemba/headless"
65
+
66
+ hotkeys = Struct.new(:m) { def key_for(a) = Gemba::HotkeyMap::DEFAULTS[a] }.new(nil)
67
+ win = Gemba::HelpWindow.new(app: app, hotkeys: hotkeys)
68
+ win.show
69
+
70
+ # pause default is 'p'
71
+ text = app.command("#{Gemba::HelpWindow::TOP}.f.row_pause.key", :cget, '-text')
72
+ assert_equal 'p', text, "pause row key should show 'p'"
73
+
74
+ # quick_save default is 'F5'
75
+ text = app.command("#{Gemba::HelpWindow::TOP}.f.row_quick_save.key", :cget, '-text')
76
+ assert_equal 'F5', text, "quick_save row key should show 'F5'"
77
+
78
+ win.hide
79
+ app.command(:destroy, Gemba::HelpWindow::TOP) rescue nil
80
+ end
81
+ end
82
+ end
@@ -29,7 +29,7 @@ end
29
29
 
30
30
  class TestHotkeyMap < Minitest::Test
31
31
  def setup
32
- require "gemba/hotkey_map"
32
+ require "gemba/headless"
33
33
  end
34
34
 
35
35
  def make_map(hotkey_data = {})
@@ -393,4 +393,25 @@ class TestHotkeyMap < Minitest::Test
393
393
  map, = make_map
394
394
  assert_equal :record, map.action_for('F10')
395
395
  end
396
+
397
+ # -- Open ROM action -------------------------------------------------------
398
+
399
+ def test_open_rom_in_actions
400
+ assert_includes Gemba::HotkeyMap::ACTIONS, :open_rom
401
+ end
402
+
403
+ def test_open_rom_default_is_ctrl_o
404
+ map, = make_map
405
+ assert_equal ['Control', 'o'], map.key_for(:open_rom)
406
+ end
407
+
408
+ def test_open_rom_dispatches_on_ctrl_o
409
+ map, = make_map
410
+ assert_equal :open_rom, map.action_for('o', modifiers: Set.new(['Control']))
411
+ end
412
+
413
+ def test_open_rom_plain_o_does_not_dispatch
414
+ map, = make_map
415
+ assert_nil map.action_for('o')
416
+ end
396
417
  end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/headless"
6
+ require "tmpdir"
7
+
8
+ class TestInputRecorder < Minitest::Test
9
+ TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
10
+
11
+
12
+ # -- Lifecycle ---------------------------------------------------------------
13
+
14
+ def test_start_and_stop
15
+ Dir.mktmpdir do |dir|
16
+ path = File.join(dir, "test.gir")
17
+
18
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
19
+ rec = Gemba::InputRecorder.new(path, core: player.core)
20
+ refute rec.recording?
21
+ rec.start
22
+ assert rec.recording?
23
+ rec.capture(0)
24
+ rec.stop
25
+ refute rec.recording?
26
+ end
27
+
28
+ assert File.exist?(path)
29
+ end
30
+ end
31
+
32
+ def test_double_start_raises
33
+ Dir.mktmpdir do |dir|
34
+ path = File.join(dir, "test.gir")
35
+
36
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
37
+ rec = Gemba::InputRecorder.new(path, core: player.core)
38
+ rec.start
39
+ assert_raises(RuntimeError) { rec.start }
40
+ rec.stop
41
+ end
42
+ end
43
+ end
44
+
45
+ def test_stop_without_start_is_safe
46
+ Dir.mktmpdir do |dir|
47
+ path = File.join(dir, "test.gir")
48
+
49
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
50
+ rec = Gemba::InputRecorder.new(path, core: player.core)
51
+ rec.stop # should not raise
52
+ end
53
+ end
54
+ end
55
+
56
+ # -- Anchor state ------------------------------------------------------------
57
+
58
+ def test_anchor_state_created
59
+ Dir.mktmpdir do |dir|
60
+ path = File.join(dir, "test.gir")
61
+
62
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
63
+ player.step(10) # advance so state has content
64
+ rec = Gemba::InputRecorder.new(path, core: player.core)
65
+ rec.start
66
+ rec.capture(0)
67
+ rec.stop
68
+
69
+ state_path = path.sub(/\.gir\z/, '.state')
70
+ assert File.exist?(state_path), "anchor .state file should exist"
71
+ assert_operator File.size(state_path), :>, 0
72
+ end
73
+ end
74
+ end
75
+
76
+ # -- Header ------------------------------------------------------------------
77
+
78
+ def test_header_format
79
+ Dir.mktmpdir do |dir|
80
+ path = File.join(dir, "test.gir")
81
+
82
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
83
+ core = player.core
84
+ rec = Gemba::InputRecorder.new(path, core: core)
85
+ rec.start
86
+ rec.capture(0)
87
+ rec.stop
88
+
89
+ lines = File.readlines(path)
90
+ assert_match(/^# GEMBA INPUT RECORDING v1$/, lines[0])
91
+ assert_match(/^# rom_checksum: \d+$/, lines[1])
92
+ assert_match(/^# game_code: .+$/, lines[2])
93
+ assert_match(/^# frame_count: \d+$/, lines[3])
94
+ assert_match(/^# anchor_state: .+\.state$/, lines[4])
95
+ assert_equal "---\n", lines[5]
96
+ end
97
+ end
98
+ end
99
+
100
+ def test_header_frame_count_updated_on_clean_stop
101
+ Dir.mktmpdir do |dir|
102
+ path = File.join(dir, "test.gir")
103
+
104
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
105
+ rec = Gemba::InputRecorder.new(path, core: player.core)
106
+ rec.start
107
+ 5.times { |i| rec.capture(i) }
108
+ rec.stop
109
+
110
+ lines = File.readlines(path)
111
+ assert_equal "# frame_count: 0000000005\n", lines[3]
112
+ end
113
+ end
114
+ end
115
+
116
+ # -- Bitmask data ------------------------------------------------------------
117
+
118
+ def test_captures_correct_bitmasks
119
+ Dir.mktmpdir do |dir|
120
+ path = File.join(dir, "test.gir")
121
+
122
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
123
+ rec = Gemba::InputRecorder.new(path, core: player.core)
124
+ rec.start
125
+ rec.capture(0x000) # nothing
126
+ rec.capture(Gemba::KEY_A) # 0x001
127
+ rec.capture(Gemba::KEY_UP | Gemba::KEY_A) # 0x041
128
+ rec.capture(0x3FF) # all buttons
129
+ rec.stop
130
+
131
+ lines = File.readlines(path)
132
+ bitmasks = lines[6..] # after header (6 lines: 5 header + ---)
133
+ assert_equal "000\n", bitmasks[0]
134
+ assert_equal "001\n", bitmasks[1]
135
+ assert_equal "041\n", bitmasks[2]
136
+ assert_equal "3ff\n", bitmasks[3]
137
+ assert_equal 4, rec.frame_count
138
+ end
139
+ end
140
+ end
141
+
142
+ def test_bitmask_masked_to_10_bits
143
+ Dir.mktmpdir do |dir|
144
+ path = File.join(dir, "test.gir")
145
+
146
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
147
+ rec = Gemba::InputRecorder.new(path, core: player.core)
148
+ rec.start
149
+ rec.capture(0xFFFF) # upper bits should be masked off
150
+ rec.stop
151
+
152
+ lines = File.readlines(path)
153
+ assert_equal "3ff\n", lines[6]
154
+ end
155
+ end
156
+ end
157
+
158
+ # -- Flush behavior ----------------------------------------------------------
159
+
160
+ def test_periodic_flush_writes_data
161
+ Dir.mktmpdir do |dir|
162
+ path = File.join(dir, "test.gir")
163
+
164
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
165
+ rec = Gemba::InputRecorder.new(path, core: player.core)
166
+ rec.start
167
+
168
+ # Write exactly FLUSH_INTERVAL frames to trigger a flush
169
+ Gemba::InputRecorder::FLUSH_INTERVAL.times { rec.capture(0) }
170
+
171
+ # File should have data on disk even before stop
172
+ size_before_stop = File.size(path)
173
+ assert_operator size_before_stop, :>, 0
174
+
175
+ rec.stop
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/headless"
6
+ require "tmpdir"
7
+
8
+ class TestInputReplayDeterminism < Minitest::Test
9
+ PONG_ROM = File.expand_path("fixtures/pong.gba", __dir__)
10
+ TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
11
+
12
+ # Record inputs against pong, then replay from anchor state.
13
+ # Video buffer after replay must match the original recording frame-for-frame.
14
+ def test_deterministic_replay
15
+ Dir.mktmpdir do |dir|
16
+ gir_path = File.join(dir, "pong.gir")
17
+ frames = 120 # ~2 seconds of gameplay
18
+
19
+ # -- Phase 1: Record --
20
+ final_video = nil
21
+ Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
22
+ # Let the game boot for a few frames
23
+ player.step(30)
24
+
25
+ core = player.core
26
+ rec = Gemba::InputRecorder.new(gir_path, core: core)
27
+ rec.start
28
+
29
+ # Play some inputs: Start to begin, then Up, A, idle, Down+B
30
+ inputs = []
31
+ inputs.concat([Gemba::KEY_START] * 30)
32
+ inputs.concat([Gemba::KEY_UP] * 30)
33
+ inputs.concat([Gemba::KEY_A] * 30)
34
+ inputs.concat([0] * 30)
35
+ inputs.concat([Gemba::KEY_DOWN | Gemba::KEY_B] * 30)
36
+
37
+ inputs.each do |mask|
38
+ rec.capture(mask)
39
+ core.set_keys(mask)
40
+ core.run_frame
41
+ end
42
+
43
+ rec.stop
44
+ final_video = core.video_buffer_argb.dup
45
+ end
46
+
47
+ assert File.exist?(gir_path), ".gir file should exist"
48
+ assert File.exist?(gir_path.sub(/\.gir\z/, '.state')), ".state file should exist"
49
+
50
+ # -- Phase 2: Replay --
51
+ replay_video = nil
52
+ Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
53
+ player.replay(gir_path)
54
+ replay_video = player.video_buffer_argb.dup
55
+ end
56
+
57
+ # -- Phase 3: Compare --
58
+ assert_equal final_video.bytesize, replay_video.bytesize,
59
+ "video buffer sizes should match"
60
+ assert_equal final_video, replay_video,
61
+ "video buffer after replay should be identical to recording"
62
+ end
63
+ end
64
+
65
+ # Replay with the wrong ROM should raise ChecksumMismatch.
66
+ def test_replay_wrong_rom_raises
67
+ skip "test.gba fixture missing" unless File.exist?(TEST_ROM)
68
+
69
+ Dir.mktmpdir do |dir|
70
+ gir_path = File.join(dir, "pong.gir")
71
+
72
+ # Record against pong
73
+ Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
74
+ player.step(5)
75
+ core = player.core
76
+ rec = Gemba::InputRecorder.new(gir_path, core: core)
77
+ rec.start
78
+ rec.capture(0)
79
+ rec.stop
80
+ end
81
+
82
+ # Try to replay against test.gba — wrong ROM
83
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
84
+ assert_raises(Gemba::InputReplayer::ChecksumMismatch) do
85
+ player.replay(gir_path)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Replay with block yields each frame.
92
+ def test_replay_yields_frames
93
+ Dir.mktmpdir do |dir|
94
+ gir_path = File.join(dir, "pong.gir")
95
+
96
+ Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
97
+ player.step(5)
98
+ core = player.core
99
+ rec = Gemba::InputRecorder.new(gir_path, core: core)
100
+ rec.start
101
+ [0x001, 0x041, 0x000].each { |m| rec.capture(m) }
102
+ rec.stop
103
+ end
104
+
105
+ collected = []
106
+ Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
107
+ player.replay(gir_path) { |mask, idx| collected << [mask, idx] }
108
+ end
109
+
110
+ assert_equal [[0x001, 0], [0x041, 1], [0x000, 2]], collected
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/headless"
6
+ require "tmpdir"
7
+
8
+ class TestInputReplayer < Minitest::Test
9
+ TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
10
+
11
+
12
+ # -- Round-trip: record then replay ------------------------------------------
13
+
14
+ def test_round_trip_frame_count
15
+ Dir.mktmpdir do |dir|
16
+ gir_path = File.join(dir, "test.gir")
17
+ record(gir_path, [0x000, 0x001, 0x041, 0x3FF])
18
+
19
+ replayer = Gemba::InputReplayer.new(gir_path)
20
+ assert_equal 4, replayer.frame_count
21
+ end
22
+ end
23
+
24
+ def test_round_trip_bitmasks
25
+ Dir.mktmpdir do |dir|
26
+ gir_path = File.join(dir, "test.gir")
27
+ inputs = [0x000, 0x001, 0x041, 0x3FF]
28
+ record(gir_path, inputs)
29
+
30
+ replayer = Gemba::InputReplayer.new(gir_path)
31
+ inputs.each_with_index do |expected, i|
32
+ assert_equal expected, replayer.bitmask_at(i), "frame #{i}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def test_each_bitmask
38
+ Dir.mktmpdir do |dir|
39
+ gir_path = File.join(dir, "test.gir")
40
+ inputs = [0x001, 0x041, 0x000]
41
+ record(gir_path, inputs)
42
+
43
+ replayer = Gemba::InputReplayer.new(gir_path)
44
+ collected = []
45
+ replayer.each_bitmask { |mask, idx| collected << [mask, idx] }
46
+
47
+ assert_equal [[0x001, 0], [0x041, 1], [0x000, 2]], collected
48
+ end
49
+ end
50
+
51
+ # -- Header parsing ----------------------------------------------------------
52
+
53
+ def test_rom_checksum_parsed
54
+ Dir.mktmpdir do |dir|
55
+ gir_path = File.join(dir, "test.gir")
56
+ record(gir_path, [0])
57
+
58
+ replayer = Gemba::InputReplayer.new(gir_path)
59
+ assert_kind_of Integer, replayer.rom_checksum
60
+ assert_operator replayer.rom_checksum, :>, 0
61
+ end
62
+ end
63
+
64
+ def test_game_code_parsed
65
+ Dir.mktmpdir do |dir|
66
+ gir_path = File.join(dir, "test.gir")
67
+ record(gir_path, [0])
68
+
69
+ replayer = Gemba::InputReplayer.new(gir_path)
70
+ assert_equal "AGB-BGBE", replayer.game_code
71
+ end
72
+ end
73
+
74
+ def test_anchor_state_path
75
+ Dir.mktmpdir do |dir|
76
+ gir_path = File.join(dir, "test.gir")
77
+ record(gir_path, [0])
78
+
79
+ replayer = Gemba::InputReplayer.new(gir_path)
80
+ expected = File.join(dir, "test.state")
81
+ assert_equal expected, replayer.anchor_state_path
82
+ assert File.exist?(replayer.anchor_state_path)
83
+ end
84
+ end
85
+
86
+ # -- Validation --------------------------------------------------------------
87
+
88
+ def test_validate_passes_with_matching_rom
89
+ Dir.mktmpdir do |dir|
90
+ gir_path = File.join(dir, "test.gir")
91
+ record(gir_path, [0])
92
+
93
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
94
+ replayer = Gemba::InputReplayer.new(gir_path)
95
+ replayer.validate!(player.core)
96
+ # should not raise
97
+ end
98
+ end
99
+ end
100
+
101
+ def test_validate_raises_on_checksum_mismatch
102
+ Dir.mktmpdir do |dir|
103
+ gir_path = File.join(dir, "test.gir")
104
+ # Write a fake .gir with a bogus checksum
105
+ File.write(gir_path, <<~GIR)
106
+ # GEMBA INPUT RECORDING v1
107
+ # rom_checksum: 99999
108
+ # game_code: FAKE
109
+ # frame_count: 1
110
+ # anchor_state: test.state
111
+ ---
112
+ 000
113
+ GIR
114
+
115
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
116
+ replayer = Gemba::InputReplayer.new(gir_path)
117
+ assert_raises(Gemba::InputReplayer::ChecksumMismatch) do
118
+ replayer.validate!(player.core)
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # -- Crash resilience --------------------------------------------------------
125
+
126
+ def test_truncated_file_loads_available_frames
127
+ Dir.mktmpdir do |dir|
128
+ gir_path = File.join(dir, "test.gir")
129
+ # Simulate a crash: header says 0 frames but body has 3
130
+ File.write(gir_path, <<~GIR)
131
+ # GEMBA INPUT RECORDING v1
132
+ # rom_checksum: 12345
133
+ # game_code: TEST
134
+ # frame_count: 0
135
+ # anchor_state: test.state
136
+ ---
137
+ 001
138
+ 041
139
+ 000
140
+ GIR
141
+
142
+ replayer = Gemba::InputReplayer.new(gir_path)
143
+ assert_equal 3, replayer.frame_count
144
+ assert_equal 0x001, replayer.bitmask_at(0)
145
+ assert_equal 0x041, replayer.bitmask_at(1)
146
+ assert_equal 0x000, replayer.bitmask_at(2)
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def record(gir_path, bitmasks)
153
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
154
+ player.step(5) # advance a few frames for meaningful state
155
+ core = player.core
156
+ rec = Gemba::InputRecorder.new(gir_path, core: core)
157
+ rec.start
158
+ bitmasks.each { |m| rec.capture(m) }
159
+ rec.stop
160
+ end
161
+ end
162
+ end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest/autorun"
4
- require "gemba"
5
- require_relative "../lib/gemba/config"
6
- require_relative "../lib/gemba/input_mappings"
4
+ require "gemba/headless"
7
5
  require_relative "support/input_mocks"
8
6
 
9
7
  class TestKeyboardMap < Minitest::Test
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "gemba/headless"
5
+
6
+ class TestLibretroBackend < Minitest::Test
7
+ def setup
8
+ Gemba::GameIndex.reset!
9
+ @backend = Gemba::BoxartFetcher::LibretroBackend.new
10
+ end
11
+
12
+ def test_url_for_known_gba_game
13
+ url = @backend.url_for("AGB-AXVE")
14
+ assert_match %r{thumbnails\.libretro\.com}, url
15
+ assert_match %r{Nintendo%20-%20Game%20Boy%20Advance}, url
16
+ assert_match %r{Named_Boxarts}, url
17
+ assert_match %r{Pokemon}, url
18
+ assert url.end_with?(".png")
19
+ end
20
+
21
+ def test_url_for_unknown_game_returns_nil
22
+ assert_nil @backend.url_for("AGB-ZZZZ")
23
+ end
24
+
25
+ def test_url_for_unknown_platform_returns_nil
26
+ assert_nil @backend.url_for("XYZ-AAAA")
27
+ end
28
+
29
+ def test_url_encodes_special_characters
30
+ # Games with special chars (parentheses, ampersands, etc.) should be encoded
31
+ url = @backend.url_for("AGB-AXVE")
32
+ refute_includes url, " " # no raw spaces
33
+ end
34
+
35
+ def test_url_for_gb_game
36
+ url = @backend.url_for("DMG-APAU")
37
+ if url # GB data is sparse
38
+ assert_match %r{Nintendo%20-%20Game%20Boy/}, url
39
+ end
40
+ end
41
+ end
data/test/test_locale.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "minitest/autorun"
4
4
  require "yaml"
5
- require_relative "../lib/gemba/locale"
5
+ require "gemba/headless"
6
6
 
7
7
  class TestMGBALocale < Minitest::Test
8
8
  # -- Loading ---------------------------------------------------------------