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.
- checksums.yaml +4 -4
- data/THIRD_PARTY_NOTICES +37 -2
- data/assets/placeholder_boxart.png +0 -0
- data/bin/gemba +2 -2
- data/ext/gemba/extconf.rb +23 -1
- data/ext/gemba/gemba_ext.c +436 -2
- data/ext/gemba/gemba_ext.h +2 -0
- data/gemba.gemspec +5 -3
- data/lib/gemba/achievements/achievement.rb +23 -0
- data/lib/gemba/achievements/backend.rb +186 -0
- data/lib/gemba/achievements/cache.rb +70 -0
- data/lib/gemba/achievements/credentials_presenter.rb +142 -0
- data/lib/gemba/achievements/fake_backend.rb +205 -0
- data/lib/gemba/achievements/null_backend.rb +11 -0
- data/lib/gemba/achievements/offline_backend.rb +168 -0
- data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1015 -0
- data/lib/gemba/bios.rb +54 -0
- data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
- data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
- data/lib/gemba/boxart_fetcher.rb +79 -0
- data/lib/gemba/bus_emitter.rb +13 -0
- data/lib/gemba/child_window.rb +24 -1
- data/lib/gemba/cli/commands/config_cmd.rb +83 -0
- data/lib/gemba/cli/commands/decode.rb +154 -0
- data/lib/gemba/cli/commands/patch.rb +78 -0
- data/lib/gemba/cli/commands/play.rb +78 -0
- data/lib/gemba/cli/commands/record.rb +114 -0
- data/lib/gemba/cli/commands/replay.rb +161 -0
- data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
- data/lib/gemba/cli/commands/version.rb +22 -0
- data/lib/gemba/cli.rb +52 -364
- data/lib/gemba/config.rb +134 -1
- data/lib/gemba/data/gb_games.json +1 -0
- data/lib/gemba/data/gb_md5.json +1 -0
- data/lib/gemba/data/gba_games.json +1 -0
- data/lib/gemba/data/gba_md5.json +1 -0
- data/lib/gemba/data/gbc_games.json +1 -0
- data/lib/gemba/data/gbc_md5.json +1 -0
- data/lib/gemba/emulator_frame.rb +1060 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +60 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +268 -0
- data/lib/gemba/gamepad_map.rb +103 -0
- data/lib/gemba/headless.rb +6 -5
- data/lib/gemba/headless_player.rb +33 -3
- data/lib/gemba/help_window.rb +61 -0
- data/lib/gemba/hotkey_map.rb +3 -1
- data/lib/gemba/input_recorder.rb +107 -0
- data/lib/gemba/input_replayer.rb +119 -0
- data/lib/gemba/keyboard_map.rb +90 -0
- data/lib/gemba/locales/en.yml +97 -5
- data/lib/gemba/locales/ja.yml +97 -5
- data/lib/gemba/main_window.rb +56 -0
- data/lib/gemba/modal_stack.rb +81 -0
- data/lib/gemba/patcher_window.rb +223 -0
- data/lib/gemba/platform/gb.rb +21 -0
- data/lib/gemba/platform/gba.rb +21 -0
- data/lib/gemba/platform/gbc.rb +23 -0
- data/lib/gemba/platform.rb +20 -0
- data/lib/gemba/platform_open.rb +19 -0
- data/lib/gemba/recorder.rb +4 -3
- data/lib/gemba/replay_player.rb +691 -0
- data/lib/gemba/rom_info.rb +57 -0
- data/lib/gemba/rom_info_window.rb +16 -3
- data/lib/gemba/rom_library.rb +106 -0
- data/lib/gemba/rom_overrides.rb +47 -0
- data/lib/gemba/rom_patcher/bps.rb +161 -0
- data/lib/gemba/rom_patcher/ips.rb +101 -0
- data/lib/gemba/rom_patcher/ups.rb +118 -0
- data/lib/gemba/rom_patcher.rb +109 -0
- data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
- data/lib/gemba/runtime.rb +59 -26
- data/lib/gemba/save_state_manager.rb +4 -7
- data/lib/gemba/save_state_picker.rb +17 -4
- data/lib/gemba/session_logger.rb +64 -0
- data/lib/gemba/settings/audio_tab.rb +77 -0
- data/lib/gemba/settings/gamepad_tab.rb +351 -0
- data/lib/gemba/settings/hotkeys_tab.rb +259 -0
- data/lib/gemba/settings/paths.rb +11 -0
- data/lib/gemba/settings/recording_tab.rb +83 -0
- data/lib/gemba/settings/save_states_tab.rb +91 -0
- data/lib/gemba/settings/system_tab.rb +362 -0
- data/lib/gemba/settings/video_tab.rb +318 -0
- data/lib/gemba/settings_window.rb +162 -1036
- data/lib/gemba/version.rb +1 -1
- data/lib/gemba/virtual_keyboard.rb +19 -0
- data/lib/gemba.rb +2 -12
- data/test/achievements_window/test_bulk_sync.rb +218 -0
- data/test/achievements_window/test_bus_events.rb +125 -0
- data/test/achievements_window/test_close_confirmation.rb +201 -0
- data/test/achievements_window/test_initial_state.rb +164 -0
- data/test/achievements_window/test_sorting.rb +227 -0
- data/test/achievements_window/test_tree_rendering.rb +133 -0
- data/test/fixtures/fake_bios.bin +0 -0
- data/test/fixtures/pong.gba +0 -0
- data/test/fixtures/test.gb +0 -0
- data/test/fixtures/test.gbc +0 -0
- data/test/fixtures/test_quicksave.ss +0 -0
- data/test/screenshots/no_focus.png +0 -0
- data/test/shared/teek_test_worker.rb +17 -1
- data/test/shared/tk_test_helper.rb +91 -4
- data/test/support/achievements_window_helpers.rb +18 -0
- data/test/support/fake_core.rb +25 -0
- data/test/support/fake_ra_runtime.rb +74 -0
- data/test/support/fake_requester.rb +68 -0
- data/test/support/player_helpers.rb +20 -5
- data/test/test_achievement.rb +32 -0
- data/test/{test_player.rb → test_app_controller.rb} +353 -85
- data/test/test_bios.rb +123 -0
- data/test/test_boxart_fetcher.rb +150 -0
- data/test/test_cli.rb +17 -265
- data/test/test_cli_config.rb +64 -0
- data/test/test_cli_decode.rb +97 -0
- data/test/test_cli_patch.rb +58 -0
- data/test/test_cli_play.rb +213 -0
- data/test/test_cli_ra.rb +175 -0
- data/test/test_cli_record.rb +69 -0
- data/test/test_cli_replay.rb +72 -0
- data/test/test_cli_sync_requester.rb +152 -0
- data/test/test_cli_version.rb +27 -0
- data/test/test_config.rb +2 -3
- data/test/test_config_ra.rb +69 -0
- data/test/test_core.rb +62 -1
- data/test/test_credentials_presenter.rb +192 -0
- data/test/test_event_bus.rb +100 -0
- data/test/test_fake_backend_achievements.rb +130 -0
- data/test/test_fake_backend_auth.rb +68 -0
- data/test/test_game_index.rb +77 -0
- data/test/test_game_picker_frame.rb +310 -0
- data/test/test_gamepad_map.rb +1 -3
- data/test/test_headless_player.rb +17 -3
- data/test/test_help_window.rb +82 -0
- data/test/test_hotkey_map.rb +22 -1
- data/test/test_input_recorder.rb +179 -0
- data/test/test_input_replay_determinism.rb +113 -0
- data/test/test_input_replayer.rb +162 -0
- data/test/test_keyboard_map.rb +1 -3
- data/test/test_libretro_backend.rb +41 -0
- data/test/test_locale.rb +1 -1
- data/test/test_logging.rb +123 -0
- data/test/test_null_backend.rb +42 -0
- data/test/test_offline_backend.rb +116 -0
- data/test/test_overlay_renderer.rb +1 -1
- data/test/test_platform.rb +149 -0
- data/test/test_ra_backend.rb +313 -0
- data/test/test_ra_backend_unlock_gate.rb +56 -0
- data/test/test_recorder.rb +0 -3
- data/test/test_replay_player.rb +316 -0
- data/test/test_rom_info.rb +149 -0
- data/test/test_rom_overrides.rb +86 -0
- data/test/test_rom_patcher.rb +382 -0
- data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
- data/test/test_save_state_manager.rb +2 -4
- data/test/test_settings_audio.rb +107 -0
- data/test/test_settings_hotkeys.rb +83 -66
- data/test/test_settings_recording.rb +49 -0
- data/test/test_settings_save_states.rb +97 -0
- data/test/test_settings_system.rb +133 -0
- data/test/test_settings_video.rb +450 -0
- data/test/test_settings_window.rb +76 -507
- data/test/test_tip_service.rb +6 -6
- data/test/test_toast_overlay.rb +1 -1
- data/test/test_virtual_events.rb +156 -0
- data/test/test_virtual_keyboard.rb +1 -1
- data/vendor/rcheevos/CHANGELOG.md +495 -0
- data/vendor/rcheevos/LICENSE +21 -0
- data/vendor/rcheevos/Package.swift +33 -0
- data/vendor/rcheevos/README.md +67 -0
- data/vendor/rcheevos/include/module.modulemap +70 -0
- data/vendor/rcheevos/include/rc_api_editor.h +296 -0
- data/vendor/rcheevos/include/rc_api_info.h +280 -0
- data/vendor/rcheevos/include/rc_api_request.h +77 -0
- data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
- data/vendor/rcheevos/include/rc_api_user.h +262 -0
- data/vendor/rcheevos/include/rc_client.h +877 -0
- data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
- data/vendor/rcheevos/include/rc_consoles.h +138 -0
- data/vendor/rcheevos/include/rc_error.h +59 -0
- data/vendor/rcheevos/include/rc_export.h +100 -0
- data/vendor/rcheevos/include/rc_hash.h +200 -0
- data/vendor/rcheevos/include/rc_runtime.h +148 -0
- data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
- data/vendor/rcheevos/include/rc_util.h +51 -0
- data/vendor/rcheevos/include/rcheevos.h +8 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
- data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
- data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
- data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
- data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
- data/vendor/rcheevos/src/rc_client.c +6941 -0
- data/vendor/rcheevos/src/rc_client_external.c +281 -0
- data/vendor/rcheevos/src/rc_client_external.h +177 -0
- data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
- data/vendor/rcheevos/src/rc_client_internal.h +409 -0
- data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
- data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
- data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
- data/vendor/rcheevos/src/rc_compat.c +251 -0
- data/vendor/rcheevos/src/rc_compat.h +121 -0
- data/vendor/rcheevos/src/rc_libretro.c +915 -0
- data/vendor/rcheevos/src/rc_libretro.h +98 -0
- data/vendor/rcheevos/src/rc_util.c +199 -0
- data/vendor/rcheevos/src/rc_version.c +11 -0
- data/vendor/rcheevos/src/rc_version.h +32 -0
- data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
- data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
- data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
- data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
- data/vendor/rcheevos/src/rcheevos/format.c +330 -0
- data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
- data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
- data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
- data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
- data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
- data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
- data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
- data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
- data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
- data/vendor/rcheevos/src/rcheevos/value.c +935 -0
- data/vendor/rcheevos/src/rhash/aes.c +480 -0
- data/vendor/rcheevos/src/rhash/aes.h +49 -0
- data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
- data/vendor/rcheevos/src/rhash/hash.c +1402 -0
- data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
- data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
- data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
- data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
- data/vendor/rcheevos/src/rhash/md5.c +382 -0
- data/vendor/rcheevos/src/rhash/md5.h +91 -0
- data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
- data/vendor/rcheevos/test/libretro.h +205 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
- data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
- data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
- data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
- data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
- data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
- data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
- data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
- data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
- data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
- data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
- data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
- data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
- data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
- data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
- data/vendor/rcheevos/test/rhash/data.c +657 -0
- data/vendor/rcheevos/test/rhash/data.h +32 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
- data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
- data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
- data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
- data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
- data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
- data/vendor/rcheevos/test/test.c +113 -0
- data/vendor/rcheevos/test/test_framework.h +205 -0
- data/vendor/rcheevos/test/test_rc_client.c +10509 -0
- data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
- data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
- data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
- data/vendor/rcheevos/test/test_types.natvis +9 -0
- data/vendor/rcheevos/validator/validator.c +658 -0
- data/vendor/rcheevos/validator/validator.vcxproj +152 -0
- data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
- metadata +274 -11
- data/lib/gemba/input_mappings.rb +0 -214
- data/lib/gemba/player.rb +0 -1525
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Floating hotkey reference panel toggled by pressing '?'.
|
|
5
|
+
#
|
|
6
|
+
# Non-modal — no grab, no focus steal. Positioned to the right of the
|
|
7
|
+
# main window via ChildWindow#position_near_parent. AppController pauses
|
|
8
|
+
# emulation while the panel is visible and restores play on close.
|
|
9
|
+
class HelpWindow
|
|
10
|
+
include ChildWindow
|
|
11
|
+
include Locale::Translatable
|
|
12
|
+
|
|
13
|
+
TOP = '.help_window'
|
|
14
|
+
|
|
15
|
+
def initialize(app:, hotkeys:)
|
|
16
|
+
@app = app
|
|
17
|
+
@hotkeys = hotkeys
|
|
18
|
+
build_toplevel(translate('settings.hotkeys'), geometry: '220x400') { build_ui }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show = show_window(modal: false)
|
|
22
|
+
def hide = hide_window(modal: false)
|
|
23
|
+
def visible? = @app.tcl_eval("wm state #{TOP}") == 'normal'
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_ui
|
|
28
|
+
f = "#{TOP}.f"
|
|
29
|
+
@app.command('ttk::frame', f, padding: 8)
|
|
30
|
+
@app.command(:pack, f, fill: :both, expand: 1)
|
|
31
|
+
|
|
32
|
+
@app.command('ttk::label', "#{f}.title",
|
|
33
|
+
text: translate('settings.hotkeys'),
|
|
34
|
+
font: '{TkDefaultFont} 11 bold')
|
|
35
|
+
@app.command(:pack, "#{f}.title", pady: [0, 4])
|
|
36
|
+
|
|
37
|
+
@app.command('ttk::separator', "#{f}.sep", orient: :horizontal)
|
|
38
|
+
@app.command(:pack, "#{f}.sep", fill: :x, pady: [0, 6])
|
|
39
|
+
|
|
40
|
+
Settings::HotkeysTab::LOCALE_KEYS.each do |action, locale_key|
|
|
41
|
+
row = "#{f}.row_#{action}"
|
|
42
|
+
@app.command('ttk::frame', row)
|
|
43
|
+
@app.command(:pack, row, fill: :x, pady: 1)
|
|
44
|
+
|
|
45
|
+
act_lbl = "#{row}.act"
|
|
46
|
+
key_lbl = "#{row}.key"
|
|
47
|
+
|
|
48
|
+
key_text = HotkeyMap.display_name(@hotkeys.key_for(action))
|
|
49
|
+
|
|
50
|
+
@app.command('ttk::label', act_lbl, text: translate(locale_key), anchor: :w)
|
|
51
|
+
@app.command('ttk::label', key_lbl, text: key_text, anchor: :e,
|
|
52
|
+
font: '{TkFixedFont} 9')
|
|
53
|
+
|
|
54
|
+
@app.command(:grid, act_lbl, row: 0, column: 0, sticky: :w)
|
|
55
|
+
@app.command(:grid, key_lbl, row: 0, column: 1, sticky: :e)
|
|
56
|
+
@app.command(:grid, :columnconfigure, row, 0, weight: 1)
|
|
57
|
+
@app.command(:grid, :columnconfigure, row, 1, weight: 0)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/gemba/hotkey_map.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Gemba
|
|
|
11
11
|
class HotkeyMap
|
|
12
12
|
ACTIONS = %i[quit pause fast_forward fullscreen show_fps
|
|
13
13
|
quick_save quick_load save_states screenshot rewind
|
|
14
|
-
record].freeze
|
|
14
|
+
record input_record open_rom].freeze
|
|
15
15
|
|
|
16
16
|
DEFAULTS = {
|
|
17
17
|
quit: 'q', pause: 'p', fast_forward: 'Tab',
|
|
@@ -20,6 +20,8 @@ module Gemba
|
|
|
20
20
|
save_states: 'F6', screenshot: 'F9',
|
|
21
21
|
rewind: ['Shift', 'Tab'],
|
|
22
22
|
record: 'F10',
|
|
23
|
+
input_record: 'F4',
|
|
24
|
+
open_rom: ['Control', 'o'],
|
|
23
25
|
}.freeze
|
|
24
26
|
|
|
25
27
|
# Tk keysyms that are modifier keys → normalized name
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Records per-frame input bitmasks to a .gir (Gemba Input Recording) file.
|
|
5
|
+
#
|
|
6
|
+
# Each GBA frame's pressed-button bitmask is stored as a 3-char hex line.
|
|
7
|
+
# An anchor save state is written alongside so replays start from the
|
|
8
|
+
# exact same emulator state.
|
|
9
|
+
#
|
|
10
|
+
# The header's frame_count is a best-effort hint (correct on clean stop,
|
|
11
|
+
# zero on crash). The replayer counts lines for the authoritative count.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# recorder = InputRecorder.new("session.gir", core: core)
|
|
15
|
+
# recorder.start
|
|
16
|
+
# loop do
|
|
17
|
+
# mask = poll_input
|
|
18
|
+
# recorder.capture(mask)
|
|
19
|
+
# core.set_keys(mask)
|
|
20
|
+
# core.run_frame
|
|
21
|
+
# end
|
|
22
|
+
# recorder.stop
|
|
23
|
+
class InputRecorder
|
|
24
|
+
VERSION = 1
|
|
25
|
+
FLUSH_INTERVAL = 60 # frames between flushes (~1s at 59.7 fps)
|
|
26
|
+
|
|
27
|
+
# @param path [String] output .gir file path
|
|
28
|
+
# @param core [Gemba::Core] mGBA core (for ROM metadata and save state)
|
|
29
|
+
# @param rom_path [String, nil] path to the ROM file (stored in header for easy replay)
|
|
30
|
+
def initialize(path, core:, rom_path: nil)
|
|
31
|
+
@path = path
|
|
32
|
+
@core = core
|
|
33
|
+
@rom_path = rom_path
|
|
34
|
+
@recording = false
|
|
35
|
+
@frame_count = 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Start recording. Saves an anchor save state and opens the .gir file.
|
|
39
|
+
def start
|
|
40
|
+
raise "Already recording" if @recording
|
|
41
|
+
|
|
42
|
+
@core.save_state_to_file(anchor_state_path)
|
|
43
|
+
@frame_count = 0
|
|
44
|
+
@file = File.open(@path, 'w')
|
|
45
|
+
write_header
|
|
46
|
+
@file.flush
|
|
47
|
+
@recording = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Capture one frame's input bitmask.
|
|
51
|
+
# @param bitmask [Integer] bitwise OR of KEY_* constants (0x000–0x3FF)
|
|
52
|
+
def capture(bitmask)
|
|
53
|
+
return unless @recording
|
|
54
|
+
|
|
55
|
+
@file.puts(format('%03x', bitmask & 0x3FF))
|
|
56
|
+
@frame_count += 1
|
|
57
|
+
@file.flush if (@frame_count % FLUSH_INTERVAL).zero?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Stop recording and close the file.
|
|
61
|
+
def stop
|
|
62
|
+
return unless @recording
|
|
63
|
+
|
|
64
|
+
@recording = false
|
|
65
|
+
rewrite_frame_count
|
|
66
|
+
@file.close
|
|
67
|
+
@file = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Boolean] true if currently recording
|
|
71
|
+
def recording?
|
|
72
|
+
@recording
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Integer] number of frames captured so far
|
|
76
|
+
attr_reader :frame_count
|
|
77
|
+
|
|
78
|
+
# @return [String] path to the anchor save state file
|
|
79
|
+
def anchor_state_path
|
|
80
|
+
@path.sub(/\.gir\z/, '.state')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
FRAME_COUNT_WIDTH = 10 # zero-padded digits (covers ~5.3 years at 60fps)
|
|
86
|
+
|
|
87
|
+
def write_header
|
|
88
|
+
@file.puts "# GEMBA INPUT RECORDING v#{VERSION}"
|
|
89
|
+
@file.puts "# rom_checksum: #{@core.checksum}"
|
|
90
|
+
@file.puts "# game_code: #{@core.game_code}"
|
|
91
|
+
@file.puts "# rom_path: #{@rom_path}" if @rom_path
|
|
92
|
+
@file.write "# frame_count: "
|
|
93
|
+
@frame_count_offset = @file.pos
|
|
94
|
+
@file.puts format("%0#{FRAME_COUNT_WIDTH}d", 0)
|
|
95
|
+
@file.puts "# anchor_state: #{File.basename(anchor_state_path)}"
|
|
96
|
+
@file.puts "---"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Best-effort: seek to the frame_count field and overwrite in place.
|
|
100
|
+
def rewrite_frame_count
|
|
101
|
+
@file.flush
|
|
102
|
+
@file.seek(@frame_count_offset)
|
|
103
|
+
@file.write(format("%0#{FRAME_COUNT_WIDTH}d", @frame_count))
|
|
104
|
+
@file.seek(0, IO::SEEK_END)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Replays a .gir (Gemba Input Recording) file by feeding recorded
|
|
5
|
+
# per-frame bitmasks back to the emulator core.
|
|
6
|
+
#
|
|
7
|
+
# The authoritative frame count comes from counting bitmask lines,
|
|
8
|
+
# not the header (which is best-effort and may be zero on crash).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# replayer = InputReplayer.new("session.gir")
|
|
12
|
+
# replayer.validate!(core)
|
|
13
|
+
# core.load_state_from_file(replayer.anchor_state_path)
|
|
14
|
+
# replayer.each_bitmask do |mask, frame|
|
|
15
|
+
# core.set_keys(mask)
|
|
16
|
+
# core.run_frame
|
|
17
|
+
# end
|
|
18
|
+
class InputReplayer
|
|
19
|
+
class ChecksumMismatch < StandardError; end
|
|
20
|
+
|
|
21
|
+
# @param gir_path [String] path to .gir file
|
|
22
|
+
def initialize(gir_path)
|
|
23
|
+
@path = gir_path
|
|
24
|
+
@header = {}
|
|
25
|
+
@bitmasks = []
|
|
26
|
+
parse!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Integer] ROM checksum from the recording header
|
|
30
|
+
def rom_checksum
|
|
31
|
+
@header[:rom_checksum]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [String] game code from the recording header
|
|
35
|
+
def game_code
|
|
36
|
+
@header[:game_code]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [String, nil] ROM path from the recording header
|
|
40
|
+
def rom_path
|
|
41
|
+
@header[:rom_path]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Integer] number of recorded frames (counted from bitmask lines)
|
|
45
|
+
def frame_count
|
|
46
|
+
@bitmasks.length
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [String] path to the anchor save state file
|
|
50
|
+
def anchor_state_path
|
|
51
|
+
dir = File.dirname(@path)
|
|
52
|
+
File.join(dir, @header[:anchor_state])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate that the recording matches the loaded ROM.
|
|
56
|
+
# @param core [Gemba::Core] mGBA core to validate against
|
|
57
|
+
# @raise [ChecksumMismatch] if ROM checksum doesn't match
|
|
58
|
+
def validate!(core)
|
|
59
|
+
if rom_checksum && core.checksum != rom_checksum
|
|
60
|
+
raise ChecksumMismatch,
|
|
61
|
+
"ROM checksum mismatch: recording has #{rom_checksum}, " \
|
|
62
|
+
"loaded ROM has #{core.checksum}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param frame [Integer] zero-based frame index
|
|
67
|
+
# @return [Integer] bitmask for the given frame
|
|
68
|
+
def bitmask_at(frame)
|
|
69
|
+
@bitmasks[frame]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Iterate over all recorded bitmasks.
|
|
73
|
+
# @yield [Integer, Integer] bitmask and zero-based frame index
|
|
74
|
+
def each_bitmask(&block)
|
|
75
|
+
@bitmasks.each_with_index(&block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def parse!
|
|
81
|
+
in_header = true
|
|
82
|
+
|
|
83
|
+
File.foreach(@path) do |line|
|
|
84
|
+
line = line.strip
|
|
85
|
+
|
|
86
|
+
if in_header
|
|
87
|
+
if line == '---'
|
|
88
|
+
in_header = false
|
|
89
|
+
elsif line.start_with?('# ')
|
|
90
|
+
parse_header_line(line)
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
next if line.empty?
|
|
94
|
+
@bitmasks << line.to_i(16)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_header_line(line)
|
|
100
|
+
# Format: "# key: value"
|
|
101
|
+
content = line.sub(/^# /, '')
|
|
102
|
+
key, _, value = content.partition(': ')
|
|
103
|
+
return if value.empty?
|
|
104
|
+
|
|
105
|
+
case key
|
|
106
|
+
when 'rom_checksum'
|
|
107
|
+
@header[:rom_checksum] = value.to_i
|
|
108
|
+
when 'game_code'
|
|
109
|
+
@header[:game_code] = value
|
|
110
|
+
when 'anchor_state'
|
|
111
|
+
@header[:anchor_state] = value
|
|
112
|
+
when 'rom_path'
|
|
113
|
+
@header[:rom_path] = value
|
|
114
|
+
when 'frame_count'
|
|
115
|
+
@header[:header_frame_count] = value.to_i
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Manages keyboard keysym → GBA bitmask mappings.
|
|
5
|
+
#
|
|
6
|
+
# Shares the same interface as {GamepadMap} so that Player can
|
|
7
|
+
# delegate to either without knowing which device type is active.
|
|
8
|
+
class KeyboardMap
|
|
9
|
+
DEFAULT_MAP = {
|
|
10
|
+
'z' => KEY_A,
|
|
11
|
+
'x' => KEY_B,
|
|
12
|
+
'BackSpace' => KEY_SELECT,
|
|
13
|
+
'Return' => KEY_START,
|
|
14
|
+
'Right' => KEY_RIGHT,
|
|
15
|
+
'Left' => KEY_LEFT,
|
|
16
|
+
'Up' => KEY_UP,
|
|
17
|
+
'Down' => KEY_DOWN,
|
|
18
|
+
'a' => KEY_L,
|
|
19
|
+
's' => KEY_R,
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def initialize(config)
|
|
23
|
+
@config = config
|
|
24
|
+
@map = DEFAULT_MAP.dup
|
|
25
|
+
@device = nil
|
|
26
|
+
load_config
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_writer :device
|
|
30
|
+
|
|
31
|
+
def mask
|
|
32
|
+
return 0 unless @device
|
|
33
|
+
m = 0
|
|
34
|
+
@map.each { |key, bit| m |= bit if @device.button?(key) }
|
|
35
|
+
m
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set(gba_btn, input_key)
|
|
39
|
+
bit = GBA_BTN_BITS[gba_btn] or return
|
|
40
|
+
@map.delete_if { |_, v| v == bit }
|
|
41
|
+
@map[input_key.to_s] = bit
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset!
|
|
45
|
+
@map = DEFAULT_MAP.dup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def load_config
|
|
49
|
+
cfg = @config.mappings(Config::KEYBOARD_GUID)
|
|
50
|
+
if cfg.empty?
|
|
51
|
+
@map = DEFAULT_MAP.dup
|
|
52
|
+
else
|
|
53
|
+
@map = {}
|
|
54
|
+
cfg.each do |gba_str, keysym|
|
|
55
|
+
bit = GBA_BTN_BITS[gba_str.to_sym]
|
|
56
|
+
next unless bit
|
|
57
|
+
@map[keysym] = bit
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reload!
|
|
63
|
+
@config.reload!
|
|
64
|
+
load_config
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def labels
|
|
68
|
+
result = {}
|
|
69
|
+
@map.each do |input, bit|
|
|
70
|
+
gba_btn = GBA_BTN_BITS.key(bit)
|
|
71
|
+
result[gba_btn] = input if gba_btn
|
|
72
|
+
end
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def save_to_config
|
|
77
|
+
@map.each do |input, bit|
|
|
78
|
+
gba_btn = GBA_BTN_BITS.key(bit)
|
|
79
|
+
@config.set_mapping(Config::KEYBOARD_GUID, gba_btn, input) if gba_btn
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def supports_deadzone? = false
|
|
84
|
+
def dead_zone_pct = 0
|
|
85
|
+
|
|
86
|
+
def set_dead_zone(_)
|
|
87
|
+
raise NotImplementedError, "keyboard does not support dead zones"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/gemba/locales/en.yml
CHANGED
|
@@ -5,8 +5,12 @@ menu:
|
|
|
5
5
|
quit: "Quit"
|
|
6
6
|
settings: "Settings"
|
|
7
7
|
view: "View"
|
|
8
|
+
game_library: "Game Library"
|
|
8
9
|
fullscreen: "Fullscreen"
|
|
9
10
|
rom_info: "ROM Info…"
|
|
11
|
+
achievements: "Achievements…"
|
|
12
|
+
open_logs_dir: "Open Logs Directory"
|
|
13
|
+
patch_rom: "Patch ROM…"
|
|
10
14
|
emulation: "Emulation"
|
|
11
15
|
pause: "Pause"
|
|
12
16
|
resume: "Resume"
|
|
@@ -14,8 +18,10 @@ menu:
|
|
|
14
18
|
quick_save: "Quick Save"
|
|
15
19
|
quick_load: "Quick Load"
|
|
16
20
|
save_states: "Save States…"
|
|
17
|
-
start_recording: "Start
|
|
18
|
-
stop_recording: "Stop
|
|
21
|
+
start_recording: "Start Capture"
|
|
22
|
+
stop_recording: "Stop Capture"
|
|
23
|
+
start_input_recording: "Start Recording Inputs"
|
|
24
|
+
stop_input_recording: "Stop Recording Inputs"
|
|
19
25
|
|
|
20
26
|
toast:
|
|
21
27
|
save_blocked: "Save blocked (too fast)"
|
|
@@ -32,12 +38,18 @@ toast:
|
|
|
32
38
|
no_rewind: "No rewind data"
|
|
33
39
|
paused: "Paused"
|
|
34
40
|
waiting_for: "Waiting for {label}…"
|
|
35
|
-
recording_started: "
|
|
36
|
-
recording_stopped: "
|
|
41
|
+
recording_started: "Capture started"
|
|
42
|
+
recording_stopped: "Captured {frames} frames"
|
|
43
|
+
input_recording_started: "Recording inputs"
|
|
44
|
+
input_recording_stopped: "Inputs recorded ({frames} frames)"
|
|
37
45
|
|
|
38
46
|
dialog:
|
|
39
47
|
game_running_title: "Game Running"
|
|
40
48
|
game_running_msg: "Another game is running. Switch to {name}?"
|
|
49
|
+
return_to_library_title: "Return to Game Library"
|
|
50
|
+
return_to_library_msg: "Return to the Game Library? Unsaved progress will be lost."
|
|
51
|
+
quit_title: "Quit Gemba"
|
|
52
|
+
quit_msg: "A game is running. Quit anyway? Unsaved progress will be lost."
|
|
41
53
|
drop_error_title: "Drop Error"
|
|
42
54
|
drop_single_file_only: "Please drop a single ROM file."
|
|
43
55
|
drop_unsupported_type: "Unsupported file type: {ext}"
|
|
@@ -51,6 +63,8 @@ dialog:
|
|
|
51
63
|
reset_hotkeys_title: "Reset Hotkeys"
|
|
52
64
|
reset_hotkeys_msg: "Reset all hotkey bindings to defaults?"
|
|
53
65
|
key_conflict_title: "Key Conflict"
|
|
66
|
+
cancel_bulk_sync_title: "Cancel Sync?"
|
|
67
|
+
cancel_bulk_sync_msg: "A bulk sync is in progress. Closing will cancel it — games will need to be re-synced manually."
|
|
54
68
|
|
|
55
69
|
settings:
|
|
56
70
|
video: "Video"
|
|
@@ -91,7 +105,9 @@ settings:
|
|
|
91
105
|
hk_save_states: "Save States"
|
|
92
106
|
hk_screenshot: "Screenshot"
|
|
93
107
|
hk_rewind: "Rewind"
|
|
94
|
-
hk_record: "
|
|
108
|
+
hk_record: "Capture"
|
|
109
|
+
hk_input_record: "Record Inputs"
|
|
110
|
+
hk_open_rom: "Open ROM"
|
|
95
111
|
hk_reset_defaults: "Reset to Defaults"
|
|
96
112
|
pixel_filter: "Pixel Filter:"
|
|
97
113
|
filter_nearest: "Nearest Neighbor"
|
|
@@ -111,11 +127,39 @@ settings:
|
|
|
111
127
|
tip_per_game: "Save separate video, audio, and save state settings for each ROM."
|
|
112
128
|
tip_turbo_speed: "Fast-forward speed when holding the turbo hotkey."
|
|
113
129
|
tip_toast_duration: "How long on-screen notifications stay visible."
|
|
130
|
+
system: "System"
|
|
131
|
+
bios_header: "GBA BIOS"
|
|
132
|
+
bios_path: "BIOS file:"
|
|
133
|
+
bios_browse: "Browse…"
|
|
134
|
+
bios_clear: "Clear"
|
|
135
|
+
bios_not_set: "Not set — using built-in HLE (recommended for most games)"
|
|
136
|
+
bios_not_found: "File not found"
|
|
137
|
+
skip_bios: "Skip boot animation"
|
|
138
|
+
tip_skip_bios: "Jump straight to the game, skipping the Game Boy Advance logo screen.\nOnly applies when a real BIOS file is loaded."
|
|
139
|
+
retroachievements: "Achievements"
|
|
140
|
+
ra_enabled: "Enable RetroAchievements"
|
|
141
|
+
ra_credentials: "Account"
|
|
142
|
+
ra_username_placeholder: "Username:"
|
|
143
|
+
ra_token_placeholder: "Password:"
|
|
144
|
+
ra_rich_presence: "Rich Presence (per-game)"
|
|
145
|
+
ra_hardcore: "Hardcore mode (disables save states and rewind)"
|
|
146
|
+
tip_ra_password: "Your password is only used to fetch an API token and is never saved."
|
|
147
|
+
ra_login: "Login"
|
|
148
|
+
ra_verify: "Verify Token"
|
|
149
|
+
ra_logout: "Logout"
|
|
150
|
+
ra_reset: "Reset"
|
|
151
|
+
ra_reset_title: "Reset RetroAchievements"
|
|
152
|
+
ra_reset_confirm: "Clear saved credentials and log out?"
|
|
153
|
+
ra_test_ok: "Connection OK ✓"
|
|
154
|
+
ra_disabled: "RetroAchievements is disabled"
|
|
155
|
+
ra_not_logged_in: "Not logged in"
|
|
156
|
+
ra_logged_in_as: "Logged in as {username}"
|
|
114
157
|
recording: "Recording"
|
|
115
158
|
recording_compression: "Compression:"
|
|
116
159
|
tip_recording_compression: "Zlib level for .grec files.\n1 = fastest (default), 6+ has diminishing returns."
|
|
117
160
|
recordings_folder: "Recordings folder:"
|
|
118
161
|
open_recordings_folder: "Open Recordings Folder…"
|
|
162
|
+
open_replay_player: "Open Input Replay…"
|
|
119
163
|
gp_a: "A"
|
|
120
164
|
gp_b: "B"
|
|
121
165
|
gp_l: "L"
|
|
@@ -134,6 +178,13 @@ picker:
|
|
|
134
178
|
slot: "Slot {n}"
|
|
135
179
|
close: "Close"
|
|
136
180
|
|
|
181
|
+
game_picker:
|
|
182
|
+
menu:
|
|
183
|
+
play: "Play"
|
|
184
|
+
quick_load: "Quick Load"
|
|
185
|
+
set_boxart: "Set Boxart"
|
|
186
|
+
remove: "Remove from Library"
|
|
187
|
+
|
|
137
188
|
rom_info:
|
|
138
189
|
title: "ROM Info"
|
|
139
190
|
field_title: "Title:"
|
|
@@ -148,6 +199,29 @@ rom_info:
|
|
|
148
199
|
close: "Close"
|
|
149
200
|
na: "N/A"
|
|
150
201
|
|
|
202
|
+
replay:
|
|
203
|
+
open_recording: "Open Input Recording…"
|
|
204
|
+
replay_player: "Input Replay"
|
|
205
|
+
ended: "Replay complete ({frames} frames)"
|
|
206
|
+
empty_hint: "Open Input Recording (Cmd+O)"
|
|
207
|
+
|
|
208
|
+
patcher:
|
|
209
|
+
title: "Patch ROM"
|
|
210
|
+
rom_label: "ROM file:"
|
|
211
|
+
patch_label: "Patch file:"
|
|
212
|
+
outdir_label: "Output dir:"
|
|
213
|
+
browse: "Browse…"
|
|
214
|
+
apply: "Apply Patch"
|
|
215
|
+
working: "Applying patch…"
|
|
216
|
+
done: "Done →"
|
|
217
|
+
err_missing_fields: "Please fill in all fields."
|
|
218
|
+
err_rom_not_found: "ROM file not found."
|
|
219
|
+
err_patch_not_found: "Patch file not found."
|
|
220
|
+
err_failed: "Patch failed:"
|
|
221
|
+
overwrite_title: "File Exists"
|
|
222
|
+
overwrite_msg: "{path} already exists. Overwrite it?"
|
|
223
|
+
thread_mode_warn: "Note: Progress may appear stuck on Ruby < 4"
|
|
224
|
+
|
|
151
225
|
player:
|
|
152
226
|
open_rom_hint: "File > Open ROM…"
|
|
153
227
|
fps: "{fps} fps"
|
|
@@ -155,3 +229,21 @@ player:
|
|
|
155
229
|
ff_max: ">> MAX"
|
|
156
230
|
none: "(none)"
|
|
157
231
|
clear: "Clear"
|
|
232
|
+
|
|
233
|
+
achievements:
|
|
234
|
+
title: "Achievements"
|
|
235
|
+
game_label: "Game:"
|
|
236
|
+
sync: "Sync"
|
|
237
|
+
name_col: "Achievement"
|
|
238
|
+
points_col: "Points"
|
|
239
|
+
earned_col: "Earned"
|
|
240
|
+
none: "No achievements loaded"
|
|
241
|
+
earned_label: "{earned} / {total} earned"
|
|
242
|
+
sync_pending: "Syncing…"
|
|
243
|
+
sync_failed: "Sync failed"
|
|
244
|
+
sync_no_game: "No game loaded"
|
|
245
|
+
sync_timeout: "Sync timed out"
|
|
246
|
+
not_logged_in: "Not logged in"
|
|
247
|
+
include_unofficial: "Include Unofficial"
|
|
248
|
+
bulk_syncing: "Syncing {title} ({n}/{total})…"
|
|
249
|
+
bulk_sync_done: "Synced {count} games"
|