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
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,15 @@ 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,
52
61
  }.freeze
53
62
 
54
63
  # Settings that can be overridden per ROM. Maps config key → locale key.
@@ -64,6 +73,7 @@ module Gemba
64
73
  'turbo_speed' => 'settings.turbo_speed',
65
74
  'quick_save_slot' => 'settings.quick_save_slot',
66
75
  'save_state_backup' => 'settings.keep_backup',
76
+ 'ra_rich_presence' => 'settings.ra_rich_presence',
67
77
  }.freeze
68
78
 
69
79
  PER_GAME_KEYS = PER_GAME_SETTINGS.keys.to_set.freeze
@@ -123,9 +133,18 @@ module Gemba
123
133
  },
124
134
  }.freeze
125
135
 
126
- def initialize(path: nil)
136
+ def initialize(path: nil, subscribe: true)
127
137
  @path = path || self.class.default_path
128
138
  @data = load_file
139
+ subscribe_to_bus if subscribe
140
+ end
141
+
142
+ # Re-wire bus subscriptions onto the current Gemba.bus.
143
+ # Called by AppController after it creates a fresh EventBus, because the
144
+ # Config may have been instantiated earlier (e.g. by the CLI) and subscribed
145
+ # to whatever bus existed at that time (possibly nil).
146
+ def resubscribe
147
+ subscribe_to_bus
129
148
  end
130
149
 
131
150
  # @return [String] path to the config file
@@ -401,6 +420,82 @@ module Gemba
401
420
  global['pause_on_focus_loss'] = !!val
402
421
  end
403
422
 
423
+ # @return [String, nil] BIOS filename (relative to Config.bios_dir), or nil for HLE
424
+ def bios_path
425
+ global['bios_path']
426
+ end
427
+
428
+ def bios_path=(val)
429
+ global['bios_path'] = val.nil? ? nil : val.to_s
430
+ end
431
+
432
+ def skip_bios?
433
+ !!global['skip_bios']
434
+ end
435
+
436
+ def skip_bios=(val)
437
+ global['skip_bios'] = !!val
438
+ end
439
+
440
+ # @return [String] log level (debug, info, warn, error)
441
+ def log_level
442
+ global['log_level']
443
+ end
444
+
445
+ def log_level=(val)
446
+ global['log_level'] = val.to_s
447
+ end
448
+
449
+ # -- RetroAchievements ----------------------------------------------------
450
+
451
+ def ra_enabled?
452
+ global['ra_enabled']
453
+ end
454
+
455
+ def ra_enabled=(val)
456
+ global['ra_enabled'] = val ? true : false
457
+ end
458
+
459
+ def ra_username
460
+ global['ra_username'] || ''
461
+ end
462
+
463
+ def ra_username=(val)
464
+ global['ra_username'] = val.to_s
465
+ end
466
+
467
+ def ra_token
468
+ global['ra_token'] || ''
469
+ end
470
+
471
+ def ra_token=(val)
472
+ global['ra_token'] = val.to_s
473
+ end
474
+
475
+ def ra_hardcore?
476
+ global['ra_hardcore']
477
+ end
478
+
479
+ def ra_hardcore=(val)
480
+ global['ra_hardcore'] = val ? true : false
481
+ end
482
+
483
+ def ra_unofficial?
484
+ global['ra_unofficial']
485
+ end
486
+
487
+ def ra_unofficial=(val)
488
+ global['ra_unofficial'] = val ? true : false
489
+ end
490
+
491
+ def ra_rich_presence?
492
+ global['ra_rich_presence']
493
+ end
494
+
495
+ def ra_rich_presence=(val)
496
+ global['ra_rich_presence'] = val ? true : false
497
+ end
498
+
404
499
  # @return [String] directory for .grec recording files
405
500
  def recordings_dir
406
501
  global['recordings_dir'] || self.class.default_recordings_dir
@@ -568,8 +663,46 @@ module Gemba
568
663
  File.join(config_dir, 'recordings')
569
664
  end
570
665
 
666
+ # @return [String] default directory for session log files
667
+ def self.default_logs_dir
668
+ File.join(config_dir, 'logs')
669
+ end
670
+
671
+ # @return [String] default directory for cached box art images
672
+ def self.boxart_dir
673
+ File.join(config_dir, 'boxart')
674
+ end
675
+
676
+ # @return [String] directory for cached RA achievement lists (one JSON per rom_id)
677
+ def self.achievements_cache_dir
678
+ File.join(config_dir, 'achievements')
679
+ end
680
+
681
+ # @return [String] default directory for patched ROMs
682
+ def self.default_patches_dir
683
+ File.join(config_dir, 'patches')
684
+ end
685
+
686
+ # @return [String] directory for BIOS files
687
+ def self.bios_dir
688
+ File.join(config_dir, 'bios')
689
+ end
690
+
691
+ # @return [String] path to the per-ROM overrides JSON file
692
+ def self.rom_overrides_path
693
+ File.join(config_dir, 'rom_overrides.json')
694
+ end
695
+
571
696
  private
572
697
 
698
+ def subscribe_to_bus
699
+ Gemba.bus.on(:rom_loaded) do |rom_id:, path:, **|
700
+ activate_game(rom_id)
701
+ add_recent_rom(path)
702
+ save!
703
+ end
704
+ end
705
+
573
706
  def global
574
707
  @proxy || global_base
575
708
  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)"}