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
@@ -5,40 +5,39 @@ require "gemba"
5
5
  require "tmpdir"
6
6
  require "zip"
7
7
 
8
- class TestRomLoader < Minitest::Test
8
+ class TestRomResolver < Minitest::Test
9
9
  TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
10
10
 
11
11
  def setup
12
- skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
13
12
  @tmpdir = Dir.mktmpdir("rom_loader_test")
14
13
  end
15
14
 
16
15
  def teardown
17
16
  FileUtils.rm_rf(@tmpdir) if @tmpdir && File.directory?(@tmpdir)
18
- Gemba::RomLoader.cleanup_temp
17
+ Gemba::RomResolver.cleanup_temp
19
18
  end
20
19
 
21
20
  # -- resolve passthrough --
22
21
 
23
22
  def test_resolve_gba_returns_path_unchanged
24
- assert_equal TEST_ROM, Gemba::RomLoader.resolve(TEST_ROM)
23
+ assert_equal TEST_ROM, Gemba::RomResolver.resolve(TEST_ROM)
25
24
  end
26
25
 
27
26
  def test_resolve_gb_returns_path_unchanged
28
27
  path = "/some/game.gb"
29
- assert_equal path, Gemba::RomLoader.resolve(path)
28
+ assert_equal path, Gemba::RomResolver.resolve(path)
30
29
  end
31
30
 
32
31
  def test_resolve_gbc_returns_path_unchanged
33
32
  path = "/some/game.gbc"
34
- assert_equal path, Gemba::RomLoader.resolve(path)
33
+ assert_equal path, Gemba::RomResolver.resolve(path)
35
34
  end
36
35
 
37
36
  # -- resolve from zip --
38
37
 
39
38
  def test_resolve_zip_extracts_rom
40
39
  zip_path = create_zip("game.zip", "game.gba" => File.binread(TEST_ROM))
41
- result = Gemba::RomLoader.resolve(zip_path)
40
+ result = Gemba::RomResolver.resolve(zip_path)
42
41
 
43
42
  assert File.exist?(result), "extracted ROM should exist"
44
43
  assert_equal ".gba", File.extname(result).downcase
@@ -47,7 +46,7 @@ class TestRomLoader < Minitest::Test
47
46
 
48
47
  def test_resolve_zip_loads_in_core
49
48
  zip_path = create_zip("game.zip", "game.gba" => File.binread(TEST_ROM))
50
- rom_path = Gemba::RomLoader.resolve(zip_path)
49
+ rom_path = Gemba::RomResolver.resolve(zip_path)
51
50
 
52
51
  core = Gemba::Core.new(rom_path)
53
52
  assert_equal "GEMBATEST", core.title
@@ -60,8 +59,8 @@ class TestRomLoader < Minitest::Test
60
59
  def test_resolve_zip_no_rom_raises
61
60
  zip_path = create_zip("empty.zip", "readme.txt" => "hello")
62
61
 
63
- err = assert_raises(Gemba::RomLoader::NoRomInZip) do
64
- Gemba::RomLoader.resolve(zip_path)
62
+ err = assert_raises(Gemba::RomResolver::NoRomInZip) do
63
+ Gemba::RomResolver.resolve(zip_path)
65
64
  end
66
65
  assert_includes err.message, "empty.zip"
67
66
  end
@@ -72,15 +71,15 @@ class TestRomLoader < Minitest::Test
72
71
  "game1.gba" => rom_data,
73
72
  "game2.gba" => rom_data)
74
73
 
75
- err = assert_raises(Gemba::RomLoader::MultipleRomsInZip) do
76
- Gemba::RomLoader.resolve(zip_path)
74
+ err = assert_raises(Gemba::RomResolver::MultipleRomsInZip) do
75
+ Gemba::RomResolver.resolve(zip_path)
77
76
  end
78
77
  assert_includes err.message, "multi.zip"
79
78
  end
80
79
 
81
80
  def test_resolve_unsupported_extension_raises
82
- assert_raises(Gemba::RomLoader::UnsupportedFormat) do
83
- Gemba::RomLoader.resolve("/some/file.rar")
81
+ assert_raises(Gemba::RomResolver::UnsupportedFormat) do
82
+ Gemba::RomResolver.resolve("/some/file.rar")
84
83
  end
85
84
  end
86
85
 
@@ -88,8 +87,8 @@ class TestRomLoader < Minitest::Test
88
87
  corrupt = File.join(@tmpdir, "corrupt.zip")
89
88
  File.binwrite(corrupt, "this is not a zip file")
90
89
 
91
- assert_raises(Gemba::RomLoader::ZipReadError) do
92
- Gemba::RomLoader.resolve(corrupt)
90
+ assert_raises(Gemba::RomResolver::ZipReadError) do
91
+ Gemba::RomResolver.resolve(corrupt)
93
92
  end
94
93
  end
95
94
 
@@ -102,32 +101,32 @@ class TestRomLoader < Minitest::Test
102
101
  zos.write(File.binread(TEST_ROM))
103
102
  end
104
103
 
105
- assert_raises(Gemba::RomLoader::NoRomInZip) do
106
- Gemba::RomLoader.resolve(zip_path)
104
+ assert_raises(Gemba::RomResolver::NoRomInZip) do
105
+ Gemba::RomResolver.resolve(zip_path)
107
106
  end
108
107
  end
109
108
 
110
109
  # -- constants --
111
110
 
112
111
  def test_rom_extensions
113
- assert_includes Gemba::RomLoader::ROM_EXTENSIONS, ".gba"
114
- assert_includes Gemba::RomLoader::ROM_EXTENSIONS, ".gb"
115
- assert_includes Gemba::RomLoader::ROM_EXTENSIONS, ".gbc"
112
+ assert_includes Gemba::RomResolver::ROM_EXTENSIONS, ".gba"
113
+ assert_includes Gemba::RomResolver::ROM_EXTENSIONS, ".gb"
114
+ assert_includes Gemba::RomResolver::ROM_EXTENSIONS, ".gbc"
116
115
  end
117
116
 
118
117
  def test_supported_extensions_includes_zip
119
- assert_includes Gemba::RomLoader::SUPPORTED_EXTENSIONS, ".zip"
118
+ assert_includes Gemba::RomResolver::SUPPORTED_EXTENSIONS, ".zip"
120
119
  end
121
120
 
122
121
  # -- cleanup --
123
122
 
124
123
  def test_cleanup_temp_removes_directory
125
124
  zip_path = create_zip("game.zip", "game.gba" => File.binread(TEST_ROM))
126
- Gemba::RomLoader.resolve(zip_path)
127
- assert File.directory?(Gemba::RomLoader.tmp_dir)
125
+ Gemba::RomResolver.resolve(zip_path)
126
+ assert File.directory?(Gemba::RomResolver.tmp_dir)
128
127
 
129
- Gemba::RomLoader.cleanup_temp
130
- refute File.directory?(Gemba::RomLoader.tmp_dir)
128
+ Gemba::RomResolver.cleanup_temp
129
+ refute File.directory?(Gemba::RomResolver.tmp_dir)
131
130
  end
132
131
 
133
132
  private
@@ -3,9 +3,7 @@
3
3
  require "minitest/autorun"
4
4
  require "tmpdir"
5
5
  require "json"
6
- require_relative "../lib/gemba/config"
7
- require_relative "../lib/gemba/locale"
8
- require_relative "../lib/gemba/save_state_manager"
6
+ require "gemba/headless"
9
7
 
10
8
  class TestSaveStateManager < Minitest::Test
11
9
  # Recording mock for the mGBA Core.
@@ -92,7 +90,7 @@ class TestSaveStateManager < Minitest::Test
92
90
  end
93
91
 
94
92
  def new_manager(core: @core, config: @config, app: @app)
95
- mgr = Gemba::SaveStateManager.new(core: core, config: config, app: app)
93
+ mgr = Gemba::SaveStateManager.new(core: core, config: config, app: app, platform: Gemba::Platform.default)
96
94
  mgr.state_dir = mgr.state_dir_for_rom(core)
97
95
  mgr
98
96
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+
6
+ class TestSettingsAudioTab < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ # -- Volume slider ------------------------------------------------------
10
+
11
+ def test_volume_defaults_to_100
12
+ assert_tk_app("volume defaults to 100") do
13
+ require "gemba/headless"
14
+ sw = Gemba::SettingsWindow.new(app)
15
+ sw.show
16
+ app.update
17
+
18
+ assert_equal '100', app.get_variable(Gemba::Settings::AudioTab::VAR_VOLUME)
19
+ end
20
+ end
21
+
22
+ def test_dragging_volume_to_50_fires_callback
23
+ assert_tk_app("dragging volume to 50 fires on_volume_change") do
24
+ require "gemba/headless"
25
+ received = nil
26
+ Gemba.bus = Gemba::EventBus.new
27
+ Gemba.bus.on(:volume_changed) { |v| received = v }
28
+ sw = Gemba::SettingsWindow.new(app)
29
+ sw.show
30
+ app.update
31
+
32
+ # Simulate user dragging volume slider to 50
33
+ app.command(Gemba::Settings::AudioTab::VOLUME_SCALE, 'set', 50)
34
+ app.update
35
+
36
+ assert_in_delta 0.5, received, 0.01
37
+ end
38
+ end
39
+
40
+ def test_volume_at_zero
41
+ assert_tk_app("volume at zero") do
42
+ require "gemba/headless"
43
+ received = nil
44
+ Gemba.bus = Gemba::EventBus.new
45
+ Gemba.bus.on(:volume_changed) { |v| received = v }
46
+ sw = Gemba::SettingsWindow.new(app)
47
+ sw.show
48
+ app.update
49
+
50
+ app.command(Gemba::Settings::AudioTab::VOLUME_SCALE, 'set', 0)
51
+ app.update
52
+
53
+ assert_in_delta 0.0, received, 0.01
54
+ end
55
+ end
56
+
57
+ # -- Mute checkbox ------------------------------------------------------
58
+
59
+ def test_mute_defaults_to_off
60
+ assert_tk_app("mute defaults to off") do
61
+ require "gemba/headless"
62
+ sw = Gemba::SettingsWindow.new(app)
63
+ sw.show
64
+ app.update
65
+
66
+ assert_equal '0', app.get_variable(Gemba::Settings::AudioTab::VAR_MUTE)
67
+ end
68
+ end
69
+
70
+ def test_clicking_mute_fires_callback
71
+ assert_tk_app("clicking mute fires on_mute_change") do
72
+ require "gemba/headless"
73
+ received = nil
74
+ Gemba.bus = Gemba::EventBus.new
75
+ Gemba.bus.on(:mute_changed) { |m| received = m }
76
+ sw = Gemba::SettingsWindow.new(app)
77
+ sw.show
78
+ app.update
79
+
80
+ # Simulate user clicking the mute checkbox
81
+ app.command(Gemba::Settings::AudioTab::MUTE_CHECK, 'invoke')
82
+ app.update
83
+
84
+ assert_equal true, received
85
+ end
86
+ end
87
+
88
+ def test_clicking_mute_twice_unmutes
89
+ assert_tk_app("clicking mute twice unmutes") do
90
+ require "gemba/headless"
91
+ received = nil
92
+ Gemba.bus = Gemba::EventBus.new
93
+ Gemba.bus.on(:mute_changed) { |m| received = m }
94
+ sw = Gemba::SettingsWindow.new(app)
95
+ sw.show
96
+ app.update
97
+
98
+ app.command(Gemba::Settings::AudioTab::MUTE_CHECK, 'invoke')
99
+ app.update
100
+ assert_equal true, received
101
+
102
+ app.command(Gemba::Settings::AudioTab::MUTE_CHECK, 'invoke')
103
+ app.update
104
+ assert_equal false, received
105
+ end
106
+ end
107
+ end
@@ -8,9 +8,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
8
8
 
9
9
  def test_hotkeys_tab_exists
10
10
  assert_tk_app("hotkeys tab exists in notebook") do
11
- require "gemba/settings_window"
12
- require "gemba/hotkey_map"
13
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
11
+ require "gemba/headless"
12
+ require "gemba/headless"
13
+ sw = Gemba::SettingsWindow.new(app)
14
14
  sw.show
15
15
  app.update
16
16
 
@@ -21,9 +21,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
21
21
 
22
22
  def test_hotkey_buttons_show_default_keysyms
23
23
  assert_tk_app("hotkey buttons show default keysyms") do
24
- require "gemba/settings_window"
25
- require "gemba/hotkey_map"
26
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
24
+ require "gemba/headless"
25
+ require "gemba/headless"
26
+ sw = Gemba::SettingsWindow.new(app)
27
27
  sw.show
28
28
  app.update
29
29
 
@@ -40,9 +40,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
40
40
 
41
41
  def test_clicking_hotkey_button_enters_listen_mode
42
42
  assert_tk_app("clicking hotkey button enters listen mode") do
43
- require "gemba/settings_window"
44
- require "gemba/hotkey_map"
45
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
43
+ require "gemba/headless"
44
+ require "gemba/headless"
45
+ sw = Gemba::SettingsWindow.new(app)
46
46
  sw.show
47
47
  app.update
48
48
 
@@ -57,13 +57,13 @@ class TestMGBASettingsHotkeys < Minitest::Test
57
57
 
58
58
  def test_capture_updates_label_and_fires_callback
59
59
  assert_tk_app("capturing hotkey updates label and fires callback") do
60
- require "gemba/settings_window"
61
- require "gemba/hotkey_map"
60
+ require "gemba/headless"
61
+ require "gemba/headless"
62
62
  received_action = nil
63
63
  received_key = nil
64
- sw = Gemba::SettingsWindow.new(app, callbacks: {
65
- on_hotkey_change: proc { |a, k| received_action = a; received_key = k }
66
- })
64
+ Gemba.bus = Gemba::EventBus.new
65
+ Gemba.bus.on(:hotkey_changed) { |a, k| received_action = a; received_key = k }
66
+ sw = Gemba::SettingsWindow.new(app)
67
67
  sw.show
68
68
  app.update
69
69
 
@@ -85,9 +85,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
85
85
 
86
86
  def test_capture_enables_undo_button
87
87
  assert_tk_app("capturing hotkey enables undo button") do
88
- require "gemba/settings_window"
89
- require "gemba/hotkey_map"
90
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
88
+ require "gemba/headless"
89
+ require "gemba/headless"
90
+ sw = Gemba::SettingsWindow.new(app)
91
91
  sw.show
92
92
  app.update
93
93
 
@@ -108,12 +108,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
108
108
 
109
109
  def test_undo_fires_callback_and_disables
110
110
  assert_tk_app("undo fires on_undo_hotkeys and disables button") do
111
- require "gemba/settings_window"
112
- require "gemba/hotkey_map"
111
+ require "gemba/headless"
112
+ require "gemba/headless"
113
113
  undo_called = false
114
- sw = Gemba::SettingsWindow.new(app, callbacks: {
115
- on_undo_hotkeys: proc { undo_called = true }
116
- })
114
+ Gemba.bus = Gemba::EventBus.new
115
+ Gemba.bus.on(:undo_hotkeys) { undo_called = true }
116
+ sw = Gemba::SettingsWindow.new(app)
117
117
  sw.show
118
118
  app.update
119
119
 
@@ -135,11 +135,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
135
135
 
136
136
  def test_reset_restores_defaults
137
137
  assert_tk_app("reset restores default hotkey labels") do
138
- require "gemba/settings_window"
139
- require "gemba/hotkey_map"
138
+ require "gemba/headless"
139
+ require "gemba/headless"
140
140
  reset_called = false
141
+ Gemba.bus = Gemba::EventBus.new
142
+ Gemba.bus.on(:hotkey_reset) { reset_called = true }
141
143
  sw = Gemba::SettingsWindow.new(app, callbacks: {
142
- on_hotkey_reset: proc { reset_called = true },
143
144
  on_confirm_reset_hotkeys: -> { true },
144
145
  })
145
146
  sw.show
@@ -168,9 +169,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
168
169
 
169
170
  def test_refresh_hotkeys_updates_labels
170
171
  assert_tk_app("refresh_hotkeys updates button labels") do
171
- require "gemba/settings_window"
172
- require "gemba/hotkey_map"
173
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
172
+ require "gemba/headless"
173
+ require "gemba/headless"
174
+ sw = Gemba::SettingsWindow.new(app)
174
175
  sw.show
175
176
  app.update
176
177
 
@@ -187,9 +188,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
187
188
 
188
189
  def test_cancel_listen_restores_label
189
190
  assert_tk_app("canceling listen restores original label") do
190
- require "gemba/settings_window"
191
- require "gemba/hotkey_map"
192
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
191
+ require "gemba/headless"
192
+ require "gemba/headless"
193
+ sw = Gemba::SettingsWindow.new(app)
193
194
  sw.show
194
195
  app.update
195
196
 
@@ -210,12 +211,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
210
211
 
211
212
  def test_capture_without_listen_is_noop
212
213
  assert_tk_app("capture without listen mode is a no-op") do
213
- require "gemba/settings_window"
214
- require "gemba/hotkey_map"
214
+ require "gemba/headless"
215
+ require "gemba/headless"
215
216
  received = false
216
- sw = Gemba::SettingsWindow.new(app, callbacks: {
217
- on_hotkey_change: proc { |*| received = true }
218
- })
217
+ Gemba.bus = Gemba::EventBus.new
218
+ Gemba.bus.on(:hotkey_changed) { |*| received = true }
219
+ sw = Gemba::SettingsWindow.new(app)
219
220
  sw.show
220
221
  app.update
221
222
 
@@ -233,12 +234,13 @@ class TestMGBASettingsHotkeys < Minitest::Test
233
234
 
234
235
  def test_hotkey_rejected_when_conflicting_with_gamepad_key
235
236
  assert_tk_app("hotkey rejected when key conflicts with gamepad mapping") do
236
- require "gemba/settings_window"
237
- require "gemba/hotkey_map"
237
+ require "gemba/headless"
238
+ require "gemba/headless"
238
239
  received = false
239
240
  conflict_msg = nil
241
+ Gemba.bus = Gemba::EventBus.new
242
+ Gemba.bus.on(:hotkey_changed) { |*| received = true }
240
243
  sw = Gemba::SettingsWindow.new(app, callbacks: {
241
- on_hotkey_change: proc { |*| received = true },
242
244
  on_validate_hotkey: ->(keysym) {
243
245
  # Simulate: 'z' is GBA button A
244
246
  keysym == 'z' ? '"z" is mapped to GBA button A' : nil
@@ -265,11 +267,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
265
267
 
266
268
  def test_hotkey_accepted_when_no_conflict
267
269
  assert_tk_app("hotkey accepted when no conflict") do
268
- require "gemba/settings_window"
269
- require "gemba/hotkey_map"
270
+ require "gemba/headless"
271
+ require "gemba/headless"
270
272
  received_action = nil
273
+ Gemba.bus = Gemba::EventBus.new
274
+ Gemba.bus.on(:hotkey_changed) { |a, _| received_action = a }
271
275
  sw = Gemba::SettingsWindow.new(app, callbacks: {
272
- on_hotkey_change: proc { |a, _| received_action = a },
273
276
  on_validate_hotkey: ->(_) { nil },
274
277
  })
275
278
  sw.show
@@ -290,13 +293,13 @@ class TestMGBASettingsHotkeys < Minitest::Test
290
293
 
291
294
  def test_capture_modifier_then_key_produces_combo
292
295
  assert_tk_app("modifier + key produces combo hotkey") do
293
- require "gemba/settings_window"
294
- require "gemba/hotkey_map"
296
+ require "gemba/headless"
297
+ require "gemba/headless"
295
298
  received_action = nil
296
299
  received_hk = nil
297
- sw = Gemba::SettingsWindow.new(app, callbacks: {
298
- on_hotkey_change: proc { |a, hk| received_action = a; received_hk = hk },
299
- })
300
+ Gemba.bus = Gemba::EventBus.new
301
+ Gemba.bus.on(:hotkey_changed) { |a, hk| received_action = a; received_hk = hk }
302
+ sw = Gemba::SettingsWindow.new(app)
300
303
  sw.show
301
304
  app.update
302
305
 
@@ -319,12 +322,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
319
322
 
320
323
  def test_capture_multi_modifier_combo
321
324
  assert_tk_app("multi-modifier combo (Ctrl+Shift+S)") do
322
- require "gemba/settings_window"
323
- require "gemba/hotkey_map"
325
+ require "gemba/headless"
326
+ require "gemba/headless"
324
327
  received_hk = nil
325
- sw = Gemba::SettingsWindow.new(app, callbacks: {
326
- on_hotkey_change: proc { |_, hk| received_hk = hk },
327
- })
328
+ Gemba.bus = Gemba::EventBus.new
329
+ Gemba.bus.on(:hotkey_changed) { |_, hk| received_hk = hk }
330
+ sw = Gemba::SettingsWindow.new(app)
328
331
  sw.show
329
332
  app.update
330
333
 
@@ -344,11 +347,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
344
347
 
345
348
  def test_combo_hotkey_skips_gamepad_conflict_validation
346
349
  assert_tk_app("combo hotkey skips gamepad conflict validation") do
347
- require "gemba/settings_window"
348
- require "gemba/hotkey_map"
350
+ require "gemba/headless"
351
+ require "gemba/headless"
349
352
  received_hk = nil
353
+ Gemba.bus = Gemba::EventBus.new
354
+ Gemba.bus.on(:hotkey_changed) { |_, hk| received_hk = hk }
350
355
  sw = Gemba::SettingsWindow.new(app, callbacks: {
351
- on_hotkey_change: proc { |_, hk| received_hk = hk },
352
356
  # 'z' conflicts as a plain key, but Ctrl+z should be fine
353
357
  on_validate_hotkey: ->(key) {
354
358
  key == 'z' ? '"z" is mapped to GBA button A' : nil
@@ -370,9 +374,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
370
374
 
371
375
  def test_refresh_hotkeys_shows_combo_display_name
372
376
  assert_tk_app("refresh_hotkeys shows combo display name") do
373
- require "gemba/settings_window"
374
- require "gemba/hotkey_map"
375
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
377
+ require "gemba/headless"
378
+ require "gemba/headless"
379
+ sw = Gemba::SettingsWindow.new(app)
376
380
  sw.show
377
381
  app.update
378
382
 
@@ -387,12 +391,12 @@ class TestMGBASettingsHotkeys < Minitest::Test
387
391
 
388
392
  def test_bind_script_modifier_combo_roundtrip
389
393
  assert_tk_app("Tcl bind script round-trip with modifier+key combo") do
390
- require "gemba/settings_window"
391
- require "gemba/hotkey_map"
394
+ require "gemba/headless"
395
+ require "gemba/headless"
392
396
  received_hk = nil
393
- sw = Gemba::SettingsWindow.new(app, callbacks: {
394
- on_hotkey_change: proc { |_, hk| received_hk = hk },
395
- })
397
+ Gemba.bus = Gemba::EventBus.new
398
+ Gemba.bus.on(:hotkey_changed) { |_, hk| received_hk = hk }
399
+ sw = Gemba::SettingsWindow.new(app)
396
400
  sw.show
397
401
  app.update
398
402
 
@@ -421,9 +425,9 @@ class TestMGBASettingsHotkeys < Minitest::Test
421
425
 
422
426
  def test_record_hotkey_button_shows_default
423
427
  assert_tk_app("record hotkey button shows F10") do
424
- require "gemba/settings_window"
425
- require "gemba/hotkey_map"
426
- sw = Gemba::SettingsWindow.new(app, callbacks: {})
428
+ require "gemba/headless"
429
+ require "gemba/headless"
430
+ sw = Gemba::SettingsWindow.new(app)
427
431
  sw.show
428
432
  app.update
429
433
 
@@ -431,4 +435,17 @@ class TestMGBASettingsHotkeys < Minitest::Test
431
435
  assert_equal 'F10', text
432
436
  end
433
437
  end
438
+
439
+ def test_open_rom_hotkey_button_shows_default
440
+ assert_tk_app("open rom hotkey button shows Ctrl+O") do
441
+ require "gemba/headless"
442
+ require "gemba/headless"
443
+ sw = Gemba::SettingsWindow.new(app)
444
+ sw.show
445
+ app.update
446
+
447
+ text = app.command(Gemba::SettingsWindow::HK_ACTIONS[:open_rom], 'cget', '-text')
448
+ assert_equal 'Ctrl+O', text
449
+ end
450
+ end
434
451
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+
6
+ class TestSettingsRecordingTab < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ def test_recording_tab_exists
10
+ assert_tk_app("recording tab exists in notebook") do
11
+ require "gemba/headless"
12
+ sw = Gemba::SettingsWindow.new(app)
13
+ sw.show
14
+ app.update
15
+
16
+ tabs = app.command(Gemba::Settings::Paths::NB, 'tabs')
17
+ assert_includes tabs, Gemba::Settings::RecordingTab::FRAME
18
+ end
19
+ end
20
+
21
+ def test_compression_combobox_defaults_to_1
22
+ assert_tk_app("compression combobox defaults to 1") do
23
+ require "gemba/headless"
24
+ sw = Gemba::SettingsWindow.new(app)
25
+ sw.show
26
+ app.update
27
+
28
+ assert_equal '1', app.get_variable(Gemba::Settings::RecordingTab::VAR_COMPRESSION)
29
+ end
30
+ end
31
+
32
+ def test_selecting_compression_fires_callback
33
+ assert_tk_app("selecting compression fires on_compression_change") do
34
+ require "gemba/headless"
35
+ received = nil
36
+ Gemba.bus = Gemba::EventBus.new
37
+ Gemba.bus.on(:compression_changed) { |v| received = v }
38
+ sw = Gemba::SettingsWindow.new(app)
39
+ sw.show
40
+ app.update
41
+
42
+ app.set_variable(Gemba::Settings::RecordingTab::VAR_COMPRESSION, '6')
43
+ app.command(:event, 'generate', Gemba::Settings::RecordingTab::COMPRESSION_COMBO, '<<ComboboxSelected>>')
44
+ app.update
45
+
46
+ assert_equal 6, received
47
+ end
48
+ end
49
+ end