gemba 0.1.0 → 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 +24 -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 +135 -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 -1515
@@ -5,8 +5,12 @@ menu:
5
5
  quit: "終了"
6
6
  settings: "設定"
7
7
  view: "表示"
8
+ game_library: "ゲームライブラリ"
8
9
  fullscreen: "フルスクリーン"
9
10
  rom_info: "ROM情報…"
11
+ achievements: "アチーブメント…"
12
+ open_logs_dir: "ログフォルダを開く"
13
+ patch_rom: "ROMをパッチ…"
10
14
  emulation: "エミュレーション"
11
15
  pause: "一時停止"
12
16
  resume: "再開"
@@ -14,8 +18,10 @@ menu:
14
18
  quick_save: "クイックセーブ"
15
19
  quick_load: "クイックロード"
16
20
  save_states: "ステートセーブ…"
17
- start_recording: "録画開始"
18
- stop_recording: "録画停止"
21
+ start_recording: "キャプチャ開始"
22
+ stop_recording: "キャプチャ停止"
23
+ start_input_recording: "入力記録開始"
24
+ stop_input_recording: "入力記録停止"
19
25
 
20
26
  toast:
21
27
  save_blocked: "セーブ制限中(間隔が短すぎます)"
@@ -32,12 +38,18 @@ toast:
32
38
  no_rewind: "巻き戻しデータがありません"
33
39
  paused: "一時停止"
34
40
  waiting_for: "{label}を待機中…"
35
- recording_started: "録画を開始しました"
36
- recording_stopped: "{frames}フレームを録画しました"
41
+ recording_started: "キャプチャを開始しました"
42
+ recording_stopped: "{frames}フレームをキャプチャしました"
43
+ input_recording_started: "入力記録を開始しました"
44
+ input_recording_stopped: "入力を記録しました({frames}フレーム)"
37
45
 
38
46
  dialog:
39
47
  game_running_title: "ゲーム実行中"
40
48
  game_running_msg: "別のゲームが実行中です。{name}に切り替えますか?"
49
+ return_to_library_title: "ゲームライブラリに戻る"
50
+ return_to_library_msg: "ゲームライブラリに戻りますか?保存されていない進行状況は失われます。"
51
+ quit_title: "Gembaを終了"
52
+ quit_msg: "ゲームが実行中です。終了しますか?保存されていない進行状況は失われます。"
41
53
  drop_error_title: "ドロップエラー"
42
54
  drop_single_file_only: "ROMファイルを1つだけドロップしてください。"
43
55
  drop_unsupported_type: "対応していないファイル形式: {ext}"
@@ -51,6 +63,8 @@ dialog:
51
63
  reset_hotkeys_title: "ホットキーをリセット"
52
64
  reset_hotkeys_msg: "すべてのホットキー設定をデフォルトに戻しますか?"
53
65
  key_conflict_title: "キーの競合"
66
+ cancel_bulk_sync_title: "同期をキャンセル?"
67
+ cancel_bulk_sync_msg: "一括同期が実行中です。閉じるとキャンセルされます — 手動で再同期が必要になります。"
54
68
 
55
69
  settings:
56
70
  video: "映像"
@@ -91,7 +105,9 @@ settings:
91
105
  hk_save_states: "ステートセーブ"
92
106
  hk_screenshot: "スクリーンショット"
93
107
  hk_rewind: "巻き戻し"
94
- hk_record: "録画"
108
+ hk_record: "キャプチャ"
109
+ hk_input_record: "入力記録"
110
+ hk_open_rom: "ROM を開く"
95
111
  hk_reset_defaults: "デフォルトに戻す"
96
112
  pixel_filter: "ピクセルフィルタ:"
97
113
  filter_nearest: "ニアレストネイバー"
@@ -111,11 +127,39 @@ settings:
111
127
  tip_per_game: "ROM毎に映像・音声・ステートセーブの設定を個別に保存します。"
112
128
  tip_turbo_speed: "早送りホットキー使用時の速度。"
113
129
  tip_toast_duration: "画面上の通知の表示時間。"
130
+ system: "システム"
131
+ bios_header: "GBA BIOS"
132
+ bios_path: "BIOSファイル:"
133
+ bios_browse: "参照…"
134
+ bios_clear: "クリア"
135
+ bios_not_set: "未設定 — 内蔵HLEを使用(ほとんどのゲームに推奨)"
136
+ bios_not_found: "ファイルが見つかりません"
137
+ skip_bios: "起動アニメーションをスキップ"
138
+ tip_skip_bios: "ゲームボーイアドバンスのロゴ画面をスキップして\n直接ゲームを開始します。実BIOSが必要です。"
139
+ retroachievements: "実績"
140
+ ra_enabled: "RetroAchievementsを有効にする"
141
+ ra_credentials: "アカウント"
142
+ ra_username_placeholder: "ユーザー名:"
143
+ ra_token_placeholder: "パスワード:"
144
+ ra_rich_presence: "リッチプレゼンス(ゲームごと)"
145
+ ra_hardcore: "ハードコアモード(セーブステートと巻き戻し無効)"
146
+ tip_ra_password: "パスワードはAPIトークン取得のみに使用され、保存されません。"
147
+ ra_login: "ログイン"
148
+ ra_verify: "トークン確認"
149
+ ra_logout: "ログアウト"
150
+ ra_reset: "リセット"
151
+ ra_reset_title: "RetroAchievementsをリセット"
152
+ ra_reset_confirm: "保存済みの認証情報を削除してログアウトしますか?"
153
+ ra_test_ok: "接続OK ✓"
154
+ ra_disabled: "RetroAchievementsは無効です"
155
+ ra_not_logged_in: "未ログイン"
156
+ ra_logged_in_as: "{username}でログイン中"
114
157
  recording: "録画"
115
158
  recording_compression: "圧縮レベル:"
116
159
  tip_recording_compression: ".grecファイルのzlib圧縮レベル。\n1 = 最速(デフォルト)、6以上は効果が小さくなります。"
117
160
  recordings_folder: "録画フォルダ:"
118
161
  open_recordings_folder: "録画フォルダを開く…"
162
+ open_replay_player: "入力リプレイを開く…"
119
163
  gp_a: "A"
120
164
  gp_b: "B"
121
165
  gp_l: "L"
@@ -134,6 +178,13 @@ picker:
134
178
  slot: "スロット{n}"
135
179
  close: "閉じる"
136
180
 
181
+ game_picker:
182
+ menu:
183
+ play: "プレイ"
184
+ quick_load: "クイックロード"
185
+ set_boxart: "カスタム画像を設定"
186
+ remove: "ライブラリから削除"
187
+
137
188
  rom_info:
138
189
  title: "ROM情報"
139
190
  field_title: "タイトル:"
@@ -148,6 +199,29 @@ rom_info:
148
199
  close: "閉じる"
149
200
  na: "N/A"
150
201
 
202
+ replay:
203
+ open_recording: "入力記録を開く…"
204
+ replay_player: "入力リプレイ"
205
+ ended: "リプレイ完了({frames}フレーム)"
206
+ empty_hint: "入力記録を開く (Cmd+O)"
207
+
208
+ patcher:
209
+ title: "ROMをパッチ"
210
+ rom_label: "ROMファイル:"
211
+ patch_label: "パッチファイル:"
212
+ outdir_label: "出力フォルダ:"
213
+ browse: "参照…"
214
+ apply: "パッチを適用"
215
+ working: "パッチ適用中…"
216
+ done: "完了 →"
217
+ err_missing_fields: "すべての項目を入力してください。"
218
+ err_rom_not_found: "ROMファイルが見つかりません。"
219
+ err_patch_not_found: "パッチファイルが見つかりません。"
220
+ err_failed: "パッチ失敗:"
221
+ overwrite_title: "ファイルが存在します"
222
+ overwrite_msg: "{path} は既に存在します。上書きしますか?"
223
+ thread_mode_warn: "注意: Ruby < 4 ではプログレスが止まって見える場合があります"
224
+
151
225
  player:
152
226
  open_rom_hint: "ファイル > ROMを開く…"
153
227
  fps: "{fps} fps"
@@ -155,3 +229,21 @@ player:
155
229
  ff_max: ">> 最大"
156
230
  none: "(なし)"
157
231
  clear: "クリア"
232
+
233
+ achievements:
234
+ title: "アチーブメント"
235
+ game_label: "ゲーム:"
236
+ sync: "同期"
237
+ name_col: "アチーブメント"
238
+ points_col: "ポイント"
239
+ earned_col: "取得日時"
240
+ none: "アチーブメントなし"
241
+ earned_label: "{earned} / {total} 取得"
242
+ sync_pending: "同期中…"
243
+ sync_failed: "同期に失敗しました"
244
+ sync_no_game: "ゲームが読み込まれていません"
245
+ sync_timeout: "同期がタイムアウトしました"
246
+ not_logged_in: "ログインしていません"
247
+ include_unofficial: "非公式を含む"
248
+ bulk_syncing: "{title} を同期中 ({n}/{total})…"
249
+ bulk_sync_done: "{count} ゲームを同期しました"
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Gemba
5
+ # Pure Tk shell — creates the app window and hosts a FrameStack.
6
+ #
7
+ # MainWindow knows nothing about ROMs, emulation, menus, or config.
8
+ # It provides geometry/title/fullscreen primitives that the AppController
9
+ # drives. Its only structural contribution is the FrameStack, which
10
+ # manages show/hide transitions to prevent visual flash (FOUC).
11
+ class MainWindow
12
+ attr_reader :app, :frame_stack
13
+
14
+ def initialize
15
+ @app = Teek::App.new
16
+ @app.show
17
+ @frame_stack = FrameStack.new
18
+ end
19
+
20
+ def set_title(title)
21
+ @app.set_window_title(title)
22
+ end
23
+
24
+ def set_geometry(w, h)
25
+ @app.set_window_geometry("#{w}x#{h}")
26
+ end
27
+
28
+ def set_aspect(numer, denom)
29
+ @app.command(:wm, 'aspect', '.', numer, denom, numer, denom)
30
+ end
31
+
32
+ def set_minsize(w, h)
33
+ @app.command(:wm, 'minsize', '.', w, h)
34
+ end
35
+
36
+ def reset_minsize
37
+ @app.command(:wm, 'minsize', '.', 0, 0)
38
+ end
39
+
40
+ def reset_aspect_ratio
41
+ @app.command(:wm, 'aspect', '.', '', '', '', '')
42
+ end
43
+
44
+ def set_timer_speed(ms)
45
+ @app.interp.thread_timer_ms = ms
46
+ end
47
+
48
+ def fullscreen=(val)
49
+ @app.command(:wm, 'attributes', '.', '-fullscreen', val ? 1 : 0)
50
+ end
51
+
52
+ def mainloop
53
+ @app.mainloop
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemba
4
+ # Push/pop stack for modal child windows.
5
+ #
6
+ # One modal can push another (e.g. Settings → Replay Player) and the
7
+ # previous modal is automatically re-shown on pop.
8
+ #
9
+ # Windows must implement the ModalWindow protocol:
10
+ # show_modal(**args) — reveal the window (deiconify, grab, position)
11
+ # withdraw — hide the window (release grab, withdraw — NO callback)
12
+ #
13
+ # @example
14
+ # stack = ModalStack.new(
15
+ # on_enter: ->(name) { pause_emulation },
16
+ # on_exit: -> { unpause_emulation },
17
+ # on_focus_change: ->(name) { update_toast(name) },
18
+ # )
19
+ # stack.push(:settings, @settings_window, show_args: { tab: :video })
20
+ # stack.push(:replay, @replay_player) # settings auto-withdrawn
21
+ # stack.pop # replay closed, settings re-shown
22
+ # stack.pop # settings closed, on_exit fired
23
+ class ModalStack
24
+ Entry = Data.define(:name, :window, :show_args)
25
+
26
+ # @param on_enter [Proc] called with (name) when stack goes empty → non-empty
27
+ # @param on_exit [Proc] called when stack goes non-empty → empty
28
+ # @param on_focus_change [Proc, nil] called with (name) whenever the top modal changes
29
+ def initialize(on_enter:, on_exit:, on_focus_change: nil)
30
+ @stack = []
31
+ @on_enter = on_enter
32
+ @on_exit = on_exit
33
+ @on_focus_change = on_focus_change
34
+ end
35
+
36
+ # @return [Boolean] true if any modal is open
37
+ def active? = !@stack.empty?
38
+
39
+ # @return [Symbol, nil] name of the topmost modal, or nil
40
+ def current = @stack.last&.name
41
+
42
+ # @return [Integer] number of modals on the stack
43
+ def size = @stack.length
44
+
45
+ # Push a modal onto the stack.
46
+ #
47
+ # If another modal is on top, it is withdrawn (without callback).
48
+ # If the stack was empty, on_enter is fired (pause emulation, etc.).
49
+ #
50
+ # @param name [Symbol] identifier for the modal (e.g. :settings, :picker)
51
+ # @param window [#show_modal, #withdraw] the modal window object
52
+ # @param show_args [Hash] keyword arguments forwarded to window.show_modal
53
+ def push(name, window, show_args: {})
54
+ was_empty = @stack.empty?
55
+
56
+ # Withdraw current top without firing its on_dismiss
57
+ @stack.last&.window&.withdraw
58
+
59
+ @stack.push(Entry.new(name: name, window: window, show_args: show_args))
60
+ @on_enter.call(name) if was_empty
61
+ @on_focus_change&.call(name)
62
+ window.show_modal(**show_args)
63
+ end
64
+
65
+ # Pop the current modal off the stack.
66
+ #
67
+ # If the stack still has entries, the previous modal is re-shown.
68
+ # If the stack is now empty, on_exit is fired (unpause emulation, etc.).
69
+ def pop
70
+ return unless (entry = @stack.pop)
71
+ entry.window.withdraw
72
+
73
+ if (prev = @stack.last)
74
+ @on_focus_change&.call(prev.name)
75
+ prev.window.show_modal(**prev.show_args)
76
+ else
77
+ @on_exit.call
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemba
4
+ # Floating window for applying IPS/BPS/UPS patches to ROM files.
5
+ #
6
+ # Non-modal — no grab. Shows three file pickers (ROM, patch, output dir)
7
+ # and runs the patch in a background thread so the UI stays responsive.
8
+ class PatcherWindow
9
+ include ChildWindow
10
+ include Locale::Translatable
11
+
12
+ # Worker class for Ractor-based patching.
13
+ # Defined as a class so Ractor.shareable_proc never sees nested closures —
14
+ # the lambda and rescue are created at runtime inside the Ractor.
15
+ class PatchWorker
16
+ def call(t, d)
17
+ RomPatcher.patch(
18
+ rom_path: d[:rom],
19
+ patch_path: d[:patch],
20
+ out_path: d[:out],
21
+ on_progress: ->(pct) { t.yield(pct) }
22
+ )
23
+ t.yield({ ok: true, path: d[:out] })
24
+ rescue => e
25
+ t.yield({ ok: false, error: e.message })
26
+ end
27
+ end
28
+
29
+ TOP = '.patcher_window'
30
+ BG_MODE = (RUBY_VERSION >= '4.0' ? :ractor : :thread).freeze
31
+
32
+ VAR_ROM = '::gemba_patcher_rom'
33
+ VAR_PATCH = '::gemba_patcher_patch'
34
+ VAR_OUTDIR = '::gemba_patcher_outdir'
35
+ VAR_STATUS = '::gemba_patcher_status'
36
+
37
+ def initialize(app:)
38
+ @app = app
39
+ @callbacks = {}
40
+ build_toplevel(translate('patcher.title'), geometry: '540x220') { build_ui }
41
+ end
42
+
43
+ def show = show_window(modal: false)
44
+ def hide = hide_window(modal: false)
45
+ def visible? = @app.tcl_eval("wm state #{TOP}") == 'normal'
46
+
47
+ private
48
+
49
+ def build_ui
50
+ f = "#{TOP}.f"
51
+ @app.command('ttk::frame', f, padding: 12)
52
+ @app.command(:pack, f, fill: :both, expand: 1)
53
+
54
+ @app.set_variable(VAR_ROM, '')
55
+ @app.set_variable(VAR_PATCH, '')
56
+ @app.set_variable(VAR_OUTDIR, Config.default_patches_dir)
57
+ @app.set_variable(VAR_STATUS, '')
58
+
59
+ build_file_row(f, 'rom', translate('patcher.rom_label'),
60
+ "{{GBA ROMs} {.gba .zip}} {{All Files} *}")
61
+ build_file_row(f, 'patch', translate('patcher.patch_label'),
62
+ "{{Patch Files} {.ips .bps .ups}} {{All Files} *}")
63
+ build_dir_row(f, 'outdir', translate('patcher.outdir_label'), VAR_OUTDIR)
64
+
65
+ btn_row = "#{f}.btn_row"
66
+ @app.command('ttk::frame', btn_row)
67
+ @app.command(:pack, btn_row, fill: :x, pady: [10, 0])
68
+
69
+ @apply_btn = "#{btn_row}.apply"
70
+ @app.command('ttk::button', @apply_btn,
71
+ text: translate('patcher.apply'),
72
+ command: proc { apply_patch })
73
+ @app.command(:pack, @apply_btn, side: :left)
74
+
75
+ @progress_bar = "#{btn_row}.pb"
76
+ @app.command('ttk::progressbar', @progress_bar,
77
+ orient: :horizontal, length: 200,
78
+ mode: :determinate, maximum: 100)
79
+ @app.command(:pack, @progress_bar, side: :left, padx: [8, 0])
80
+
81
+ if BG_MODE == :thread
82
+ @app.command('ttk::label', "#{btn_row}.ruby_warn",
83
+ text: translate('patcher.thread_mode_warn'),
84
+ foreground: 'gray')
85
+ @app.command(:pack, "#{btn_row}.ruby_warn", side: :left, padx: [10, 0])
86
+ end
87
+
88
+ @app.command('ttk::label', "#{f}.status",
89
+ textvariable: VAR_STATUS,
90
+ wraplength: 500)
91
+ @app.command(:pack, "#{f}.status", fill: :x, pady: [6, 0])
92
+ end
93
+
94
+ def build_file_row(parent, name, label_text, filetypes_tcl)
95
+ row = "#{parent}.#{name}_row"
96
+ var = case name
97
+ when 'rom' then VAR_ROM
98
+ when 'patch' then VAR_PATCH
99
+ end
100
+
101
+ @app.command('ttk::frame', row)
102
+ @app.command(:pack, row, fill: :x, pady: 2)
103
+
104
+ @app.command('ttk::label', "#{row}.lbl", text: label_text, width: 10, anchor: :w)
105
+ @app.command(:grid, "#{row}.lbl", row: 0, column: 0, sticky: :w)
106
+
107
+ @app.command('ttk::entry', "#{row}.ent", textvariable: var, width: 48)
108
+ @app.command(:grid, "#{row}.ent", row: 0, column: 1, sticky: :ew, padx: [4, 4])
109
+
110
+ @app.command('ttk::button', "#{row}.btn",
111
+ text: translate('patcher.browse'),
112
+ command: proc { browse_file(var, filetypes_tcl) })
113
+ @app.command(:grid, "#{row}.btn", row: 0, column: 2)
114
+ @app.command(:grid, :columnconfigure, row, 1, weight: 1)
115
+ end
116
+
117
+ def build_dir_row(parent, name, label_text, var)
118
+ row = "#{parent}.#{name}_row"
119
+ @app.command('ttk::frame', row)
120
+ @app.command(:pack, row, fill: :x, pady: 2)
121
+
122
+ @app.command('ttk::label', "#{row}.lbl", text: label_text, width: 10, anchor: :w)
123
+ @app.command(:grid, "#{row}.lbl", row: 0, column: 0, sticky: :w)
124
+
125
+ @app.command('ttk::entry', "#{row}.ent", textvariable: var, width: 48)
126
+ @app.command(:grid, "#{row}.ent", row: 0, column: 1, sticky: :ew, padx: [4, 4])
127
+
128
+ @app.command('ttk::button', "#{row}.btn",
129
+ text: translate('patcher.browse'),
130
+ command: proc { browse_dir(var) })
131
+ @app.command(:grid, "#{row}.btn", row: 0, column: 2)
132
+ @app.command(:grid, :columnconfigure, row, 1, weight: 1)
133
+ end
134
+
135
+ def browse_file(var, filetypes_tcl)
136
+ path = @app.tcl_eval("tk_getOpenFile -filetypes {#{filetypes_tcl}}")
137
+ @app.set_variable(var, path) unless path.to_s.strip.empty?
138
+ end
139
+
140
+ def browse_dir(var)
141
+ dir = @app.tcl_eval("tk_chooseDirectory")
142
+ @app.set_variable(var, dir) unless dir.to_s.strip.empty?
143
+ end
144
+
145
+ def apply_patch
146
+ rom = @app.get_variable(VAR_ROM).strip
147
+ patch = @app.get_variable(VAR_PATCH).strip
148
+ outdir = @app.get_variable(VAR_OUTDIR).strip
149
+
150
+ if rom.empty? || patch.empty? || outdir.empty?
151
+ set_status(translate('patcher.err_missing_fields'))
152
+ return
153
+ end
154
+
155
+ unless File.exist?(rom)
156
+ set_status(translate('patcher.err_rom_not_found'))
157
+ return
158
+ end
159
+
160
+ unless File.exist?(patch)
161
+ set_status(translate('patcher.err_patch_not_found'))
162
+ return
163
+ end
164
+
165
+ resolved_rom = begin
166
+ RomResolver.resolve(rom)
167
+ rescue => e
168
+ set_status("#{translate('patcher.err_failed')} #{e.message}")
169
+ return
170
+ end
171
+
172
+ rom_ext = File.extname(resolved_rom)
173
+ basename = File.basename(rom, '.*') + '-patched' + rom_ext
174
+ desired_out = File.join(outdir, basename)
175
+
176
+ out_path = if File.exist?(desired_out)
177
+ msg = translate('patcher.overwrite_msg').gsub('{path}', File.basename(desired_out))
178
+ answer = @app.command('tk_messageBox',
179
+ parent: TOP,
180
+ title: translate('patcher.overwrite_title'),
181
+ message: msg,
182
+ type: :yesnocancel,
183
+ icon: :question)
184
+ case answer
185
+ when 'yes' then desired_out
186
+ when 'no' then RomPatcher.safe_out_path(desired_out)
187
+ else return # cancel — abort silently
188
+ end
189
+ else
190
+ desired_out
191
+ end
192
+
193
+ set_status(translate('patcher.working'))
194
+ @app.command(@apply_btn, :configure, state: :disabled)
195
+ @app.command(@progress_bar, :configure, value: 0)
196
+
197
+ data = Ractor.make_shareable({ rom: resolved_rom.freeze, patch: patch.freeze, out: out_path.freeze })
198
+ Teek::BackgroundWork.drop_intermediate = false
199
+ Teek::BackgroundWork.new(@app, data, mode: BG_MODE, worker: PatchWorker).on_progress do |result|
200
+ case result
201
+ when Float
202
+ @app.command(@progress_bar, :configure, value: (result * 100).round)
203
+ @app.update
204
+ when Hash
205
+ @app.command(@apply_btn, :configure, state: :normal)
206
+ @app.command(@progress_bar, :configure, value: result[:ok] ? 100 : 0)
207
+ @app.update
208
+ if result[:ok]
209
+ set_status("#{translate('patcher.done')} #{File.basename(result[:path])}")
210
+ else
211
+ set_status("#{translate('patcher.err_failed')} #{result[:error]}")
212
+ end
213
+ end
214
+ end.on_done do
215
+ @app.command(@apply_btn, :configure, state: :normal)
216
+ end
217
+ end
218
+
219
+ def set_status(msg)
220
+ @app.set_variable(VAR_STATUS, msg)
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemba
4
+ module Platform
5
+ class GB
6
+ def width = 160
7
+ def height = 144
8
+ def fps = 59.7275
9
+ def fps_fraction = [4194304, 70224]
10
+ def aspect = [10, 9]
11
+ def name = "Game Boy"
12
+ def short_name = "GB"
13
+ def buttons = %i[a b start select up down left right]
14
+ def thumb_size = [80, 72]
15
+
16
+ def ==(other) = other.is_a?(Platform::GB)
17
+ def eql?(other) = self == other
18
+ def hash = self.class.hash
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemba
4
+ module Platform
5
+ class GBA
6
+ def width = 240
7
+ def height = 160
8
+ def fps = 59.7272
9
+ def fps_fraction = [262144, 4389]
10
+ def aspect = [3, 2]
11
+ def name = "Game Boy Advance"
12
+ def short_name = "GBA"
13
+ def buttons = %i[a b l r start select up down left right]
14
+ def thumb_size = [120, 80]
15
+
16
+ def ==(other) = other.is_a?(Platform::GBA)
17
+ def eql?(other) = self == other
18
+ def hash = self.class.hash
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemba
4
+ module Platform
5
+ # Same hardware specs as GB (resolution, FPS, buttons).
6
+ # Separate class for distinct name and future color-specific behavior.
7
+ class GBC
8
+ def width = 160
9
+ def height = 144
10
+ def fps = 59.7275
11
+ def fps_fraction = [4194304, 70224]
12
+ def aspect = [10, 9]
13
+ def name = "Game Boy Color"
14
+ def short_name = "GBC"
15
+ def buttons = %i[a b start select up down left right]
16
+ def thumb_size = [80, 72]
17
+
18
+ def ==(other) = other.is_a?(Platform::GBC)
19
+ def eql?(other) = self == other
20
+ def hash = self.class.hash
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Gemba
5
+ module Platform
6
+ # Build a Platform from a loaded Core.
7
+ # @param core [Gemba::Core] initialized core with ROM loaded
8
+ # @return [GBA, GB, GBC]
9
+ def self.for(core)
10
+ case core.platform
11
+ when "GBA" then GBA.new
12
+ when "GBC" then GBC.new
13
+ else GB.new
14
+ end
15
+ end
16
+
17
+ # Default platform before any ROM is loaded (most common case).
18
+ def self.default = GBA.new
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Gemba
6
+ # Open a directory in the platform's file manager.
7
+ # @param dir [String] directory path (created if missing)
8
+ def self.open_directory(dir)
9
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
10
+ p = Teek.platform
11
+ if p.darwin?
12
+ system('open', dir)
13
+ elsif p.windows?
14
+ system('explorer.exe', dir.tr('/', '\\'))
15
+ else
16
+ system('xdg-open', dir)
17
+ end
18
+ end
19
+ end
@@ -30,13 +30,14 @@ module Gemba
30
30
  # @param audio_rate [Integer] audio sample rate (default 44100)
31
31
  # @param audio_channels [Integer] audio channels (default 2)
32
32
  # @param compression [Integer] zlib compression level 1-9 (default 1 = fastest)
33
- def initialize(path, width:, height:, audio_rate: 44100, audio_channels: 2,
34
- compression: Zlib::BEST_SPEED)
33
+ def initialize(path, width:, height:, fps_fraction:, audio_rate: 44100,
34
+ audio_channels: 2, compression: Zlib::BEST_SPEED)
35
35
  @path = path
36
36
  @width = width
37
37
  @height = height
38
38
  @audio_rate = audio_rate
39
39
  @audio_channels = audio_channels
40
+ @fps_fraction = fps_fraction
40
41
  @compression = compression
41
42
  @frame_size = width * height * 4
42
43
  @recording = false
@@ -127,7 +128,7 @@ module Gemba
127
128
  h << MAGIC # 8 bytes
128
129
  h << [VERSION].pack('C') # 1 byte
129
130
  h << [@width, @height].pack('v2') # 4 bytes
130
- h << [262_144, 4389].pack('V2') # 8 bytes (fps = 262144/4389 ≈ 59.7272)
131
+ h << @fps_fraction.pack('V2') # 8 bytes (fps as numerator/denominator)
131
132
  h << [@audio_rate].pack('V') # 4 bytes
132
133
  h << [@audio_channels, 16].pack('C2') # 2 bytes
133
134
  h << ("\0" * 5) # 5 bytes reserved