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,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/achievements"
6
+ require_relative "support/fake_core"
7
+
8
+ class TestFakeBackendAchievements < Minitest::Test
9
+ ADDR = 0x02000000
10
+
11
+ def setup
12
+ @backend = Gemba::Achievements::FakeBackend.new
13
+ @backend.add_achievement(id: 'test', title: 'Test', description: 'desc', points: 5) do |mem|
14
+ mem.call(ADDR) == 0x42
15
+ end
16
+ @core = FakeCore.new
17
+ @unlocked = []
18
+ @backend.on_unlock { |ach| @unlocked << ach }
19
+ end
20
+
21
+ def test_no_unlock_when_condition_false
22
+ @core.poke(ADDR, 0x00)
23
+ @backend.do_frame(@core)
24
+ assert_empty @unlocked
25
+ end
26
+
27
+ def test_unlock_fires_when_condition_becomes_true
28
+ @core.poke(ADDR, 0x42)
29
+ @backend.do_frame(@core)
30
+ assert_equal 1, @unlocked.size
31
+ assert_equal 'test', @unlocked.first.id
32
+ assert_equal 'Test', @unlocked.first.title
33
+ assert_equal 5, @unlocked.first.points
34
+ assert @unlocked.first.earned?
35
+ end
36
+
37
+ def test_unlock_fires_only_once_rising_edge
38
+ @core.poke(ADDR, 0x42)
39
+ @backend.do_frame(@core)
40
+ @backend.do_frame(@core)
41
+ assert_equal 1, @unlocked.size
42
+ end
43
+
44
+ def test_no_second_unlock_after_condition_cycles
45
+ @core.poke(ADDR, 0x42)
46
+ @backend.do_frame(@core)
47
+ @core.poke(ADDR, 0x00)
48
+ @backend.do_frame(@core)
49
+ @core.poke(ADDR, 0x42)
50
+ @backend.do_frame(@core)
51
+ assert_equal 1, @unlocked.size
52
+ end
53
+
54
+ def test_fires_on_first_true_frame
55
+ @core.poke(ADDR, 0x42)
56
+ @backend.do_frame(@core)
57
+ assert_equal 1, @unlocked.size
58
+ end
59
+
60
+ def test_no_unlock_on_wrong_value
61
+ @core.poke(ADDR, 0x01)
62
+ @backend.do_frame(@core)
63
+ assert_empty @unlocked
64
+ end
65
+
66
+ def test_achievement_list_reflects_earned_state
67
+ assert_equal 1, @backend.total_count
68
+ assert_equal 0, @backend.earned_count
69
+
70
+ @core.poke(ADDR, 0x42)
71
+ @backend.do_frame(@core)
72
+
73
+ assert_equal 1, @backend.earned_count
74
+ assert @backend.achievement_list.first.earned?
75
+ end
76
+
77
+ def test_multiple_achievements_independent
78
+ @backend.add_achievement(id: 'two', title: 'Two', description: '', points: 10) do |mem|
79
+ mem.call(ADDR + 1) == 0xFF
80
+ end
81
+
82
+ @core.poke(ADDR, 0x42)
83
+ @backend.do_frame(@core)
84
+ assert_equal 1, @unlocked.size
85
+ assert_equal 'test', @unlocked.first.id
86
+
87
+ @core.poke(ADDR + 1, 0xFF)
88
+ @backend.do_frame(@core)
89
+ assert_equal 2, @unlocked.size
90
+ assert_equal 'two', @unlocked.last.id
91
+ end
92
+
93
+ def test_reset_earned_allows_re_unlock
94
+ @core.poke(ADDR, 0x42)
95
+ @backend.do_frame(@core)
96
+ assert_equal 1, @backend.earned_count
97
+
98
+ @backend.reset_earned
99
+ assert_equal 0, @backend.earned_count
100
+
101
+ @unlocked.clear
102
+ @core.poke(ADDR, 0x00)
103
+ @backend.do_frame(@core)
104
+ @core.poke(ADDR, 0x42)
105
+ @backend.do_frame(@core)
106
+ assert_equal 1, @unlocked.size
107
+ end
108
+
109
+ def test_load_game_resets_earned
110
+ @core.poke(ADDR, 0x42)
111
+ @backend.do_frame(@core)
112
+ assert_equal 1, @backend.earned_count
113
+
114
+ @backend.load_game(@core)
115
+ assert_equal 0, @backend.earned_count
116
+ end
117
+
118
+ def test_multiple_unlock_callbacks_all_fire
119
+ other = []
120
+ @backend.on_unlock { |ach| other << ach.id }
121
+ @core.poke(ADDR, 0x42)
122
+ @backend.do_frame(@core)
123
+ assert_equal ['test'], @unlocked.map(&:id)
124
+ assert_equal ['test'], other
125
+ end
126
+
127
+ def test_enabled
128
+ assert @backend.enabled?
129
+ end
130
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/achievements"
6
+
7
+ class TestFakeBackendAuth < Minitest::Test
8
+ def test_any_nonempty_creds_succeed_by_default
9
+ b = Gemba::Achievements::FakeBackend.new
10
+ statuses = []
11
+ b.on_auth_change { |status, _| statuses << status }
12
+ b.login_with_token(username: 'alice', token: 'anything')
13
+ assert b.authenticated?
14
+ assert_equal [:ok], statuses
15
+ end
16
+
17
+ def test_empty_username_fails
18
+ b = Gemba::Achievements::FakeBackend.new
19
+ b.login_with_token(username: '', token: 'tok')
20
+ refute b.authenticated?
21
+ end
22
+
23
+ def test_empty_token_fails
24
+ b = Gemba::Achievements::FakeBackend.new
25
+ b.login_with_token(username: 'alice', token: '')
26
+ refute b.authenticated?
27
+ end
28
+
29
+ def test_restricted_correct_pair_succeeds
30
+ b = Gemba::Achievements::FakeBackend.new(valid_username: 'alice', valid_token: 'secret')
31
+ b.login_with_token(username: 'alice', token: 'secret')
32
+ assert b.authenticated?
33
+ end
34
+
35
+ def test_restricted_wrong_user_fails
36
+ b = Gemba::Achievements::FakeBackend.new(valid_username: 'alice', valid_token: 'secret')
37
+ statuses = []
38
+ errors = []
39
+ b.on_auth_change { |s, e| statuses << s; errors << e }
40
+ b.login_with_token(username: 'bob', token: 'secret')
41
+ refute b.authenticated?
42
+ assert_equal [:error], statuses
43
+ refute_nil errors.first
44
+ end
45
+
46
+ def test_restricted_wrong_token_fails
47
+ b = Gemba::Achievements::FakeBackend.new(valid_username: 'alice', valid_token: 'secret')
48
+ b.login_with_token(username: 'alice', token: 'wrong')
49
+ refute b.authenticated?
50
+ end
51
+
52
+ def test_logout_clears_auth
53
+ b = Gemba::Achievements::FakeBackend.new
54
+ b.login_with_token(username: 'alice', token: 'tok')
55
+ assert b.authenticated?
56
+ b.logout
57
+ refute b.authenticated?
58
+ end
59
+
60
+ def test_multiple_auth_callbacks_all_fired
61
+ b = Gemba::Achievements::FakeBackend.new
62
+ results = []
63
+ b.on_auth_change { |s, _| results << "cb1:#{s}" }
64
+ b.on_auth_change { |s, _| results << "cb2:#{s}" }
65
+ b.login_with_token(username: 'u', token: 't')
66
+ assert_equal ['cb1:ok', 'cb2:ok'], results
67
+ end
68
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "gemba/headless"
5
+
6
+ class TestGameIndex < Minitest::Test
7
+ def setup
8
+ Gemba::GameIndex.reset!
9
+ end
10
+
11
+ # -- lookup (by game code) -----------------------------------------------
12
+
13
+ def test_lookup_known_gba_game
14
+ name = Gemba::GameIndex.lookup("AGB-AXVE")
15
+ assert_includes name, "Pokemon"
16
+ assert_includes name, "Ruby"
17
+ end
18
+
19
+ def test_lookup_returns_nil_for_unknown_code
20
+ assert_nil Gemba::GameIndex.lookup("AGB-ZZZZ")
21
+ end
22
+
23
+ def test_lookup_returns_nil_for_nil_input
24
+ assert_nil Gemba::GameIndex.lookup(nil)
25
+ end
26
+
27
+ def test_lookup_returns_nil_for_empty_string
28
+ assert_nil Gemba::GameIndex.lookup("")
29
+ end
30
+
31
+ def test_lookup_returns_nil_for_unknown_platform_prefix
32
+ assert_nil Gemba::GameIndex.lookup("XYZ-AAAA")
33
+ end
34
+
35
+ def test_lookup_known_gbc_game
36
+ # Pokemon Gold is a well-known GBC title
37
+ name = Gemba::GameIndex.lookup("CGB-BYEE")
38
+ assert_includes name, "Pokemon" if name # only assert content if present
39
+ # At minimum it must not raise
40
+ end
41
+
42
+ # -- lookup_by_md5 -------------------------------------------------------
43
+
44
+ def test_lookup_by_md5_returns_nil_for_nil_md5
45
+ assert_nil Gemba::GameIndex.lookup_by_md5(nil, "gba")
46
+ end
47
+
48
+ def test_lookup_by_md5_returns_nil_for_empty_md5
49
+ assert_nil Gemba::GameIndex.lookup_by_md5("", "gba")
50
+ end
51
+
52
+ def test_lookup_by_md5_returns_nil_for_unknown_platform
53
+ assert_nil Gemba::GameIndex.lookup_by_md5("abc123", "snes")
54
+ end
55
+
56
+ def test_lookup_by_md5_returns_nil_for_unknown_digest
57
+ assert_nil Gemba::GameIndex.lookup_by_md5("0" * 32, "gba")
58
+ end
59
+
60
+ def test_lookup_by_md5_is_case_insensitive
61
+ # Use a real digest present in gba_md5.json so both lookups must return a non-nil title
62
+ md5 = "0007d212d9b76a466c7ca003d50c8c74"
63
+ lower = Gemba::GameIndex.lookup_by_md5(md5.downcase, "gba")
64
+ upper = Gemba::GameIndex.lookup_by_md5(md5.upcase, "gba")
65
+ refute_nil lower, "lowercase MD5 lookup must return a title"
66
+ assert_equal lower, upper
67
+ end
68
+
69
+ # -- reset! / caching ----------------------------------------------------
70
+
71
+ def test_reset_clears_cache_and_reloads
72
+ Gemba::GameIndex.lookup("AGB-AXVE")
73
+ Gemba::GameIndex.reset!
74
+ name = Gemba::GameIndex.lookup("AGB-AXVE")
75
+ assert_includes name, "Pokemon"
76
+ end
77
+ end
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+
6
+ class TestGamePickerFrame < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ # Each test gets a fresh picker. Since Teek::TestWorker persists across tests,
10
+ # we must destroy .game_picker at the end of each test so the next test can
11
+ # recreate it cleanly (ttk::frame fails if the path already exists).
12
+ def cleanup_picker(app)
13
+ app.command(:destroy, '.game_picker') rescue nil
14
+ end
15
+
16
+ def test_empty_library_shows_all_hollow_cards
17
+ assert_tk_app("empty library shows all hollow cards") do
18
+ require "gemba/headless"
19
+
20
+ lib = Struct.new(:roms) { def all = roms }.new([])
21
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
22
+ picker.show
23
+
24
+ 16.times do |i|
25
+ title = app.command(".game_picker.card#{i}.title", :cget, '-text')
26
+ assert_equal '', title, "Card #{i} title should be empty"
27
+
28
+ img = app.command(".game_picker.card#{i}.img", :cget, '-image')
29
+ assert_equal 'boxart_placeholder', img, "Card #{i} should show placeholder image"
30
+
31
+ bg = app.command(".game_picker.card#{i}", :cget, '-bg')
32
+ refute_equal '#2a2a2a', bg, "Card #{i} should not have populated background"
33
+ end
34
+
35
+ picker.cleanup
36
+ app.command(:destroy, '.game_picker') rescue nil
37
+ end
38
+ end
39
+
40
+ def test_populated_card_shows_title_and_platform
41
+ assert_tk_app("populated card shows title and platform text") do
42
+ require "gemba/headless"
43
+
44
+ rom = { 'title' => 'Pokemon Ruby', 'platform' => 'gba',
45
+ 'game_code' => 'AGB-AXVE', 'path' => '/games/ruby.gba' }
46
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
47
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
48
+ picker.show
49
+
50
+ assert_equal 'Pokemon - Ruby Version (USA, Europe)', app.command('.game_picker.card0.title', :cget, '-text')
51
+ assert_equal 'GBA', app.command('.game_picker.card0.plat', :cget, '-text')
52
+
53
+ picker.cleanup
54
+ app.command(:destroy, '.game_picker') rescue nil
55
+ end
56
+ end
57
+
58
+ def test_platform_is_uppercased
59
+ assert_tk_app("platform label is uppercased") do
60
+ require "gemba/headless"
61
+
62
+ rom = { 'title' => 'Tetris', 'platform' => 'gbc', 'path' => '/games/tetris.gbc' }
63
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
64
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
65
+ picker.show
66
+
67
+ assert_equal 'GBC', app.command('.game_picker.card0.plat', :cget, '-text')
68
+
69
+ picker.cleanup
70
+ app.command(:destroy, '.game_picker') rescue nil
71
+ end
72
+ end
73
+
74
+ def test_title_falls_back_to_rom_id_when_title_missing
75
+ assert_tk_app("title falls back to rom_id when title key absent") do
76
+ require "gemba/headless"
77
+
78
+ rom = { 'rom_id' => 'MY-ROM', 'platform' => 'gba', 'path' => '/games/x.gba' }
79
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
80
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
81
+ picker.show
82
+
83
+ assert_equal 'MY-ROM', app.command('.game_picker.card0.title', :cget, '-text')
84
+
85
+ picker.cleanup
86
+ app.command(:destroy, '.game_picker') rescue nil
87
+ end
88
+ end
89
+
90
+ def test_populated_card_background_differs_from_hollow
91
+ assert_tk_app("populated card has different background color than hollow card") do
92
+ require "gemba/headless"
93
+
94
+ rom = { 'title' => 'Test Game', 'platform' => 'gba', 'path' => '/games/test.gba' }
95
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
96
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
97
+ picker.show
98
+
99
+ pop_bg = app.command('.game_picker.card0', :cget, '-bg')
100
+ hollow_bg = app.command('.game_picker.card1', :cget, '-bg')
101
+
102
+ assert_equal '#2a2a2a', pop_bg, "Populated card background"
103
+ refute_equal pop_bg, hollow_bg, "Hollow card should differ from populated"
104
+
105
+ picker.cleanup
106
+ app.command(:destroy, '.game_picker') rescue nil
107
+ end
108
+ end
109
+
110
+ def test_multiple_roms_populate_correct_cards_in_order
111
+ assert_tk_app("multiple ROMs fill cards in order; remainder are hollow") do
112
+ require "gemba/headless"
113
+
114
+ roms = [
115
+ { 'title' => 'Alpha', 'platform' => 'gba', 'path' => '/a.gba' },
116
+ { 'title' => 'Beta', 'platform' => 'gbc', 'path' => '/b.gbc' },
117
+ ]
118
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
119
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
120
+ picker.show
121
+
122
+ assert_equal 'Alpha', app.command('.game_picker.card0.title', :cget, '-text')
123
+ assert_equal 'GBA', app.command('.game_picker.card0.plat', :cget, '-text')
124
+ assert_equal 'Beta', app.command('.game_picker.card1.title', :cget, '-text')
125
+ assert_equal 'GBC', app.command('.game_picker.card1.plat', :cget, '-text')
126
+ assert_equal '', app.command('.game_picker.card2.title', :cget, '-text')
127
+
128
+ picker.cleanup
129
+ app.command(:destroy, '.game_picker') rescue nil
130
+ end
131
+ end
132
+
133
+ def test_pre_cached_boxart_shown_immediately_without_network
134
+ assert_tk_app("pre-cached boxart image is set on the card without any network fetch") do
135
+ require "gemba/headless"
136
+ require "gemba/headless"
137
+ require "tmpdir"
138
+ require "fileutils"
139
+
140
+ game_code = 'AGB-AXVE'
141
+ tmpdir = Dir.mktmpdir('picker_test')
142
+ cache_dir = File.join(tmpdir, game_code)
143
+ FileUtils.mkdir_p(cache_dir)
144
+ # Re-use the placeholder PNG — it's a real PNG Tk already loads successfully
145
+ FileUtils.cp(Gemba::GamePickerFrame::PLACEHOLDER_PNG, File.join(cache_dir, 'boxart.png'))
146
+
147
+ # Backend that would return a URL, but the cache hit means it's never called
148
+ backend = Struct.new(:url) { def url_for(_) = url }.new('https://example.com/fake.png')
149
+ fetcher = Gemba::BoxartFetcher.new(app: app, cache_dir: tmpdir, backend: backend)
150
+
151
+ rom = { 'title' => 'Pokemon Ruby', 'platform' => 'gba',
152
+ 'game_code' => game_code, 'path' => '/fake.gba' }
153
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
154
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib, boxart_fetcher: fetcher)
155
+ picker.show
156
+
157
+ img_name = app.command('.game_picker.card0.img', :cget, '-image')
158
+ assert_equal "boxart_#{game_code}", img_name,
159
+ "Card should display cached boxart image, not the placeholder"
160
+
161
+ FileUtils.rm_rf(tmpdir)
162
+ picker.cleanup
163
+ app.command(:destroy, '.game_picker') rescue nil
164
+ end
165
+ end
166
+
167
+ def test_no_fetcher_leaves_placeholder_on_card
168
+ assert_tk_app("card with game_code but no fetcher stays on placeholder") do
169
+ require "gemba/headless"
170
+
171
+ rom = { 'title' => 'Some Game', 'platform' => 'gba',
172
+ 'game_code' => 'AGB-TEST', 'path' => '/games/some.gba' }
173
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
174
+ # No boxart_fetcher passed
175
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
176
+ picker.show
177
+
178
+ img_name = app.command('.game_picker.card0.img', :cget, '-image')
179
+ assert_equal 'boxart_placeholder', img_name
180
+
181
+ picker.cleanup
182
+ app.command(:destroy, '.game_picker') rescue nil
183
+ end
184
+ end
185
+
186
+ # -- Quick Load context menu ------------------------------------------------
187
+ # Menu entry indices: 0=Play, 1=Quick Load, 2=Set Boxart, 3=separator, 4=Remove
188
+
189
+ def test_quick_load_disabled_when_no_save_state
190
+ assert_tk_app("quick load menu entry is disabled when no .ss file exists") do
191
+ require "gemba/headless"
192
+ require "tmpdir"
193
+
194
+ Dir.mktmpdir("picker_qs_test") do |tmpdir|
195
+ rom_id = "AGB-TEST-DEADBEEF"
196
+ rom = { 'title' => 'Test Game', 'platform' => 'gba',
197
+ 'rom_id' => rom_id, 'game_code' => 'AGB-TEST', 'path' => '/games/test.gba' }
198
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
199
+
200
+ Gemba.user_config.states_dir = tmpdir
201
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
202
+ picker.show
203
+ app.show
204
+ app.update
205
+
206
+ # Suppress tk_popup so the right-click binding configures menu entries
207
+ # without posting the menu (no platform grab → no blocking app.update).
208
+ override_tk_popup do
209
+ app.tcl_eval("event generate .game_picker.card0 <Button-3> -x 10 -y 10")
210
+ app.update
211
+ end
212
+
213
+ # index 1 = Quick Load
214
+ state = app.tcl_eval(".game_picker.card0.ctx entrycget 1 -state")
215
+ assert_equal 'disabled', state
216
+
217
+ picker.cleanup
218
+ app.command(:destroy, '.game_picker') rescue nil
219
+ end
220
+ end
221
+ end
222
+
223
+ def test_quick_load_enabled_when_save_state_exists
224
+ assert_tk_app("quick load menu entry is enabled when .ss file exists") do
225
+ require "gemba/headless"
226
+ require "tmpdir"
227
+ require "fileutils"
228
+
229
+ fixture = File.expand_path("test/fixtures/test_quicksave.ss")
230
+
231
+ Dir.mktmpdir("picker_qs_test") do |tmpdir|
232
+ rom_id = "AGB-TEST-DEADBEEF"
233
+ slot = Gemba.user_config.quick_save_slot
234
+ state_dir = File.join(tmpdir, rom_id)
235
+ FileUtils.mkdir_p(state_dir)
236
+ FileUtils.cp(fixture, File.join(state_dir, "state#{slot}.ss"))
237
+
238
+ rom = { 'title' => 'Test Game', 'platform' => 'gba',
239
+ 'rom_id' => rom_id, 'game_code' => 'AGB-TEST', 'path' => '/games/test.gba' }
240
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
241
+
242
+ Gemba.user_config.states_dir = tmpdir
243
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
244
+ picker.show
245
+ app.show
246
+ app.update
247
+
248
+ override_tk_popup do
249
+ app.tcl_eval("event generate .game_picker.card0 <Button-3> -x 10 -y 10")
250
+ app.update
251
+ end
252
+
253
+ # index 1 = Quick Load
254
+ state = app.tcl_eval(".game_picker.card0.ctx entrycget 1 -state")
255
+ assert_equal 'normal', state
256
+
257
+ picker.cleanup
258
+ app.command(:destroy, '.game_picker') rescue nil
259
+ end
260
+ end
261
+ end
262
+
263
+ def test_quick_load_emits_rom_quick_load_event
264
+ assert_tk_app("clicking quick load emits :rom_quick_load with path and slot") do
265
+ require "gemba/headless"
266
+ require "tmpdir"
267
+ require "fileutils"
268
+
269
+ fixture = File.expand_path("test/fixtures/test_quicksave.ss")
270
+
271
+ Dir.mktmpdir("picker_qs_test") do |tmpdir|
272
+ rom_id = "AGB-TEST-DEADBEEF"
273
+ slot = Gemba.user_config.quick_save_slot
274
+ state_dir = File.join(tmpdir, rom_id)
275
+ FileUtils.mkdir_p(state_dir)
276
+ FileUtils.cp(fixture, File.join(state_dir, "state#{slot}.ss"))
277
+
278
+ rom_path = '/games/test.gba'
279
+ rom = { 'title' => 'Test Game', 'platform' => 'gba',
280
+ 'rom_id' => rom_id, 'game_code' => 'AGB-TEST', 'path' => rom_path }
281
+ lib = Struct.new(:roms) { def all = roms }.new([rom])
282
+
283
+ received = nil
284
+ Gemba.bus.on(:rom_quick_load) { |**kwargs| received = kwargs }
285
+
286
+ Gemba.user_config.states_dir = tmpdir
287
+ picker = Gemba::GamePickerFrame.new(app: app, rom_library: lib)
288
+ picker.show
289
+ app.show
290
+ app.update
291
+
292
+ override_tk_popup do
293
+ app.tcl_eval("event generate .game_picker.card0 <Button-3> -x 10 -y 10")
294
+ app.update
295
+ end
296
+
297
+ # Menu was built but not shown — invoke the Quick Load entry directly.
298
+ # index 1 = Quick Load
299
+ app.tcl_eval(".game_picker.card0.ctx invoke 1")
300
+ app.update
301
+
302
+ assert_equal rom_path, received[:path]
303
+ assert_equal slot, received[:slot]
304
+
305
+ picker.cleanup
306
+ app.command(:destroy, '.game_picker') rescue nil
307
+ end
308
+ end
309
+ end
310
+ 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 TestGamepadMap < Minitest::Test
@@ -7,9 +7,6 @@ require "tmpdir"
7
7
  class TestHeadlessPlayer < Minitest::Test
8
8
  TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
9
9
 
10
- def setup
11
- skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
12
- end
13
10
 
14
11
  # -- Lifecycle ---------------------------------------------------------------
15
12
 
@@ -202,4 +199,21 @@ class TestHeadlessPlayer < Minitest::Test
202
199
  assert_equal 0, player.rewind_count
203
200
  end
204
201
  end
202
+
203
+ # -- BIOS loading -----------------------------------------------------------
204
+
205
+ FAKE_BIOS = File.expand_path("fixtures/fake_bios.bin", __dir__)
206
+
207
+ def test_bios_not_loaded_by_default
208
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
209
+ refute player.core.bios_loaded?
210
+ end
211
+ end
212
+
213
+ def test_bios_loaded_when_path_given
214
+ skip "fake_bios.bin not present" unless File.exist?(FAKE_BIOS)
215
+ Gemba::HeadlessPlayer.open(TEST_ROM, bios_path: FAKE_BIOS) do |player|
216
+ assert player.core.bios_loaded?
217
+ end
218
+ end
205
219
  end