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
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
|
|
7
|
+
SUBCOMMANDS = %w[play record decode replay config version patch ra].freeze
|
|
9
8
|
|
|
10
|
-
# Entry point: dispatch to subcommand or
|
|
9
|
+
# Entry point: dispatch to subcommand or default to play.
|
|
11
10
|
# @param argv [Array<String>]
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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)"}
|