gemba 0.1.1 → 0.2.1

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 (289) 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 +190 -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 +511 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +28 -0
  19. data/lib/gemba/achievements/retro_achievements/unlock_retry_worker.rb +35 -0
  20. data/lib/gemba/achievements.rb +19 -0
  21. data/lib/gemba/achievements_window.rb +556 -0
  22. data/lib/gemba/app_controller.rb +1036 -0
  23. data/lib/gemba/bios.rb +54 -0
  24. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  25. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  26. data/lib/gemba/boxart_fetcher.rb +79 -0
  27. data/lib/gemba/bus_emitter.rb +13 -0
  28. data/lib/gemba/child_window.rb +24 -1
  29. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  30. data/lib/gemba/cli/commands/decode.rb +154 -0
  31. data/lib/gemba/cli/commands/patch.rb +78 -0
  32. data/lib/gemba/cli/commands/play.rb +78 -0
  33. data/lib/gemba/cli/commands/record.rb +114 -0
  34. data/lib/gemba/cli/commands/replay.rb +161 -0
  35. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  36. data/lib/gemba/cli/commands/version.rb +22 -0
  37. data/lib/gemba/cli.rb +52 -364
  38. data/lib/gemba/config.rb +154 -1
  39. data/lib/gemba/data/gb_games.json +1 -0
  40. data/lib/gemba/data/gb_md5.json +1 -0
  41. data/lib/gemba/data/gba_games.json +1 -0
  42. data/lib/gemba/data/gba_md5.json +1 -0
  43. data/lib/gemba/data/gbc_games.json +1 -0
  44. data/lib/gemba/data/gbc_md5.json +1 -0
  45. data/lib/gemba/emulator_frame.rb +1084 -0
  46. data/lib/gemba/event_bus.rb +48 -0
  47. data/lib/gemba/frame_stack.rb +70 -0
  48. data/lib/gemba/game_index.rb +84 -0
  49. data/lib/gemba/game_picker_frame.rb +309 -0
  50. data/lib/gemba/gamepad_map.rb +103 -0
  51. data/lib/gemba/headless.rb +6 -5
  52. data/lib/gemba/headless_player.rb +33 -3
  53. data/lib/gemba/help_window.rb +61 -0
  54. data/lib/gemba/hotkey_map.rb +3 -1
  55. data/lib/gemba/input_recorder.rb +107 -0
  56. data/lib/gemba/input_replayer.rb +119 -0
  57. data/lib/gemba/keyboard_map.rb +90 -0
  58. data/lib/gemba/list_picker_frame.rb +271 -0
  59. data/lib/gemba/locales/en.yml +109 -5
  60. data/lib/gemba/locales/ja.yml +109 -5
  61. data/lib/gemba/main_window.rb +56 -0
  62. data/lib/gemba/modal_stack.rb +81 -0
  63. data/lib/gemba/patcher_window.rb +223 -0
  64. data/lib/gemba/platform/gb.rb +21 -0
  65. data/lib/gemba/platform/gba.rb +21 -0
  66. data/lib/gemba/platform/gbc.rb +23 -0
  67. data/lib/gemba/platform.rb +20 -0
  68. data/lib/gemba/platform_open.rb +19 -0
  69. data/lib/gemba/recorder.rb +4 -3
  70. data/lib/gemba/replay_player.rb +691 -0
  71. data/lib/gemba/rom_info.rb +57 -0
  72. data/lib/gemba/rom_info_window.rb +16 -3
  73. data/lib/gemba/rom_library.rb +106 -0
  74. data/lib/gemba/rom_overrides.rb +47 -0
  75. data/lib/gemba/rom_patcher/bps.rb +161 -0
  76. data/lib/gemba/rom_patcher/ips.rb +101 -0
  77. data/lib/gemba/rom_patcher/ups.rb +119 -0
  78. data/lib/gemba/rom_patcher.rb +109 -0
  79. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  80. data/lib/gemba/runtime.rb +59 -26
  81. data/lib/gemba/save_state_manager.rb +4 -7
  82. data/lib/gemba/save_state_picker.rb +17 -4
  83. data/lib/gemba/session_logger.rb +64 -0
  84. data/lib/gemba/settings/audio_tab.rb +77 -0
  85. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  86. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  87. data/lib/gemba/settings/paths.rb +11 -0
  88. data/lib/gemba/settings/recording_tab.rb +83 -0
  89. data/lib/gemba/settings/save_states_tab.rb +91 -0
  90. data/lib/gemba/settings/system_tab.rb +377 -0
  91. data/lib/gemba/settings/video_tab.rb +318 -0
  92. data/lib/gemba/settings_window.rb +162 -1036
  93. data/lib/gemba/version.rb +1 -1
  94. data/lib/gemba/virtual_keyboard.rb +19 -0
  95. data/lib/gemba.rb +2 -12
  96. data/test/achievements_window/test_bulk_sync.rb +218 -0
  97. data/test/achievements_window/test_bus_events.rb +125 -0
  98. data/test/achievements_window/test_close_confirmation.rb +201 -0
  99. data/test/achievements_window/test_initial_state.rb +164 -0
  100. data/test/achievements_window/test_sorting.rb +227 -0
  101. data/test/achievements_window/test_tree_rendering.rb +133 -0
  102. data/test/fixtures/fake_bios.bin +0 -0
  103. data/test/fixtures/pong.gba +0 -0
  104. data/test/fixtures/test.gb +0 -0
  105. data/test/fixtures/test.gbc +0 -0
  106. data/test/fixtures/test_quicksave.ss +0 -0
  107. data/test/screenshots/no_focus.png +0 -0
  108. data/test/shared/teek_test_worker.rb +17 -1
  109. data/test/shared/tk_test_helper.rb +92 -4
  110. data/test/support/achievements_window_helpers.rb +18 -0
  111. data/test/support/fake_core.rb +25 -0
  112. data/test/support/fake_ra_runtime.rb +74 -0
  113. data/test/support/fake_requester.rb +78 -0
  114. data/test/support/player_helpers.rb +20 -5
  115. data/test/test_achievement.rb +32 -0
  116. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  117. data/test/test_bios.rb +123 -0
  118. data/test/test_boxart_fetcher.rb +150 -0
  119. data/test/test_cli.rb +17 -265
  120. data/test/test_cli_config.rb +64 -0
  121. data/test/test_cli_decode.rb +97 -0
  122. data/test/test_cli_patch.rb +58 -0
  123. data/test/test_cli_play.rb +213 -0
  124. data/test/test_cli_ra.rb +175 -0
  125. data/test/test_cli_record.rb +69 -0
  126. data/test/test_cli_replay.rb +72 -0
  127. data/test/test_cli_sync_requester.rb +152 -0
  128. data/test/test_cli_version.rb +27 -0
  129. data/test/test_config.rb +3 -3
  130. data/test/test_config_ra.rb +69 -0
  131. data/test/test_core.rb +62 -1
  132. data/test/test_credentials_presenter.rb +192 -0
  133. data/test/test_event_bus.rb +100 -0
  134. data/test/test_fake_backend_achievements.rb +130 -0
  135. data/test/test_fake_backend_auth.rb +68 -0
  136. data/test/test_game_index.rb +77 -0
  137. data/test/test_game_picker_frame.rb +310 -0
  138. data/test/test_gamepad_map.rb +1 -3
  139. data/test/test_headless_player.rb +17 -3
  140. data/test/test_help_window.rb +82 -0
  141. data/test/test_hotkey_map.rb +22 -1
  142. data/test/test_input_recorder.rb +179 -0
  143. data/test/test_input_replay_determinism.rb +113 -0
  144. data/test/test_input_replayer.rb +162 -0
  145. data/test/test_keyboard_map.rb +1 -3
  146. data/test/test_libretro_backend.rb +41 -0
  147. data/test/test_list_picker_frame.rb +391 -0
  148. data/test/test_locale.rb +1 -1
  149. data/test/test_logging.rb +123 -0
  150. data/test/test_null_backend.rb +42 -0
  151. data/test/test_offline_backend.rb +116 -0
  152. data/test/test_overlay_renderer.rb +1 -1
  153. data/test/test_platform.rb +149 -0
  154. data/test/test_ra_backend.rb +313 -0
  155. data/test/test_ra_backend_unlock_gate.rb +56 -0
  156. data/test/test_ra_backend_unlock_retry.rb +123 -0
  157. data/test/test_recorder.rb +0 -3
  158. data/test/test_replay_player.rb +316 -0
  159. data/test/test_rom_info.rb +149 -0
  160. data/test/test_rom_overrides.rb +86 -0
  161. data/test/test_rom_patcher.rb +383 -0
  162. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  163. data/test/test_save_state_manager.rb +2 -4
  164. data/test/test_settings_audio.rb +107 -0
  165. data/test/test_settings_hotkeys.rb +83 -66
  166. data/test/test_settings_recording.rb +49 -0
  167. data/test/test_settings_save_states.rb +97 -0
  168. data/test/test_settings_system.rb +133 -0
  169. data/test/test_settings_video.rb +450 -0
  170. data/test/test_settings_window.rb +76 -507
  171. data/test/test_tip_service.rb +6 -6
  172. data/test/test_toast_overlay.rb +1 -1
  173. data/test/test_virtual_events.rb +221 -0
  174. data/test/test_virtual_keyboard.rb +1 -1
  175. data/vendor/rcheevos/CHANGELOG.md +495 -0
  176. data/vendor/rcheevos/LICENSE +21 -0
  177. data/vendor/rcheevos/Package.swift +33 -0
  178. data/vendor/rcheevos/README.md +67 -0
  179. data/vendor/rcheevos/include/module.modulemap +70 -0
  180. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  181. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  182. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  183. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  184. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  185. data/vendor/rcheevos/include/rc_client.h +877 -0
  186. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  187. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  188. data/vendor/rcheevos/include/rc_error.h +59 -0
  189. data/vendor/rcheevos/include/rc_export.h +100 -0
  190. data/vendor/rcheevos/include/rc_hash.h +200 -0
  191. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  192. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  193. data/vendor/rcheevos/include/rc_util.h +51 -0
  194. data/vendor/rcheevos/include/rcheevos.h +8 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  197. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  198. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  199. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  200. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  201. data/vendor/rcheevos/src/rc_client.c +6941 -0
  202. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  203. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  204. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  205. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  206. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  207. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  208. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  209. data/vendor/rcheevos/src/rc_compat.c +251 -0
  210. data/vendor/rcheevos/src/rc_compat.h +121 -0
  211. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  212. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  213. data/vendor/rcheevos/src/rc_util.c +199 -0
  214. data/vendor/rcheevos/src/rc_version.c +11 -0
  215. data/vendor/rcheevos/src/rc_version.h +32 -0
  216. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  217. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  218. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  219. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  220. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  221. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  222. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  223. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  224. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  225. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  226. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  227. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  228. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  229. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  230. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  231. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  232. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  233. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  234. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  235. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  236. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  237. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  238. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  239. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  240. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  241. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  242. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  243. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  244. data/vendor/rcheevos/test/libretro.h +205 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  246. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  247. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  248. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  249. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  250. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  251. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  252. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  253. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  254. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  255. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  256. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  257. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  258. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  259. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  260. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  261. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  262. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  263. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  264. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  265. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  266. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  267. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  268. data/vendor/rcheevos/test/rhash/data.c +657 -0
  269. data/vendor/rcheevos/test/rhash/data.h +32 -0
  270. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  271. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  272. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  273. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  274. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  275. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  276. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  277. data/vendor/rcheevos/test/test.c +113 -0
  278. data/vendor/rcheevos/test/test_framework.h +205 -0
  279. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  280. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  281. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  282. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  283. data/vendor/rcheevos/test/test_types.natvis +9 -0
  284. data/vendor/rcheevos/validator/validator.c +658 -0
  285. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  286. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  287. metadata +277 -10
  288. data/lib/gemba/input_mappings.rb +0 -214
  289. data/lib/gemba/player.rb +0 -1525
data/lib/gemba/cli.rb CHANGED
@@ -1,384 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'optparse'
4
- require_relative 'version'
5
4
 
6
5
  module Gemba
7
6
  class CLI
8
- SUBCOMMANDS = %w[record decode info].freeze
7
+ SUBCOMMANDS = %w[play record decode replay config version patch ra].freeze
9
8
 
10
- # Entry point: dispatch to subcommand or player.
9
+ # Entry point: dispatch to subcommand or default to play.
11
10
  # @param argv [Array<String>]
12
- def self.run(argv = ARGV)
11
+ # @param dry_run [Boolean] parse and validate only, return execution plan
12
+ def self.run(argv = ARGV, dry_run: false)
13
13
  args = argv.dup
14
14
 
15
- case args.first
16
- when 'record'
17
- args.shift
18
- run_record(args)
19
- when 'decode'
20
- args.shift
21
- run_decode(args)
22
- when 'info'
23
- args.shift
24
- run_info(args)
25
- else
26
- run_player(args)
27
- end
28
- end
29
-
30
- # Parse player (default) command options.
31
- # @param argv [Array<String>]
32
- # @return [Hash]
33
- def self.parse(argv)
34
- options = {}
35
-
36
- parser = OptionParser.new do |o|
37
- o.banner = "Usage: gemba [options] [ROM_FILE]"
38
- o.separator ""
39
- o.separator "GBA emulator powered by teek + libmgba"
40
- o.separator ""
41
- o.separator "Commands:"
42
- o.separator " record Record video+audio to .grec (headless)"
43
- o.separator " decode Encode .grec to video via ffmpeg"
44
- o.separator " info Show .grec recording stats"
45
- o.separator ""
46
- o.separator "Player options:"
47
-
48
- o.on("-s", "--scale N", Integer, "Window scale (1-4)") do |v|
49
- options[:scale] = v.clamp(1, 4)
50
- end
51
-
52
- o.on("-v", "--volume N", Integer, "Volume (0-100)") do |v|
53
- options[:volume] = v.clamp(0, 100)
54
- end
55
-
56
- o.on("-m", "--mute", "Start muted") do
57
- options[:mute] = true
58
- end
59
-
60
- o.on("--no-sound", "Disable audio entirely") do
61
- options[:sound] = false
62
- end
63
-
64
- o.on("-f", "--fullscreen", "Start in fullscreen") do
65
- options[:fullscreen] = true
66
- end
67
-
68
- o.on("--show-fps", "Show FPS counter") do
69
- options[:show_fps] = true
70
- end
71
-
72
- o.on("--turbo-speed N", Integer, "Fast-forward speed (0=uncapped, 2-4)") do |v|
73
- options[:turbo_speed] = v.clamp(0, 4)
74
- end
75
-
76
- o.on("--locale LANG", "Language (en, ja, auto)") do |v|
77
- options[:locale] = v
78
- end
79
-
80
- o.on("--headless", "Run without GUI (requires --frames and ROM)") do
81
- options[:headless] = true
82
- end
83
-
84
- o.on("--frames N", Integer, "Run N frames then exit (requires ROM)") do |v|
85
- options[:frames] = v
86
- end
87
-
88
- o.on("--reset-config", "Delete settings file and exit (keeps saves)") do
89
- options[:reset_config] = true
90
- end
91
-
92
- o.on("-y", "--yes", "Skip confirmation prompts") do
93
- options[:yes] = true
94
- end
95
-
96
- o.on("--version", "Show version") do
97
- options[:version] = true
98
- end
99
-
100
- o.on("-h", "--help", "Show this help") do
101
- options[:help] = true
102
- end
103
- end
104
-
105
- parser.parse!(argv)
106
- options[:rom] = File.expand_path(argv.first) if argv.first
107
- options[:parser] = parser
108
- options
109
- end
110
-
111
- # Apply parsed CLI options to the user config (session-only overrides).
112
- # @param config [Gemba::Config]
113
- # @param options [Hash]
114
- def self.apply(config, options)
115
- config.scale = options[:scale] if options[:scale]
116
- config.volume = options[:volume] if options[:volume]
117
- config.muted = true if options[:mute]
118
- config.show_fps = true if options[:show_fps]
119
- config.turbo_speed = options[:turbo_speed] if options[:turbo_speed]
120
- config.locale = options[:locale] if options[:locale]
121
- end
122
-
123
- # --- Player (default command) ---
124
-
125
- def self.run_player(argv)
126
- options = parse(argv)
127
-
128
- if options[:help]
129
- puts options[:parser]
130
- return
131
- end
132
-
133
- if options[:version]
134
- puts "gemba #{Gemba::VERSION}"
135
- return
136
- end
137
-
138
- require "gemba"
139
-
140
- if options[:reset_config]
141
- path = Config.default_path
142
- unless File.exist?(path)
143
- puts "No config file found at #{path}"
144
- return
145
- end
146
- unless options[:yes]
147
- print "Delete #{path}? [y/N] "
148
- return unless $stdin.gets&.strip&.downcase == 'y'
149
- end
150
- Config.reset!(path: path)
151
- puts "Deleted #{path}"
152
- return
153
- end
154
-
155
- if options[:headless]
156
- unless options[:frames] && options[:rom]
157
- $stderr.puts "Error: --headless requires --frames N and a ROM file"
158
- exit 1
159
- end
160
- require "gemba/headless"
161
- HeadlessPlayer.open(options[:rom]) { |p| p.step(options[:frames]) }
162
- return
163
- end
164
-
165
- if options[:frames] && !options[:rom]
166
- $stderr.puts "Error: --frames requires a ROM file"
167
- exit 1
168
- end
169
-
170
- apply(Gemba.user_config, options)
171
- Gemba.load_locale if options[:locale]
172
-
173
- sound = options.fetch(:sound, true)
174
- Player.new(options[:rom], sound: sound, fullscreen: options[:fullscreen],
175
- frames: options[:frames]).run
176
- end
177
- private_class_method :run_player
178
-
179
- # --- record subcommand ---
180
-
181
- def self.parse_record(argv)
182
- options = {}
183
-
184
- parser = OptionParser.new do |o|
185
- o.banner = "Usage: gemba record [options] ROM_FILE"
186
- o.separator ""
187
- o.separator "Record video+audio to a .grec file (headless, no GUI)"
188
- o.separator ""
189
-
190
- o.on("--frames N", Integer, "Number of frames to record (required)") do |v|
191
- options[:frames] = v
192
- end
193
-
194
- o.on("-o", "--output PATH", "Output .grec path (default: ROM_ID.grec)") do |v|
195
- options[:output] = v
196
- end
197
-
198
- o.on("-c", "--compression N", Integer, "Zlib level 1-9 (default: 1, 6+ has diminishing returns)") do |v|
199
- options[:compression] = v.clamp(1, 9)
200
- end
201
-
202
- o.on("--progress", "Show recording progress") do
203
- options[:progress] = true
204
- end
205
-
206
- o.on("-h", "--help", "Show this help") do
207
- options[:help] = true
208
- end
209
- end
210
-
211
- parser.parse!(argv)
212
- options[:rom] = File.expand_path(argv.first) if argv.first
213
- options[:parser] = parser
214
- options
215
- end
216
-
217
- def self.run_record(argv)
218
- options = parse_record(argv)
219
-
220
- if options[:help]
221
- puts options[:parser]
222
- return
223
- end
224
-
225
- unless options[:frames] && options[:rom]
226
- $stderr.puts "Error: record requires --frames N and a ROM file"
227
- $stderr.puts "Run 'gemba record --help' for usage"
228
- exit 1
229
- end
230
-
231
- require "gemba/headless"
232
-
233
- total = options[:frames]
234
-
235
- HeadlessPlayer.open(options[:rom]) do |player|
236
- rec_path = options[:output] ||
237
- "#{Config.rom_id(player.game_code, player.checksum)}.grec"
238
-
239
- rec_opts = {}
240
- rec_opts[:compression] = options[:compression] if options[:compression]
241
- player.start_recording(rec_path, **rec_opts)
242
-
243
- if options[:progress]
244
- last_print = Process.clock_gettime(Process::CLOCK_MONOTONIC)
245
- player.step(total) do |frame|
246
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
247
- if frame == total || now - last_print >= 0.5
248
- pct = frame * 100.0 / total
249
- $stderr.print "\rRecording: #{frame}/#{total} (#{'%.1f' % pct}%)\e[K"
250
- last_print = now
251
- end
252
- end
253
- $stderr.print "\r\e[K"
254
- else
255
- player.step(total)
256
- end
257
-
258
- player.stop_recording
259
-
260
- info = RecorderDecoder.stats(rec_path)
261
- puts "Recorded #{info[:frame_count]} frames to #{rec_path}"
262
- puts " Duration: #{'%.1f' % info[:duration]}s"
263
- puts " Avg change: #{'%.1f' % info[:avg_change_pct]}%/frame"
264
- puts " Uncompressed: #{format_size(info[:raw_video_size])} (encode input)"
265
- puts " .grec size: #{format_size(File.size(rec_path))}"
266
- end
267
- end
268
- private_class_method :run_record
269
-
270
- # --- decode subcommand ---
271
-
272
- def self.parse_decode(argv)
273
- options = {}
274
-
275
- parser = OptionParser.new do |o|
276
- o.banner = "Usage: gemba decode [options] TREC_FILE [-- FFMPEG_ARGS...]"
277
- o.separator ""
278
- o.separator "Encode a .grec recording to a playable video via ffmpeg."
279
- o.separator "Args after -- replace the default codec flags (-c:v, -c:a, etc)."
280
- o.separator ""
281
-
282
- o.on("-o", "--output PATH", "Output path (default: INPUT.mp4)") do |v|
283
- options[:output] = v
284
- end
285
-
286
- o.on("--video-codec CODEC", "Video codec (default: libx264)") do |v|
287
- options[:video_codec] = v
288
- end
289
-
290
- o.on("--audio-codec CODEC", "Audio codec (default: aac)") do |v|
291
- options[:audio_codec] = v
292
- end
293
-
294
- o.on("-s", "--scale N", Integer, "Scale factor (default: native)") do |v|
295
- options[:scale] = v.clamp(1, 10)
296
- end
297
-
298
- o.on("--no-progress", "Disable progress indicator") do
299
- options[:progress] = false
300
- end
301
-
302
- o.on("-h", "--help", "Show this help") do
303
- options[:help] = true
304
- end
15
+ if args.first == '--help' || args.first == '-h'
16
+ puts main_help unless dry_run
17
+ return { command: :help }
305
18
  end
306
19
 
307
- parser.parse!(argv)
308
- options[:trec] = argv.shift
309
- options[:ffmpeg_args] = argv unless argv.empty?
310
- options[:parser] = parser
311
- options
312
- end
313
-
314
- def self.run_decode(argv)
315
- options = parse_decode(argv)
20
+ cmd = SUBCOMMANDS.include?(args.first) ? args.shift : 'play'
316
21
 
317
- if options[:help]
318
- puts options[:parser]
319
- return
320
- end
321
-
322
- unless options[:trec]
323
- $stderr.puts "Error: decode requires a .grec file"
324
- $stderr.puts "Run 'gemba decode --help' for usage"
325
- exit 1
22
+ case cmd
23
+ when 'play'
24
+ require 'gemba/cli/commands/play'
25
+ Commands::Play.new(args, dry_run: dry_run).call
26
+ when 'record'
27
+ require 'gemba/cli/commands/record'
28
+ Commands::Record.new(args, dry_run: dry_run).call
29
+ when 'decode'
30
+ require 'gemba/cli/commands/decode'
31
+ Commands::Decode.new(args, dry_run: dry_run).call
32
+ when 'replay'
33
+ require 'gemba/cli/commands/replay'
34
+ Commands::Replay.new(args, dry_run: dry_run).call
35
+ when 'config'
36
+ require 'gemba/cli/commands/config_cmd'
37
+ Commands::ConfigCmd.new(args, dry_run: dry_run).call
38
+ when 'version'
39
+ require 'gemba/cli/commands/version'
40
+ Commands::Version.new(args, dry_run: dry_run).call
41
+ when 'patch'
42
+ require 'gemba/cli/commands/patch'
43
+ Commands::Patch.new(args, dry_run: dry_run).call
44
+ when 'ra'
45
+ require 'gemba/cli/commands/retro_achievements'
46
+ Commands::RetroAchievements.new(args, dry_run: dry_run).call
326
47
  end
327
-
328
- require "gemba/headless"
329
-
330
- trec_path = options[:trec]
331
- output_path = options[:output] || trec_path.sub(/\.grec\z/, '') + '.mp4'
332
- codec_opts = {}
333
- codec_opts[:video_codec] = options[:video_codec] if options[:video_codec]
334
- codec_opts[:audio_codec] = options[:audio_codec] if options[:audio_codec]
335
- codec_opts[:scale] = options[:scale] if options[:scale]
336
- codec_opts[:ffmpeg_args] = options[:ffmpeg_args] if options[:ffmpeg_args]
337
- codec_opts[:progress] = options.fetch(:progress, true)
338
-
339
- info = RecorderDecoder.decode(trec_path, output_path, **codec_opts)
340
- puts "Encoded #{info[:frame_count]} frames " \
341
- "(#{info[:width]}x#{info[:height]} @ #{'%.2f' % info[:fps]} fps, " \
342
- "avg #{'%.1f' % info[:avg_change_pct]}% change/frame)"
343
- puts "Output: #{info[:output_path]}"
344
48
  end
345
- private_class_method :run_decode
346
-
347
- # --- info subcommand ---
348
49
 
349
- def self.run_info(argv)
350
- if argv.include?('--help') || argv.include?('-h') || argv.empty?
351
- puts "Usage: gemba info TREC_FILE"
352
- puts ""
353
- puts "Show recording stats (no ffmpeg needed)"
354
- return
355
- end
356
-
357
- require "gemba/headless"
358
-
359
- trec_path = argv.first
360
- info = RecorderDecoder.stats(trec_path)
361
-
362
- puts "Recording: #{trec_path}"
363
- puts " Frames: #{info[:frame_count]}"
364
- puts " Resolution: #{info[:width]}x#{info[:height]}"
365
- puts " FPS: #{'%.2f' % info[:fps]}"
366
- puts " Duration: #{'%.1f' % info[:duration]}s"
367
- puts " Avg change: #{'%.1f' % info[:avg_change_pct]}%/frame"
368
- puts " Uncompressed: #{format_size(info[:raw_video_size])} (encode input)"
369
- puts " Audio: #{info[:audio_rate]} Hz, #{info[:audio_channels]}ch"
50
+ # Main help text listing all subcommands.
51
+ def self.main_help
52
+ <<~HELP
53
+ Usage: gemba [command] [options]
54
+
55
+ GBA emulator powered by teek + libmgba
56
+
57
+ Commands:
58
+ play Play a ROM (default)
59
+ record Record video+audio to .grec (headless)
60
+ decode Encode .grec to video via ffmpeg (--stats for info)
61
+ replay Replay a .gir input recording
62
+ patch Apply an IPS/BPS/UPS patch to a ROM
63
+ config Show or reset configuration
64
+ version Show version
65
+ ra RetroAchievements login, verify, achievements
66
+
67
+ Run 'gemba <command> --help' for command-specific options.
68
+ HELP
370
69
  end
371
- private_class_method :run_info
372
70
 
373
- def self.format_size(bytes)
374
- if bytes >= 1_073_741_824
375
- "#{'%.1f' % (bytes / 1_073_741_824.0)} GB"
376
- elsif bytes >= 1_048_576
377
- "#{'%.1f' % (bytes / 1_048_576.0)} MB"
378
- else
379
- "#{'%.1f' % (bytes / 1024.0)} KB"
380
- end
381
- end
382
- private_class_method :format_size
383
71
  end
384
72
  end
data/lib/gemba/config.rb CHANGED
@@ -49,6 +49,17 @@ module Gemba
49
49
  'tip_dismiss_ms' => 4000,
50
50
  'recording_compression' => 1,
51
51
  'pause_on_focus_loss' => true,
52
+ 'log_level' => 'info',
53
+ 'bios_path' => nil,
54
+ 'skip_bios' => false,
55
+ 'ra_enabled' => false,
56
+ 'ra_username' => '',
57
+ 'ra_token' => '',
58
+ 'ra_hardcore' => false,
59
+ 'ra_unofficial' => false,
60
+ 'ra_rich_presence' => false,
61
+ 'ra_screenshot_on_unlock' => true,
62
+ 'picker_view' => 'grid',
52
63
  }.freeze
53
64
 
54
65
  # Settings that can be overridden per ROM. Maps config key → locale key.
@@ -64,6 +75,8 @@ module Gemba
64
75
  'turbo_speed' => 'settings.turbo_speed',
65
76
  'quick_save_slot' => 'settings.quick_save_slot',
66
77
  'save_state_backup' => 'settings.keep_backup',
78
+ 'ra_rich_presence' => 'settings.ra_rich_presence',
79
+ 'ra_screenshot_on_unlock' => 'settings.ra_screenshot_on_unlock',
67
80
  }.freeze
68
81
 
69
82
  PER_GAME_KEYS = PER_GAME_SETTINGS.keys.to_set.freeze
@@ -123,9 +136,18 @@ module Gemba
123
136
  },
124
137
  }.freeze
125
138
 
126
- def initialize(path: nil)
139
+ def initialize(path: nil, subscribe: true)
127
140
  @path = path || self.class.default_path
128
141
  @data = load_file
142
+ subscribe_to_bus if subscribe
143
+ end
144
+
145
+ # Re-wire bus subscriptions onto the current Gemba.bus.
146
+ # Called by AppController after it creates a fresh EventBus, because the
147
+ # Config may have been instantiated earlier (e.g. by the CLI) and subscribed
148
+ # to whatever bus existed at that time (possibly nil).
149
+ def resubscribe
150
+ subscribe_to_bus
129
151
  end
130
152
 
131
153
  # @return [String] path to the config file
@@ -401,6 +423,99 @@ module Gemba
401
423
  global['pause_on_focus_loss'] = !!val
402
424
  end
403
425
 
426
+ # @return [String, nil] BIOS filename (relative to Config.bios_dir), or nil for HLE
427
+ def bios_path
428
+ global['bios_path']
429
+ end
430
+
431
+ def bios_path=(val)
432
+ global['bios_path'] = val.nil? ? nil : val.to_s
433
+ end
434
+
435
+ def skip_bios?
436
+ !!global['skip_bios']
437
+ end
438
+
439
+ def skip_bios=(val)
440
+ global['skip_bios'] = !!val
441
+ end
442
+
443
+ # @return [String] log level (debug, info, warn, error)
444
+ def log_level
445
+ global['log_level']
446
+ end
447
+
448
+ def log_level=(val)
449
+ global['log_level'] = val.to_s
450
+ end
451
+
452
+ # -- RetroAchievements ----------------------------------------------------
453
+
454
+ def ra_enabled?
455
+ global['ra_enabled']
456
+ end
457
+
458
+ def ra_enabled=(val)
459
+ global['ra_enabled'] = val ? true : false
460
+ end
461
+
462
+ def ra_username
463
+ global['ra_username'] || ''
464
+ end
465
+
466
+ def ra_username=(val)
467
+ global['ra_username'] = val.to_s
468
+ end
469
+
470
+ def ra_token
471
+ global['ra_token'] || ''
472
+ end
473
+
474
+ def ra_token=(val)
475
+ global['ra_token'] = val.to_s
476
+ end
477
+
478
+ def ra_hardcore?
479
+ global['ra_hardcore']
480
+ end
481
+
482
+ def ra_hardcore=(val)
483
+ global['ra_hardcore'] = val ? true : false
484
+ end
485
+
486
+ def ra_unofficial?
487
+ global['ra_unofficial']
488
+ end
489
+
490
+ def ra_unofficial=(val)
491
+ global['ra_unofficial'] = val ? true : false
492
+ end
493
+
494
+ def ra_rich_presence?
495
+ global['ra_rich_presence']
496
+ end
497
+
498
+ def ra_rich_presence=(val)
499
+ global['ra_rich_presence'] = val ? true : false
500
+ end
501
+
502
+ def ra_screenshot_on_unlock?
503
+ global['ra_screenshot_on_unlock']
504
+ end
505
+
506
+ def ra_screenshot_on_unlock=(val)
507
+ global['ra_screenshot_on_unlock'] = val ? true : false
508
+ end
509
+
510
+ # @return [String] preferred picker view ('grid' or 'list')
511
+ def picker_view
512
+ global['picker_view'] || 'grid'
513
+ end
514
+
515
+ def picker_view=(val)
516
+ global['picker_view'] = val.to_s
517
+ end
518
+
404
519
  # @return [String] directory for .grec recording files
405
520
  def recordings_dir
406
521
  global['recordings_dir'] || self.class.default_recordings_dir
@@ -568,8 +683,46 @@ module Gemba
568
683
  File.join(config_dir, 'recordings')
569
684
  end
570
685
 
686
+ # @return [String] default directory for session log files
687
+ def self.default_logs_dir
688
+ File.join(config_dir, 'logs')
689
+ end
690
+
691
+ # @return [String] default directory for cached box art images
692
+ def self.boxart_dir
693
+ File.join(config_dir, 'boxart')
694
+ end
695
+
696
+ # @return [String] directory for cached RA achievement lists (one JSON per rom_id)
697
+ def self.achievements_cache_dir
698
+ File.join(config_dir, 'achievements')
699
+ end
700
+
701
+ # @return [String] default directory for patched ROMs
702
+ def self.default_patches_dir
703
+ File.join(config_dir, 'patches')
704
+ end
705
+
706
+ # @return [String] directory for BIOS files
707
+ def self.bios_dir
708
+ File.join(config_dir, 'bios')
709
+ end
710
+
711
+ # @return [String] path to the per-ROM overrides JSON file
712
+ def self.rom_overrides_path
713
+ File.join(config_dir, 'rom_overrides.json')
714
+ end
715
+
571
716
  private
572
717
 
718
+ def subscribe_to_bus
719
+ Gemba.bus.on(:rom_loaded) do |rom_id:, path:, **|
720
+ activate_game(rom_id)
721
+ add_recent_rom(path)
722
+ save!
723
+ end
724
+ end
725
+
573
726
  def global
574
727
  @proxy || global_base
575
728
  end
@@ -0,0 +1 @@
1
+ {"DMG-APAU":"Pokemon - Red Version (USA, Europe) (SGB Enhanced)","DMG-APEE":"Pokemon - Blue Version (USA, Europe) (SGB Enhanced)","DMG-APSU":"Pokemon - Yellow Version - Special Pikachu Edition (USA, Europe) (CGB+SGB Enhanced)"}