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/bios.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Immutable value object representing a GBA BIOS file.
|
|
5
|
+
# The stored config value is just the filename; this object resolves
|
|
6
|
+
# it to a full path and computes metadata on demand (memoized).
|
|
7
|
+
class Bios
|
|
8
|
+
EXPECTED_SIZE = 16_384
|
|
9
|
+
|
|
10
|
+
attr_reader :path
|
|
11
|
+
|
|
12
|
+
def initialize(path:)
|
|
13
|
+
@path = path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Build a Bios from a bare filename stored in config.
|
|
17
|
+
def self.from_config_name(name)
|
|
18
|
+
return nil if name.nil? || name.empty?
|
|
19
|
+
new(path: File.join(Config.bios_dir, name))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def filename = File.basename(@path)
|
|
23
|
+
def exists? = File.exist?(@path)
|
|
24
|
+
|
|
25
|
+
def size
|
|
26
|
+
@size ||= exists? ? File.size(@path) : 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def valid?
|
|
30
|
+
exists? && size == EXPECTED_SIZE
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def checksum
|
|
34
|
+
return @checksum if defined?(@checksum)
|
|
35
|
+
@checksum = valid? ? Gemba.gba_bios_checksum(File.binread(@path)) : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def official? = checksum == GBA_BIOS_CHECKSUM
|
|
39
|
+
def ds_mode? = checksum == GBA_DS_BIOS_CHECKSUM
|
|
40
|
+
def known? = official? || ds_mode?
|
|
41
|
+
|
|
42
|
+
def label
|
|
43
|
+
return "Official GBA BIOS" if official?
|
|
44
|
+
return "NDS GBA Mode BIOS" if ds_mode?
|
|
45
|
+
"Unknown BIOS"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def status_text
|
|
49
|
+
return "File not found (#{@path})" unless exists?
|
|
50
|
+
return "Invalid size (#{size} bytes, expected #{EXPECTED_SIZE})" unless valid?
|
|
51
|
+
"#{label} · #{size} bytes"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Gemba
|
|
6
|
+
class BoxartFetcher
|
|
7
|
+
# Resolves box art URLs from the LibRetro thumbnails CDN.
|
|
8
|
+
#
|
|
9
|
+
# URL pattern:
|
|
10
|
+
# https://thumbnails.libretro.com/{system}/Named_Boxarts/{encoded_name}.png
|
|
11
|
+
#
|
|
12
|
+
# Requires game_code → canonical name mapping via GameIndex.
|
|
13
|
+
class LibretroBackend
|
|
14
|
+
SYSTEMS = {
|
|
15
|
+
"AGB" => "Nintendo - Game Boy Advance",
|
|
16
|
+
"CGB" => "Nintendo - Game Boy Color",
|
|
17
|
+
"DMG" => "Nintendo - Game Boy",
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
BASE_URL = "https://thumbnails.libretro.com"
|
|
21
|
+
|
|
22
|
+
# @param game_code [String] e.g. "AGB-BPEE"
|
|
23
|
+
# @return [String, nil] full URL to the box art PNG, or nil if unknown
|
|
24
|
+
def url_for(game_code)
|
|
25
|
+
platform = game_code.split("-", 2).first
|
|
26
|
+
system = SYSTEMS[platform]
|
|
27
|
+
return nil unless system
|
|
28
|
+
|
|
29
|
+
name = GameIndex.lookup(game_code)
|
|
30
|
+
return nil unless name
|
|
31
|
+
|
|
32
|
+
encoded_system = URI.encode_www_form_component(system).gsub("+", "%20")
|
|
33
|
+
encoded_name = URI.encode_www_form_component(name).gsub("+", "%20")
|
|
34
|
+
|
|
35
|
+
"#{BASE_URL}/#{encoded_system}/Named_Boxarts/#{encoded_name}.png"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Gemba
|
|
7
|
+
# Fetches and caches box art images for ROMs.
|
|
8
|
+
#
|
|
9
|
+
# Delegates URL resolution to a pluggable backend (anything responding to
|
|
10
|
+
# +#url_for(game_code)+). Downloads happen off the main thread via
|
|
11
|
+
# +Teek::BackgroundWork+ so the UI stays responsive.
|
|
12
|
+
#
|
|
13
|
+
# Cache layout:
|
|
14
|
+
# {cache_dir}/{game_code}/boxart.png
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# fetcher = BoxartFetcher.new(app: app, cache_dir: Config.boxart_dir, backend: backend)
|
|
18
|
+
# fetcher.fetch("AGB-BPEE") { |path| update_card_image(path) }
|
|
19
|
+
#
|
|
20
|
+
class BoxartFetcher
|
|
21
|
+
attr_reader :cache_dir
|
|
22
|
+
|
|
23
|
+
def initialize(app:, cache_dir:, backend:)
|
|
24
|
+
@app = app
|
|
25
|
+
@cache_dir = cache_dir
|
|
26
|
+
@backend = backend
|
|
27
|
+
@in_flight = {} # game_code => true, prevents duplicate fetches
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Fetch box art for a game code. If cached, yields the path immediately.
|
|
31
|
+
# Otherwise kicks off an async download and yields the path on completion.
|
|
32
|
+
#
|
|
33
|
+
# @param game_code [String] e.g. "AGB-BPEE"
|
|
34
|
+
# @yield [path] called on the main thread with the cached file path
|
|
35
|
+
# @yieldparam path [String] absolute path to the cached PNG
|
|
36
|
+
def fetch(game_code, &on_fetched)
|
|
37
|
+
return unless on_fetched
|
|
38
|
+
|
|
39
|
+
cached = cached_path(game_code)
|
|
40
|
+
if File.exist?(cached)
|
|
41
|
+
on_fetched.call(cached)
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
url = @backend.url_for(game_code)
|
|
46
|
+
return unless url
|
|
47
|
+
return if @in_flight[game_code]
|
|
48
|
+
|
|
49
|
+
@in_flight[game_code] = true
|
|
50
|
+
|
|
51
|
+
Teek::BackgroundWork.new(@app, { url: url, dest: cached, game_code: game_code }, mode: :thread) do |t, data|
|
|
52
|
+
uri = URI(data[:url])
|
|
53
|
+
response = Net::HTTP.get_response(uri)
|
|
54
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
55
|
+
FileUtils.mkdir_p(File.dirname(data[:dest]))
|
|
56
|
+
File.binwrite(data[:dest], response.body)
|
|
57
|
+
t.yield(data[:dest])
|
|
58
|
+
else
|
|
59
|
+
t.yield(nil)
|
|
60
|
+
end
|
|
61
|
+
end.on_progress do |path|
|
|
62
|
+
@in_flight.delete(game_code)
|
|
63
|
+
on_fetched.call(path) if path
|
|
64
|
+
end.on_done do
|
|
65
|
+
@in_flight.delete(game_code)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [String] path where box art would be cached for this game code
|
|
70
|
+
def cached_path(game_code)
|
|
71
|
+
File.join(@cache_dir, game_code, "boxart.png")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [Boolean] whether box art is already cached
|
|
75
|
+
def cached?(game_code)
|
|
76
|
+
File.exist?(cached_path(game_code))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Include in any class that emits events via Gemba.bus.
|
|
5
|
+
# No constructor changes needed — just include and call emit.
|
|
6
|
+
module BusEmitter
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def emit(event, *args, **kwargs)
|
|
10
|
+
Gemba.bus.emit(event, *args, **kwargs)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/gemba/child_window.rb
CHANGED
|
@@ -27,7 +27,10 @@ module Gemba
|
|
|
27
27
|
parent_menu = @app.command('.', :cget, '-menu') rescue nil
|
|
28
28
|
@app.command(top, :configure, menu: parent_menu) if parent_menu && !parent_menu.empty?
|
|
29
29
|
end
|
|
30
|
-
@
|
|
30
|
+
@on_dismiss = @callbacks[:on_dismiss] if defined?(@callbacks)
|
|
31
|
+
@app.command(:wm, 'protocol', top, 'WM_DELETE_WINDOW', proc {
|
|
32
|
+
@on_dismiss ? @on_dismiss.call : hide
|
|
33
|
+
})
|
|
31
34
|
yield if block_given?
|
|
32
35
|
@app.command(:wm, 'withdraw', top)
|
|
33
36
|
end
|
|
@@ -58,5 +61,25 @@ module Gemba
|
|
|
58
61
|
@app.command(:wm, 'withdraw', top)
|
|
59
62
|
@callbacks[:on_close]&.call if defined?(@callbacks)
|
|
60
63
|
end
|
|
64
|
+
|
|
65
|
+
# ── ModalStack protocol ──────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
# Show the window for ModalStack (deiconify, grab, position).
|
|
68
|
+
# Override in subclasses to accept additional keyword arguments.
|
|
69
|
+
def show_modal(**_args)
|
|
70
|
+
top = self.class::TOP
|
|
71
|
+
position_near_parent
|
|
72
|
+
@app.command(:wm, 'deiconify', top)
|
|
73
|
+
@app.command(:raise, top)
|
|
74
|
+
@app.command(:grab, :set, top)
|
|
75
|
+
@app.command(:focus, top)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Withdraw the window for ModalStack (release grab, withdraw — NO callback).
|
|
79
|
+
def withdraw
|
|
80
|
+
top = self.class::TOP
|
|
81
|
+
@app.command(:grab, :release, top)
|
|
82
|
+
@app.command(:wm, 'withdraw', top)
|
|
83
|
+
end
|
|
61
84
|
end
|
|
62
85
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Gemba
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class ConfigCmd
|
|
9
|
+
def initialize(argv, dry_run: false)
|
|
10
|
+
@argv = argv
|
|
11
|
+
@dry_run = dry_run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
options = parse
|
|
16
|
+
|
|
17
|
+
if options[:help]
|
|
18
|
+
puts options[:parser] unless @dry_run
|
|
19
|
+
return { command: :config, help: true }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
result = {
|
|
23
|
+
command: options[:reset] ? :config_reset : :config_show,
|
|
24
|
+
reset: options[:reset],
|
|
25
|
+
yes: options[:yes],
|
|
26
|
+
options: options.except(:parser)
|
|
27
|
+
}
|
|
28
|
+
return result if @dry_run
|
|
29
|
+
|
|
30
|
+
require "gemba"
|
|
31
|
+
|
|
32
|
+
if options[:reset]
|
|
33
|
+
path = Config.default_path
|
|
34
|
+
unless File.exist?(path)
|
|
35
|
+
puts "No config file found at #{path}"
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
unless options[:yes]
|
|
39
|
+
print "Delete #{path}? [y/N] "
|
|
40
|
+
return unless $stdin.gets&.strip&.downcase == 'y'
|
|
41
|
+
end
|
|
42
|
+
Config.reset!(path: path)
|
|
43
|
+
puts "Deleted #{path}"
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
path = Config.default_path
|
|
48
|
+
puts "Config: #{path}"
|
|
49
|
+
puts " Exists: #{File.exist?(path)}"
|
|
50
|
+
if File.exist?(path)
|
|
51
|
+
config = Gemba.user_config
|
|
52
|
+
puts " Scale: #{config.scale}"
|
|
53
|
+
puts " Volume: #{config.volume}"
|
|
54
|
+
puts " Muted: #{config.muted?}"
|
|
55
|
+
puts " Locale: #{config.locale}"
|
|
56
|
+
puts " Show FPS: #{config.show_fps?}"
|
|
57
|
+
puts " Turbo speed: #{config.turbo_speed}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse
|
|
62
|
+
options = {}
|
|
63
|
+
argv = @argv.dup
|
|
64
|
+
|
|
65
|
+
parser = OptionParser.new do |o|
|
|
66
|
+
o.banner = "Usage: gemba config [options]"
|
|
67
|
+
o.separator ""
|
|
68
|
+
o.separator "Show or reset configuration."
|
|
69
|
+
o.separator ""
|
|
70
|
+
|
|
71
|
+
o.on("--reset", "Delete settings file (keeps saves)") { options[:reset] = true }
|
|
72
|
+
o.on("-y", "--yes", "Skip confirmation prompts") { options[:yes] = true }
|
|
73
|
+
o.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
parser.parse!(argv)
|
|
77
|
+
options[:parser] = parser
|
|
78
|
+
options
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Gemba
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class Decode
|
|
9
|
+
def initialize(argv, dry_run: false)
|
|
10
|
+
@argv = argv
|
|
11
|
+
@dry_run = dry_run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
options = parse
|
|
16
|
+
|
|
17
|
+
if options[:help]
|
|
18
|
+
puts options[:parser] unless @dry_run
|
|
19
|
+
return { command: :decode, help: true }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if options[:list]
|
|
23
|
+
list_grec_recordings unless @dry_run
|
|
24
|
+
return { command: :decode_list }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
unless options[:grec]
|
|
28
|
+
list_grec_recordings unless @dry_run
|
|
29
|
+
return { command: :decode_list }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result = {
|
|
33
|
+
command: options[:stats] ? :decode_stats : :decode,
|
|
34
|
+
grec: options[:grec],
|
|
35
|
+
stats: options[:stats],
|
|
36
|
+
output: options[:output],
|
|
37
|
+
video_codec: options[:video_codec],
|
|
38
|
+
audio_codec: options[:audio_codec],
|
|
39
|
+
scale: options[:scale],
|
|
40
|
+
ffmpeg_args: options[:ffmpeg_args],
|
|
41
|
+
options: options.except(:parser)
|
|
42
|
+
}
|
|
43
|
+
return result if @dry_run
|
|
44
|
+
|
|
45
|
+
require "gemba/headless"
|
|
46
|
+
|
|
47
|
+
grec_path = options[:grec]
|
|
48
|
+
|
|
49
|
+
if options[:stats]
|
|
50
|
+
info = RecorderDecoder.stats(grec_path)
|
|
51
|
+
puts "Recording: #{grec_path}"
|
|
52
|
+
puts " Frames: #{info[:frame_count]}"
|
|
53
|
+
puts " Resolution: #{info[:width]}x#{info[:height]}"
|
|
54
|
+
puts " FPS: #{'%.2f' % info[:fps]}"
|
|
55
|
+
puts " Duration: #{'%.1f' % info[:duration]}s"
|
|
56
|
+
puts " Avg change: #{'%.1f' % info[:avg_change_pct]}%/frame"
|
|
57
|
+
puts " Uncompressed: #{format_size(info[:raw_video_size])} (encode input)"
|
|
58
|
+
puts " Audio: #{info[:audio_rate]} Hz, #{info[:audio_channels]}ch"
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
output_path = options[:output] || grec_path.sub(/\.grec\z/, '') + '.mp4'
|
|
63
|
+
codec_opts = {}
|
|
64
|
+
codec_opts[:video_codec] = options[:video_codec] if options[:video_codec]
|
|
65
|
+
codec_opts[:audio_codec] = options[:audio_codec] if options[:audio_codec]
|
|
66
|
+
codec_opts[:scale] = options[:scale] if options[:scale]
|
|
67
|
+
codec_opts[:ffmpeg_args] = options[:ffmpeg_args] if options[:ffmpeg_args]
|
|
68
|
+
codec_opts[:progress] = options.fetch(:progress, true)
|
|
69
|
+
|
|
70
|
+
info = RecorderDecoder.decode(grec_path, output_path, **codec_opts)
|
|
71
|
+
puts "Encoded #{info[:frame_count]} frames " \
|
|
72
|
+
"(#{info[:width]}x#{info[:height]} @ #{'%.2f' % info[:fps]} fps, " \
|
|
73
|
+
"avg #{'%.1f' % info[:avg_change_pct]}% change/frame)"
|
|
74
|
+
puts "Output: #{info[:output_path]}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse
|
|
78
|
+
options = {}
|
|
79
|
+
argv = @argv.dup
|
|
80
|
+
|
|
81
|
+
parser = OptionParser.new do |o|
|
|
82
|
+
o.banner = "Usage: gemba decode [options] GREC_FILE [-- FFMPEG_ARGS...]"
|
|
83
|
+
o.separator ""
|
|
84
|
+
o.separator "Encode a .grec recording to a playable video via ffmpeg."
|
|
85
|
+
o.separator "Args after -- replace the default codec flags."
|
|
86
|
+
o.separator ""
|
|
87
|
+
|
|
88
|
+
o.on("-o", "--output PATH", "Output path (default: INPUT.mp4)") { |v| options[:output] = v }
|
|
89
|
+
o.on("--video-codec CODEC", "Video codec (default: libx264)") { |v| options[:video_codec] = v }
|
|
90
|
+
o.on("--audio-codec CODEC", "Audio codec (default: aac)") { |v| options[:audio_codec] = v }
|
|
91
|
+
o.on("-s", "--scale N", Integer, "Scale factor (default: native)") { |v| options[:scale] = v.clamp(1, 10) }
|
|
92
|
+
o.on("-l", "--list", "List available .grec recordings") { options[:list] = true }
|
|
93
|
+
o.on("--stats", "Show recording stats (no ffmpeg needed)") { options[:stats] = true }
|
|
94
|
+
o.on("--no-progress", "Disable progress indicator") { options[:progress] = false }
|
|
95
|
+
o.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
parser.parse!(argv)
|
|
99
|
+
options[:grec] = argv.shift
|
|
100
|
+
options[:ffmpeg_args] = argv unless argv.empty?
|
|
101
|
+
options[:parser] = parser
|
|
102
|
+
options
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def list_grec_recordings
|
|
108
|
+
require "gemba/headless"
|
|
109
|
+
|
|
110
|
+
dir = Config.default_recordings_dir
|
|
111
|
+
unless File.directory?(dir)
|
|
112
|
+
puts "No recordings directory found at #{dir}"
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
grec_files = Dir.glob(File.join(dir, '*.grec')).sort
|
|
117
|
+
if grec_files.empty?
|
|
118
|
+
puts "No .grec recordings in #{dir}"
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
entries = grec_files.map do |path|
|
|
123
|
+
info = RecorderDecoder.stats(path)
|
|
124
|
+
{
|
|
125
|
+
path: path,
|
|
126
|
+
frames: "#{info[:frame_count]} frames",
|
|
127
|
+
duration: "#{'%.1f' % info[:duration]}s",
|
|
128
|
+
size: format_size(File.size(path))
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
path_w = entries.map { |e| e[:path].length }.max
|
|
133
|
+
frames_w = entries.map { |e| e[:frames].length }.max
|
|
134
|
+
dur_w = entries.map { |e| e[:duration].length }.max
|
|
135
|
+
size_w = entries.map { |e| e[:size].length }.max
|
|
136
|
+
|
|
137
|
+
entries.each do |e|
|
|
138
|
+
puts "#{e[:path].ljust(path_w)} #{e[:frames].rjust(frames_w)} #{e[:duration].rjust(dur_w)} #{e[:size].rjust(size_w)}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def format_size(bytes)
|
|
143
|
+
if bytes >= 1_073_741_824
|
|
144
|
+
"#{'%.1f' % (bytes / 1_073_741_824.0)} GB"
|
|
145
|
+
elsif bytes >= 1_048_576
|
|
146
|
+
"#{'%.1f' % (bytes / 1_048_576.0)} MB"
|
|
147
|
+
else
|
|
148
|
+
"#{'%.1f' % (bytes / 1024.0)} KB"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Gemba
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class Patch
|
|
9
|
+
def initialize(argv, dry_run: false)
|
|
10
|
+
@argv = argv
|
|
11
|
+
@dry_run = dry_run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
options = parse
|
|
16
|
+
|
|
17
|
+
if options[:help]
|
|
18
|
+
puts options[:parser] unless @dry_run
|
|
19
|
+
return { command: :patch, help: true }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless options[:rom] && options[:patch]
|
|
23
|
+
$stderr.puts "gemba patch: ROM_FILE and PATCH_FILE are required"
|
|
24
|
+
$stderr.puts options[:parser]
|
|
25
|
+
return { command: :patch, error: :missing_args }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
rom_path = options[:rom]
|
|
29
|
+
patch_path = options[:patch]
|
|
30
|
+
out_path = if options[:output]
|
|
31
|
+
options[:output]
|
|
32
|
+
else
|
|
33
|
+
ext = File.extname(rom_path)
|
|
34
|
+
base = rom_path.chomp(ext)
|
|
35
|
+
"#{base}-patched#{ext}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result = { command: :patch, rom: rom_path, patch: patch_path, out: out_path }
|
|
39
|
+
return result if @dry_run
|
|
40
|
+
|
|
41
|
+
require "gemba/rom_patcher"
|
|
42
|
+
require "gemba/rom_patcher/ips"
|
|
43
|
+
require "gemba/rom_patcher/bps"
|
|
44
|
+
require "gemba/rom_patcher/ups"
|
|
45
|
+
|
|
46
|
+
safe_out = RomPatcher.safe_out_path(out_path)
|
|
47
|
+
puts "Patching #{File.basename(rom_path)} with #{File.basename(patch_path)}…"
|
|
48
|
+
RomPatcher.patch(rom_path: rom_path, patch_path: patch_path, out_path: safe_out)
|
|
49
|
+
puts "Written: #{safe_out}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse
|
|
53
|
+
options = {}
|
|
54
|
+
argv = @argv.dup
|
|
55
|
+
|
|
56
|
+
parser = OptionParser.new do |o|
|
|
57
|
+
o.banner = "Usage: gemba patch [options] ROM_FILE PATCH_FILE"
|
|
58
|
+
o.separator ""
|
|
59
|
+
o.separator "Apply an IPS, BPS, or UPS patch to a ROM file."
|
|
60
|
+
o.separator ""
|
|
61
|
+
o.separator "The output file is written to --output or, by default, next to the ROM."
|
|
62
|
+
o.separator "If the output path already exists, -(2), -(3) etc. are appended."
|
|
63
|
+
o.separator ""
|
|
64
|
+
|
|
65
|
+
o.on("-o", "--output PATH", "Output ROM path") { |v| options[:output] = File.expand_path(v) }
|
|
66
|
+
o.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
parser.parse!(argv)
|
|
70
|
+
options[:rom] = File.expand_path(argv[0]) if argv[0]
|
|
71
|
+
options[:patch] = File.expand_path(argv[1]) if argv[1]
|
|
72
|
+
options[:parser] = parser
|
|
73
|
+
options
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Gemba
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class Play
|
|
9
|
+
def initialize(argv, dry_run: false)
|
|
10
|
+
@argv = argv
|
|
11
|
+
@dry_run = dry_run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
options = parse
|
|
16
|
+
|
|
17
|
+
if options[:help]
|
|
18
|
+
puts options[:parser] unless @dry_run
|
|
19
|
+
return { command: :play, help: true }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
result = {
|
|
23
|
+
command: :play,
|
|
24
|
+
rom: options[:rom],
|
|
25
|
+
sound: options.fetch(:sound, true),
|
|
26
|
+
fullscreen: options[:fullscreen],
|
|
27
|
+
options: options.except(:parser)
|
|
28
|
+
}
|
|
29
|
+
return result if @dry_run
|
|
30
|
+
|
|
31
|
+
require "gemba"
|
|
32
|
+
|
|
33
|
+
apply(Gemba.user_config, options)
|
|
34
|
+
Gemba.load_locale if options[:locale]
|
|
35
|
+
Gemba::AppController.new(result[:rom], sound: result[:sound], fullscreen: result[:fullscreen]).run
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse
|
|
39
|
+
options = {}
|
|
40
|
+
argv = @argv.dup
|
|
41
|
+
|
|
42
|
+
parser = OptionParser.new do |o|
|
|
43
|
+
o.banner = "Usage: gemba [play] [options] [ROM_FILE]"
|
|
44
|
+
o.separator ""
|
|
45
|
+
o.separator "Launch the GBA emulator. ROM_FILE is optional."
|
|
46
|
+
o.separator ""
|
|
47
|
+
|
|
48
|
+
o.on("-s", "--scale N", Integer, "Window scale (1-4)") { |v| options[:scale] = v.clamp(1, 4) }
|
|
49
|
+
o.on("-v", "--volume N", Integer, "Volume (0-100)") { |v| options[:volume] = v.clamp(0, 100) }
|
|
50
|
+
o.on("-m", "--mute", "Start muted") { options[:mute] = true }
|
|
51
|
+
o.on("--no-sound", "Disable audio entirely") { options[:sound] = false }
|
|
52
|
+
o.on("-f", "--fullscreen", "Start in fullscreen") { options[:fullscreen] = true }
|
|
53
|
+
o.on("--show-fps", "Show FPS counter") { options[:show_fps] = true }
|
|
54
|
+
o.on("--turbo-speed N", Integer, "Fast-forward speed (0=uncapped, 2-4)") { |v| options[:turbo_speed] = v.clamp(0, 4) }
|
|
55
|
+
o.on("--bios PATH", "Path to GBA BIOS file (overrides saved setting)") { |v| options[:bios] = File.expand_path(v) }
|
|
56
|
+
o.on("--locale LANG", "Language (en, ja, auto)") { |v| options[:locale] = v }
|
|
57
|
+
o.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
parser.parse!(argv)
|
|
61
|
+
options[:rom] = File.expand_path(argv.first) if argv.first
|
|
62
|
+
options[:parser] = parser
|
|
63
|
+
options
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply(config, options)
|
|
67
|
+
config.scale = options[:scale] if options[:scale]
|
|
68
|
+
config.volume = options[:volume] if options[:volume]
|
|
69
|
+
config.muted = true if options[:mute]
|
|
70
|
+
config.show_fps = true if options[:show_fps]
|
|
71
|
+
config.turbo_speed = options[:turbo_speed] if options[:turbo_speed]
|
|
72
|
+
config.locale = options[:locale] if options[:locale]
|
|
73
|
+
config.bios_path = options[:bios] if options[:bios]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|