gemba 0.1.1 → 0.2.1
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 +190 -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 +511 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +28 -0
- data/lib/gemba/achievements/retro_achievements/unlock_retry_worker.rb +35 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1036 -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 +154 -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 +1084 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +70 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +309 -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/list_picker_frame.rb +271 -0
- data/lib/gemba/locales/en.yml +109 -5
- data/lib/gemba/locales/ja.yml +109 -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 +119 -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 +377 -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 +92 -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 +78 -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 +3 -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_list_picker_frame.rb +391 -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_ra_backend_unlock_retry.rb +123 -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 +383 -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 +221 -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 +277 -10
- data/lib/gemba/input_mappings.rb +0 -214
- data/lib/gemba/player.rb +0 -1525
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "shared/tk_test_helper"
|
|
5
|
+
|
|
6
|
+
class TestReplayPlayer < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
PONG_ROM = File.expand_path("fixtures/pong.gba", __dir__)
|
|
10
|
+
|
|
11
|
+
# Generate a short .gir fixture for all GUI tests.
|
|
12
|
+
# Uses HeadlessPlayer + InputRecorder to record 60 frames of pong.
|
|
13
|
+
def self.gir_fixture_dir
|
|
14
|
+
@gir_fixture_dir ||= begin
|
|
15
|
+
require "tmpdir"
|
|
16
|
+
dir = Dir.mktmpdir("gemba_replay_test")
|
|
17
|
+
at_exit { FileUtils.rm_rf(dir) }
|
|
18
|
+
|
|
19
|
+
require "gemba/headless"
|
|
20
|
+
require "gemba/headless"
|
|
21
|
+
|
|
22
|
+
gir_path = File.join(dir, "pong_test.gir")
|
|
23
|
+
Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
|
|
24
|
+
player.step(10)
|
|
25
|
+
core = player.core
|
|
26
|
+
rec = Gemba::InputRecorder.new(gir_path, core: core, rom_path: PONG_ROM)
|
|
27
|
+
rec.start
|
|
28
|
+
60.times do |i|
|
|
29
|
+
mask = i < 30 ? Gemba::KEY_START : 0
|
|
30
|
+
rec.capture(mask)
|
|
31
|
+
core.set_keys(mask)
|
|
32
|
+
core.run_frame
|
|
33
|
+
end
|
|
34
|
+
rec.stop
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
dir
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def gir_path
|
|
42
|
+
File.join(self.class.gir_fixture_dir, "pong_test.gir")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ReplayPlayer opens a window, plays frames, then exits cleanly.
|
|
46
|
+
def test_replay_exits_cleanly
|
|
47
|
+
code = <<~RUBY
|
|
48
|
+
require "gemba"
|
|
49
|
+
require "support/player_helpers"
|
|
50
|
+
|
|
51
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
52
|
+
app = rp.app
|
|
53
|
+
|
|
54
|
+
poll_until_ready(rp) { rp.running = false }
|
|
55
|
+
|
|
56
|
+
rp.run
|
|
57
|
+
RUBY
|
|
58
|
+
|
|
59
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
60
|
+
|
|
61
|
+
output = []
|
|
62
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
63
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
64
|
+
|
|
65
|
+
assert success, "ReplayPlayer should exit cleanly\n#{output.join("\n")}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# After replay ends (60 frames), player should pause on last frame.
|
|
69
|
+
def test_replay_pauses_on_end
|
|
70
|
+
code = <<~RUBY
|
|
71
|
+
require "gemba"
|
|
72
|
+
require "support/player_helpers"
|
|
73
|
+
|
|
74
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
75
|
+
app = rp.app
|
|
76
|
+
|
|
77
|
+
poll_until_ready(rp) do
|
|
78
|
+
check = proc do
|
|
79
|
+
if rp.replay_ended?
|
|
80
|
+
unless rp.paused?
|
|
81
|
+
$stderr.puts "FAIL: replay ended but not paused"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
rp.running = false
|
|
85
|
+
else
|
|
86
|
+
app.after(100, &check)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
app.after(100, &check)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
rp.run
|
|
93
|
+
RUBY
|
|
94
|
+
|
|
95
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
96
|
+
|
|
97
|
+
output = []
|
|
98
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
99
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
100
|
+
|
|
101
|
+
assert success, "ReplayPlayer should pause on replay end\n#{output.join("\n")}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Fullscreen toggle (F11 twice) should not hang.
|
|
105
|
+
def test_fullscreen_toggle
|
|
106
|
+
code = <<~RUBY
|
|
107
|
+
require "gemba"
|
|
108
|
+
require "support/player_helpers"
|
|
109
|
+
|
|
110
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
111
|
+
app = rp.app
|
|
112
|
+
|
|
113
|
+
poll_until_ready(rp) do
|
|
114
|
+
vp = rp.viewport
|
|
115
|
+
frame = vp.frame.path
|
|
116
|
+
|
|
117
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
118
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'F11')
|
|
119
|
+
app.update
|
|
120
|
+
|
|
121
|
+
app.after(50) do
|
|
122
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'F11')
|
|
123
|
+
app.update
|
|
124
|
+
app.after(50) { rp.running = false }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
rp.run
|
|
129
|
+
RUBY
|
|
130
|
+
|
|
131
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
132
|
+
|
|
133
|
+
output = []
|
|
134
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
135
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
136
|
+
|
|
137
|
+
assert success, "ReplayPlayer fullscreen toggle should not hang\n#{output.join("\n")}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Fast-forward toggle (Tab) should not hang.
|
|
141
|
+
def test_fast_forward_toggle
|
|
142
|
+
code = <<~RUBY
|
|
143
|
+
require "gemba"
|
|
144
|
+
require "support/player_helpers"
|
|
145
|
+
|
|
146
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
147
|
+
app = rp.app
|
|
148
|
+
|
|
149
|
+
poll_until_ready(rp) do
|
|
150
|
+
vp = rp.viewport
|
|
151
|
+
frame = vp.frame.path
|
|
152
|
+
|
|
153
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
154
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'Tab')
|
|
155
|
+
app.update
|
|
156
|
+
|
|
157
|
+
app.after(200) do
|
|
158
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
159
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'Tab')
|
|
160
|
+
app.update
|
|
161
|
+
app.after(50) { rp.running = false }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
rp.run
|
|
166
|
+
RUBY
|
|
167
|
+
|
|
168
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
169
|
+
|
|
170
|
+
output = []
|
|
171
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
172
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
173
|
+
|
|
174
|
+
assert success, "ReplayPlayer fast-forward toggle should not hang\n#{output.join("\n")}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Pause via public method, verify predicate.
|
|
178
|
+
def test_pause_via_method
|
|
179
|
+
code = <<~RUBY
|
|
180
|
+
require "gemba"
|
|
181
|
+
require "support/player_helpers"
|
|
182
|
+
|
|
183
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
184
|
+
app = rp.app
|
|
185
|
+
|
|
186
|
+
poll_until_ready(rp) do
|
|
187
|
+
app.after(100) do
|
|
188
|
+
rp.pause
|
|
189
|
+
unless rp.paused?
|
|
190
|
+
$stderr.puts "FAIL: pause method should set paused?"
|
|
191
|
+
exit 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
rp.resume
|
|
195
|
+
if rp.paused?
|
|
196
|
+
$stderr.puts "FAIL: resume should clear paused?"
|
|
197
|
+
exit 1
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
rp.running = false
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
rp.run
|
|
205
|
+
RUBY
|
|
206
|
+
|
|
207
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
208
|
+
|
|
209
|
+
output = []
|
|
210
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
211
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
212
|
+
|
|
213
|
+
assert success, "ReplayPlayer pause/resume methods should work\n#{output.join("\n")}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Pressing P should toggle pause via hotkey.
|
|
217
|
+
def test_pause_hotkey
|
|
218
|
+
code = <<~RUBY
|
|
219
|
+
require "gemba"
|
|
220
|
+
require "support/player_helpers"
|
|
221
|
+
|
|
222
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
223
|
+
app = rp.app
|
|
224
|
+
|
|
225
|
+
poll_until_ready(rp) do
|
|
226
|
+
vp = rp.viewport
|
|
227
|
+
frame = vp.frame.path
|
|
228
|
+
|
|
229
|
+
app.after(200) do
|
|
230
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
231
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'p')
|
|
232
|
+
app.update
|
|
233
|
+
|
|
234
|
+
app.after(200) do
|
|
235
|
+
unless rp.paused?
|
|
236
|
+
$stderr.puts "FAIL: P should pause"
|
|
237
|
+
exit 1
|
|
238
|
+
end
|
|
239
|
+
rp.running = false
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
rp.run
|
|
245
|
+
RUBY
|
|
246
|
+
|
|
247
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
248
|
+
|
|
249
|
+
output = []
|
|
250
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
251
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
252
|
+
|
|
253
|
+
assert success, "ReplayPlayer P hotkey should toggle pause\n#{output.join("\n")}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Escape should exit (when not fullscreen).
|
|
257
|
+
def test_escape_exits
|
|
258
|
+
code = <<~RUBY
|
|
259
|
+
require "gemba"
|
|
260
|
+
require "support/player_helpers"
|
|
261
|
+
|
|
262
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
263
|
+
app = rp.app
|
|
264
|
+
|
|
265
|
+
poll_until_ready(rp) do
|
|
266
|
+
vp = rp.viewport
|
|
267
|
+
frame = vp.frame.path
|
|
268
|
+
|
|
269
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
270
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'Escape')
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
rp.run
|
|
274
|
+
RUBY
|
|
275
|
+
|
|
276
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
277
|
+
|
|
278
|
+
output = []
|
|
279
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
280
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
281
|
+
|
|
282
|
+
assert success, "ReplayPlayer Escape should exit\n#{output.join("\n")}"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# frame_index should advance during replay.
|
|
286
|
+
def test_frame_index_advances
|
|
287
|
+
code = <<~RUBY
|
|
288
|
+
require "gemba"
|
|
289
|
+
require "support/player_helpers"
|
|
290
|
+
|
|
291
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
292
|
+
app = rp.app
|
|
293
|
+
|
|
294
|
+
poll_until_ready(rp) do
|
|
295
|
+
app.after(500) do
|
|
296
|
+
idx = rp.frame_index
|
|
297
|
+
if idx <= 0
|
|
298
|
+
$stderr.puts "FAIL: frame_index should advance, got \#{idx}"
|
|
299
|
+
exit 1
|
|
300
|
+
end
|
|
301
|
+
rp.running = false
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
rp.run
|
|
306
|
+
RUBY
|
|
307
|
+
|
|
308
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
309
|
+
|
|
310
|
+
output = []
|
|
311
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
312
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
313
|
+
|
|
314
|
+
assert success, "ReplayPlayer frame_index should advance\n#{output.join("\n")}"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "gemba/headless"
|
|
7
|
+
|
|
8
|
+
class TestRomInfo < Minitest::Test
|
|
9
|
+
# Stub that returns nil for every lookup — isolates RomInfo from real DAT data.
|
|
10
|
+
NULL_INDEX = Struct.new(:_) {
|
|
11
|
+
def lookup(_) = nil
|
|
12
|
+
def lookup_by_md5(*) = nil
|
|
13
|
+
}.new(nil)
|
|
14
|
+
|
|
15
|
+
ROM = {
|
|
16
|
+
'rom_id' => 'AGB_AXVE-DEADBEEF',
|
|
17
|
+
'title' => 'Pokemon Ruby',
|
|
18
|
+
'platform' => 'gba',
|
|
19
|
+
'game_code' => 'AGB-AXVE',
|
|
20
|
+
'path' => '/games/ruby.gba',
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def test_from_rom_sets_basic_fields
|
|
24
|
+
info = Gemba::RomInfo.from_rom(ROM, game_index: NULL_INDEX)
|
|
25
|
+
assert_equal 'AGB_AXVE-DEADBEEF', info.rom_id
|
|
26
|
+
assert_equal 'Pokemon Ruby', info.title
|
|
27
|
+
assert_equal 'GBA', info.platform
|
|
28
|
+
assert_equal 'AGB-AXVE', info.game_code
|
|
29
|
+
assert_equal '/games/ruby.gba', info.path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_platform_is_uppercased
|
|
33
|
+
info = Gemba::RomInfo.from_rom(ROM.merge('platform' => 'gbc'), game_index: NULL_INDEX)
|
|
34
|
+
assert_equal 'GBC', info.platform
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_title_falls_back_to_rom_id
|
|
38
|
+
rom = ROM.merge('title' => nil)
|
|
39
|
+
info = Gemba::RomInfo.from_rom(rom, game_index: NULL_INDEX)
|
|
40
|
+
assert_equal 'AGB_AXVE-DEADBEEF', info.title
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_title_from_game_index_wins_over_stored_title
|
|
44
|
+
index = Struct.new(:_) {
|
|
45
|
+
def lookup(_) = 'Index Title'
|
|
46
|
+
def lookup_by_md5(*) = nil
|
|
47
|
+
}.new(nil)
|
|
48
|
+
info = Gemba::RomInfo.from_rom(ROM, game_index: index)
|
|
49
|
+
assert_equal 'Index Title', info.title
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_no_fetcher_or_overrides_yields_nil_boxart_fields
|
|
53
|
+
info = Gemba::RomInfo.from_rom(ROM, game_index: NULL_INDEX)
|
|
54
|
+
assert_nil info.cached_boxart_path
|
|
55
|
+
assert_nil info.custom_boxart_path
|
|
56
|
+
assert_nil info.boxart_path
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_has_official_entry_true_when_index_returns_title
|
|
60
|
+
index = Struct.new(:_) {
|
|
61
|
+
def lookup(_) = 'Some Game'
|
|
62
|
+
def lookup_by_md5(*) = nil
|
|
63
|
+
}.new(nil)
|
|
64
|
+
info = Gemba::RomInfo.from_rom(ROM, game_index: index)
|
|
65
|
+
assert info.has_official_entry
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_has_official_entry_false_when_index_returns_nil
|
|
69
|
+
info = Gemba::RomInfo.from_rom(ROM, game_index: NULL_INDEX)
|
|
70
|
+
refute info.has_official_entry
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_has_official_entry_false_when_no_game_code
|
|
74
|
+
rom = ROM.merge('game_code' => nil)
|
|
75
|
+
info = Gemba::RomInfo.from_rom(rom, game_index: NULL_INDEX)
|
|
76
|
+
refute info.has_official_entry
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_boxart_path_returns_custom_when_file_exists
|
|
80
|
+
Dir.mktmpdir do |dir|
|
|
81
|
+
ENV['GEMBA_CONFIG_DIR'] = dir
|
|
82
|
+
custom = File.join(dir, "custom.png")
|
|
83
|
+
File.write(custom, "fake")
|
|
84
|
+
overrides = Gemba::RomOverrides.new(File.join(dir, "overrides.json"))
|
|
85
|
+
overrides.set_custom_boxart('AGB_AXVE-DEADBEEF', custom)
|
|
86
|
+
|
|
87
|
+
info = Gemba::RomInfo.from_rom(ROM, overrides: overrides, game_index: NULL_INDEX)
|
|
88
|
+
assert_equal File.join(dir, 'boxart', 'AGB_AXVE-DEADBEEF', 'custom.png'), info.boxart_path
|
|
89
|
+
ensure
|
|
90
|
+
ENV.delete('GEMBA_CONFIG_DIR')
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_boxart_path_falls_back_to_cache_when_no_custom
|
|
95
|
+
Dir.mktmpdir do |dir|
|
|
96
|
+
ENV['GEMBA_CONFIG_DIR'] = dir
|
|
97
|
+
cache_dir = File.join(dir, "boxart")
|
|
98
|
+
fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: cache_dir,
|
|
99
|
+
backend: Gemba::BoxartFetcher::NullBackend.new)
|
|
100
|
+
overrides = Gemba::RomOverrides.new(File.join(dir, "overrides.json"))
|
|
101
|
+
|
|
102
|
+
cached = fetcher.cached_path('AGB-AXVE')
|
|
103
|
+
FileUtils.mkdir_p(File.dirname(cached))
|
|
104
|
+
File.write(cached, "fake")
|
|
105
|
+
|
|
106
|
+
info = Gemba::RomInfo.from_rom(ROM, fetcher: fetcher, overrides: overrides, game_index: NULL_INDEX)
|
|
107
|
+
assert_equal cached, info.boxart_path
|
|
108
|
+
ensure
|
|
109
|
+
ENV.delete('GEMBA_CONFIG_DIR')
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_boxart_path_nil_when_neither_present
|
|
114
|
+
Dir.mktmpdir do |dir|
|
|
115
|
+
ENV['GEMBA_CONFIG_DIR'] = dir
|
|
116
|
+
fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: File.join(dir, "boxart"),
|
|
117
|
+
backend: Gemba::BoxartFetcher::NullBackend.new)
|
|
118
|
+
overrides = Gemba::RomOverrides.new(File.join(dir, "overrides.json"))
|
|
119
|
+
|
|
120
|
+
info = Gemba::RomInfo.from_rom(ROM, fetcher: fetcher, overrides: overrides, game_index: NULL_INDEX)
|
|
121
|
+
assert_nil info.boxart_path
|
|
122
|
+
ensure
|
|
123
|
+
ENV.delete('GEMBA_CONFIG_DIR')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_custom_beats_cache_in_boxart_path
|
|
128
|
+
Dir.mktmpdir do |dir|
|
|
129
|
+
ENV['GEMBA_CONFIG_DIR'] = dir
|
|
130
|
+
cache_dir = File.join(dir, "boxart")
|
|
131
|
+
fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: cache_dir,
|
|
132
|
+
backend: Gemba::BoxartFetcher::NullBackend.new)
|
|
133
|
+
overrides = Gemba::RomOverrides.new(File.join(dir, "overrides.json"))
|
|
134
|
+
|
|
135
|
+
cached = fetcher.cached_path('AGB-AXVE')
|
|
136
|
+
FileUtils.mkdir_p(File.dirname(cached))
|
|
137
|
+
File.write(cached, "cached")
|
|
138
|
+
|
|
139
|
+
src = File.join(dir, "my_cover.png")
|
|
140
|
+
File.write(src, "custom")
|
|
141
|
+
overrides.set_custom_boxart('AGB_AXVE-DEADBEEF', src)
|
|
142
|
+
|
|
143
|
+
info = Gemba::RomInfo.from_rom(ROM, fetcher: fetcher, overrides: overrides, game_index: NULL_INDEX)
|
|
144
|
+
assert_match %r{custom\.png$}, info.boxart_path, "Custom should beat cached"
|
|
145
|
+
ensure
|
|
146
|
+
ENV.delete('GEMBA_CONFIG_DIR')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "gemba/headless"
|
|
7
|
+
require "gemba/headless"
|
|
8
|
+
|
|
9
|
+
class TestRomOverrides < Minitest::Test
|
|
10
|
+
def setup
|
|
11
|
+
@tmpdir = Dir.mktmpdir("rom_overrides_test")
|
|
12
|
+
@json = File.join(@tmpdir, "rom_overrides.json")
|
|
13
|
+
@boxart = File.join(@tmpdir, "boxart")
|
|
14
|
+
# Point Config.boxart_dir at our tmpdir so copies land there
|
|
15
|
+
@orig_env = ENV['GEMBA_CONFIG_DIR']
|
|
16
|
+
ENV['GEMBA_CONFIG_DIR'] = @tmpdir
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def teardown
|
|
20
|
+
ENV['GEMBA_CONFIG_DIR'] = @orig_env
|
|
21
|
+
FileUtils.rm_rf(@tmpdir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_custom_boxart_returns_nil_when_nothing_set
|
|
25
|
+
overrides = Gemba::RomOverrides.new(@json)
|
|
26
|
+
assert_nil overrides.custom_boxart("AGB_AXVE-DEADBEEF")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_set_custom_boxart_copies_file_and_returns_dest
|
|
30
|
+
src = File.join(@tmpdir, "cover.png")
|
|
31
|
+
File.write(src, "fake png")
|
|
32
|
+
|
|
33
|
+
overrides = Gemba::RomOverrides.new(@json)
|
|
34
|
+
dest = overrides.set_custom_boxart("AGB_AXVE-DEADBEEF", src)
|
|
35
|
+
|
|
36
|
+
assert File.exist?(dest), "Copied file should exist at dest"
|
|
37
|
+
assert_equal "fake png", File.read(dest)
|
|
38
|
+
assert_match %r{/AGB_AXVE-DEADBEEF/custom\.png$}, dest
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_set_custom_boxart_persists_across_reload
|
|
42
|
+
src = File.join(@tmpdir, "cover.png")
|
|
43
|
+
File.write(src, "fake png")
|
|
44
|
+
|
|
45
|
+
Gemba::RomOverrides.new(@json).set_custom_boxart("AGB_AXVE-DEADBEEF", src)
|
|
46
|
+
|
|
47
|
+
reloaded = Gemba::RomOverrides.new(@json)
|
|
48
|
+
stored = reloaded.custom_boxart("AGB_AXVE-DEADBEEF")
|
|
49
|
+
refute_nil stored
|
|
50
|
+
assert File.exist?(stored)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_set_custom_boxart_preserves_extension
|
|
54
|
+
src = File.join(@tmpdir, "cover.jpg")
|
|
55
|
+
File.write(src, "fake jpg")
|
|
56
|
+
|
|
57
|
+
overrides = Gemba::RomOverrides.new(@json)
|
|
58
|
+
dest = overrides.set_custom_boxart("AGB_AXVE-DEADBEEF", src)
|
|
59
|
+
|
|
60
|
+
assert dest.end_with?(".jpg"), "Extension should be preserved"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_multiple_rom_ids_stored_independently
|
|
64
|
+
src1 = File.join(@tmpdir, "a.png"); File.write(src1, "a")
|
|
65
|
+
src2 = File.join(@tmpdir, "b.png"); File.write(src2, "b")
|
|
66
|
+
|
|
67
|
+
overrides = Gemba::RomOverrides.new(@json)
|
|
68
|
+
overrides.set_custom_boxart("AGB_AXVE-AAAAAAAA", src1)
|
|
69
|
+
overrides.set_custom_boxart("AGB_BPEE-BBBBBBBB", src2)
|
|
70
|
+
|
|
71
|
+
assert_match %r{AAAAAAAA}, overrides.custom_boxart("AGB_AXVE-AAAAAAAA")
|
|
72
|
+
assert_match %r{BBBBBBBB}, overrides.custom_boxart("AGB_BPEE-BBBBBBBB")
|
|
73
|
+
assert_nil overrides.custom_boxart("AGB_ZZZZ-ZZZZZZZZ")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_json_file_is_valid_json
|
|
77
|
+
src = File.join(@tmpdir, "cover.png")
|
|
78
|
+
File.write(src, "fake")
|
|
79
|
+
|
|
80
|
+
Gemba::RomOverrides.new(@json).set_custom_boxart("AGB_AXVE-DEADBEEF", src)
|
|
81
|
+
|
|
82
|
+
parsed = JSON.parse(File.read(@json))
|
|
83
|
+
assert_instance_of Hash, parsed
|
|
84
|
+
assert parsed.key?("AGB_AXVE-DEADBEEF")
|
|
85
|
+
end
|
|
86
|
+
end
|