gemba 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/THIRD_PARTY_NOTICES +37 -2
- data/assets/placeholder_boxart.png +0 -0
- data/bin/gemba +2 -2
- data/ext/gemba/extconf.rb +23 -1
- data/ext/gemba/gemba_ext.c +436 -2
- data/ext/gemba/gemba_ext.h +2 -0
- data/gemba.gemspec +5 -3
- data/lib/gemba/achievements/achievement.rb +23 -0
- data/lib/gemba/achievements/backend.rb +186 -0
- data/lib/gemba/achievements/cache.rb +70 -0
- data/lib/gemba/achievements/credentials_presenter.rb +142 -0
- data/lib/gemba/achievements/fake_backend.rb +205 -0
- data/lib/gemba/achievements/null_backend.rb +11 -0
- data/lib/gemba/achievements/offline_backend.rb +168 -0
- data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1015 -0
- data/lib/gemba/bios.rb +54 -0
- data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
- data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
- data/lib/gemba/boxart_fetcher.rb +79 -0
- data/lib/gemba/bus_emitter.rb +13 -0
- data/lib/gemba/child_window.rb +24 -1
- data/lib/gemba/cli/commands/config_cmd.rb +83 -0
- data/lib/gemba/cli/commands/decode.rb +154 -0
- data/lib/gemba/cli/commands/patch.rb +78 -0
- data/lib/gemba/cli/commands/play.rb +78 -0
- data/lib/gemba/cli/commands/record.rb +114 -0
- data/lib/gemba/cli/commands/replay.rb +161 -0
- data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
- data/lib/gemba/cli/commands/version.rb +22 -0
- data/lib/gemba/cli.rb +52 -364
- data/lib/gemba/config.rb +134 -1
- data/lib/gemba/data/gb_games.json +1 -0
- data/lib/gemba/data/gb_md5.json +1 -0
- data/lib/gemba/data/gba_games.json +1 -0
- data/lib/gemba/data/gba_md5.json +1 -0
- data/lib/gemba/data/gbc_games.json +1 -0
- data/lib/gemba/data/gbc_md5.json +1 -0
- data/lib/gemba/emulator_frame.rb +1060 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +60 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +268 -0
- data/lib/gemba/gamepad_map.rb +103 -0
- data/lib/gemba/headless.rb +6 -5
- data/lib/gemba/headless_player.rb +33 -3
- data/lib/gemba/help_window.rb +61 -0
- data/lib/gemba/hotkey_map.rb +3 -1
- data/lib/gemba/input_recorder.rb +107 -0
- data/lib/gemba/input_replayer.rb +119 -0
- data/lib/gemba/keyboard_map.rb +90 -0
- data/lib/gemba/locales/en.yml +97 -5
- data/lib/gemba/locales/ja.yml +97 -5
- data/lib/gemba/main_window.rb +56 -0
- data/lib/gemba/modal_stack.rb +81 -0
- data/lib/gemba/patcher_window.rb +223 -0
- data/lib/gemba/platform/gb.rb +21 -0
- data/lib/gemba/platform/gba.rb +21 -0
- data/lib/gemba/platform/gbc.rb +23 -0
- data/lib/gemba/platform.rb +20 -0
- data/lib/gemba/platform_open.rb +19 -0
- data/lib/gemba/recorder.rb +4 -3
- data/lib/gemba/replay_player.rb +691 -0
- data/lib/gemba/rom_info.rb +57 -0
- data/lib/gemba/rom_info_window.rb +16 -3
- data/lib/gemba/rom_library.rb +106 -0
- data/lib/gemba/rom_overrides.rb +47 -0
- data/lib/gemba/rom_patcher/bps.rb +161 -0
- data/lib/gemba/rom_patcher/ips.rb +101 -0
- data/lib/gemba/rom_patcher/ups.rb +118 -0
- data/lib/gemba/rom_patcher.rb +109 -0
- data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
- data/lib/gemba/runtime.rb +59 -26
- data/lib/gemba/save_state_manager.rb +4 -7
- data/lib/gemba/save_state_picker.rb +17 -4
- data/lib/gemba/session_logger.rb +64 -0
- data/lib/gemba/settings/audio_tab.rb +77 -0
- data/lib/gemba/settings/gamepad_tab.rb +351 -0
- data/lib/gemba/settings/hotkeys_tab.rb +259 -0
- data/lib/gemba/settings/paths.rb +11 -0
- data/lib/gemba/settings/recording_tab.rb +83 -0
- data/lib/gemba/settings/save_states_tab.rb +91 -0
- data/lib/gemba/settings/system_tab.rb +362 -0
- data/lib/gemba/settings/video_tab.rb +318 -0
- data/lib/gemba/settings_window.rb +162 -1036
- data/lib/gemba/version.rb +1 -1
- data/lib/gemba/virtual_keyboard.rb +19 -0
- data/lib/gemba.rb +2 -12
- data/test/achievements_window/test_bulk_sync.rb +218 -0
- data/test/achievements_window/test_bus_events.rb +125 -0
- data/test/achievements_window/test_close_confirmation.rb +201 -0
- data/test/achievements_window/test_initial_state.rb +164 -0
- data/test/achievements_window/test_sorting.rb +227 -0
- data/test/achievements_window/test_tree_rendering.rb +133 -0
- data/test/fixtures/fake_bios.bin +0 -0
- data/test/fixtures/pong.gba +0 -0
- data/test/fixtures/test.gb +0 -0
- data/test/fixtures/test.gbc +0 -0
- data/test/fixtures/test_quicksave.ss +0 -0
- data/test/screenshots/no_focus.png +0 -0
- data/test/shared/teek_test_worker.rb +17 -1
- data/test/shared/tk_test_helper.rb +91 -4
- data/test/support/achievements_window_helpers.rb +18 -0
- data/test/support/fake_core.rb +25 -0
- data/test/support/fake_ra_runtime.rb +74 -0
- data/test/support/fake_requester.rb +68 -0
- data/test/support/player_helpers.rb +20 -5
- data/test/test_achievement.rb +32 -0
- data/test/{test_player.rb → test_app_controller.rb} +353 -85
- data/test/test_bios.rb +123 -0
- data/test/test_boxart_fetcher.rb +150 -0
- data/test/test_cli.rb +17 -265
- data/test/test_cli_config.rb +64 -0
- data/test/test_cli_decode.rb +97 -0
- data/test/test_cli_patch.rb +58 -0
- data/test/test_cli_play.rb +213 -0
- data/test/test_cli_ra.rb +175 -0
- data/test/test_cli_record.rb +69 -0
- data/test/test_cli_replay.rb +72 -0
- data/test/test_cli_sync_requester.rb +152 -0
- data/test/test_cli_version.rb +27 -0
- data/test/test_config.rb +2 -3
- data/test/test_config_ra.rb +69 -0
- data/test/test_core.rb +62 -1
- data/test/test_credentials_presenter.rb +192 -0
- data/test/test_event_bus.rb +100 -0
- data/test/test_fake_backend_achievements.rb +130 -0
- data/test/test_fake_backend_auth.rb +68 -0
- data/test/test_game_index.rb +77 -0
- data/test/test_game_picker_frame.rb +310 -0
- data/test/test_gamepad_map.rb +1 -3
- data/test/test_headless_player.rb +17 -3
- data/test/test_help_window.rb +82 -0
- data/test/test_hotkey_map.rb +22 -1
- data/test/test_input_recorder.rb +179 -0
- data/test/test_input_replay_determinism.rb +113 -0
- data/test/test_input_replayer.rb +162 -0
- data/test/test_keyboard_map.rb +1 -3
- data/test/test_libretro_backend.rb +41 -0
- data/test/test_locale.rb +1 -1
- data/test/test_logging.rb +123 -0
- data/test/test_null_backend.rb +42 -0
- data/test/test_offline_backend.rb +116 -0
- data/test/test_overlay_renderer.rb +1 -1
- data/test/test_platform.rb +149 -0
- data/test/test_ra_backend.rb +313 -0
- data/test/test_ra_backend_unlock_gate.rb +56 -0
- data/test/test_recorder.rb +0 -3
- data/test/test_replay_player.rb +316 -0
- data/test/test_rom_info.rb +149 -0
- data/test/test_rom_overrides.rb +86 -0
- data/test/test_rom_patcher.rb +382 -0
- data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
- data/test/test_save_state_manager.rb +2 -4
- data/test/test_settings_audio.rb +107 -0
- data/test/test_settings_hotkeys.rb +83 -66
- data/test/test_settings_recording.rb +49 -0
- data/test/test_settings_save_states.rb +97 -0
- data/test/test_settings_system.rb +133 -0
- data/test/test_settings_video.rb +450 -0
- data/test/test_settings_window.rb +76 -507
- data/test/test_tip_service.rb +6 -6
- data/test/test_toast_overlay.rb +1 -1
- data/test/test_virtual_events.rb +156 -0
- data/test/test_virtual_keyboard.rb +1 -1
- data/vendor/rcheevos/CHANGELOG.md +495 -0
- data/vendor/rcheevos/LICENSE +21 -0
- data/vendor/rcheevos/Package.swift +33 -0
- data/vendor/rcheevos/README.md +67 -0
- data/vendor/rcheevos/include/module.modulemap +70 -0
- data/vendor/rcheevos/include/rc_api_editor.h +296 -0
- data/vendor/rcheevos/include/rc_api_info.h +280 -0
- data/vendor/rcheevos/include/rc_api_request.h +77 -0
- data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
- data/vendor/rcheevos/include/rc_api_user.h +262 -0
- data/vendor/rcheevos/include/rc_client.h +877 -0
- data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
- data/vendor/rcheevos/include/rc_consoles.h +138 -0
- data/vendor/rcheevos/include/rc_error.h +59 -0
- data/vendor/rcheevos/include/rc_export.h +100 -0
- data/vendor/rcheevos/include/rc_hash.h +200 -0
- data/vendor/rcheevos/include/rc_runtime.h +148 -0
- data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
- data/vendor/rcheevos/include/rc_util.h +51 -0
- data/vendor/rcheevos/include/rcheevos.h +8 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
- data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
- data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
- data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
- data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
- data/vendor/rcheevos/src/rc_client.c +6941 -0
- data/vendor/rcheevos/src/rc_client_external.c +281 -0
- data/vendor/rcheevos/src/rc_client_external.h +177 -0
- data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
- data/vendor/rcheevos/src/rc_client_internal.h +409 -0
- data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
- data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
- data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
- data/vendor/rcheevos/src/rc_compat.c +251 -0
- data/vendor/rcheevos/src/rc_compat.h +121 -0
- data/vendor/rcheevos/src/rc_libretro.c +915 -0
- data/vendor/rcheevos/src/rc_libretro.h +98 -0
- data/vendor/rcheevos/src/rc_util.c +199 -0
- data/vendor/rcheevos/src/rc_version.c +11 -0
- data/vendor/rcheevos/src/rc_version.h +32 -0
- data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
- data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
- data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
- data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
- data/vendor/rcheevos/src/rcheevos/format.c +330 -0
- data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
- data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
- data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
- data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
- data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
- data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
- data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
- data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
- data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
- data/vendor/rcheevos/src/rcheevos/value.c +935 -0
- data/vendor/rcheevos/src/rhash/aes.c +480 -0
- data/vendor/rcheevos/src/rhash/aes.h +49 -0
- data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
- data/vendor/rcheevos/src/rhash/hash.c +1402 -0
- data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
- data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
- data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
- data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
- data/vendor/rcheevos/src/rhash/md5.c +382 -0
- data/vendor/rcheevos/src/rhash/md5.h +91 -0
- data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
- data/vendor/rcheevos/test/libretro.h +205 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
- data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
- data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
- data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
- data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
- data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
- data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
- data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
- data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
- data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
- data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
- data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
- data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
- data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
- data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
- data/vendor/rcheevos/test/rhash/data.c +657 -0
- data/vendor/rcheevos/test/rhash/data.h +32 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
- data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
- data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
- data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
- data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
- data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
- data/vendor/rcheevos/test/test.c +113 -0
- data/vendor/rcheevos/test/test_framework.h +205 -0
- data/vendor/rcheevos/test/test_rc_client.c +10509 -0
- data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
- data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
- data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
- data/vendor/rcheevos/test/test_types.natvis +9 -0
- data/vendor/rcheevos/validator/validator.c +658 -0
- data/vendor/rcheevos/validator/validator.vcxproj +152 -0
- data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
- metadata +274 -11
- data/lib/gemba/input_mappings.rb +0 -214
- data/lib/gemba/player.rb +0 -1525
|
@@ -0,0 +1,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
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
# Bootstrap Zeitwerk autoloading without Tk/SDL2.
|
|
8
|
+
require_relative "../lib/gemba/headless"
|
|
9
|
+
|
|
10
|
+
class TestRomPatcher < Minitest::Test
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Fixture helpers — build valid binary patch data in-memory
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
# Build a minimal IPS patch that applies the given records.
|
|
17
|
+
# records: [{offset:, data:}] or [{offset:, rle_count:, rle_val:}]
|
|
18
|
+
def build_ips(records)
|
|
19
|
+
io = StringIO.new.tap { |s| s.binmode }
|
|
20
|
+
io.write("PATCH")
|
|
21
|
+
records.each do |rec|
|
|
22
|
+
off = rec[:offset]
|
|
23
|
+
io.write([off >> 16, (off >> 8) & 0xFF, off & 0xFF].pack("CCC"))
|
|
24
|
+
if rec[:rle_count]
|
|
25
|
+
io.write([0, rec[:rle_count]].pack("nn"))
|
|
26
|
+
io.write([rec[:rle_val]].pack("C"))
|
|
27
|
+
else
|
|
28
|
+
io.write([rec[:data].bytesize].pack("n"))
|
|
29
|
+
io.write(rec[:data].b)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
io.write("EOF")
|
|
33
|
+
io.string.b
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Encode a BPS varint (byuu's additive-shift encoding).
|
|
37
|
+
def bps_varint(n)
|
|
38
|
+
out = "".b
|
|
39
|
+
loop do
|
|
40
|
+
x = n & 0x7f
|
|
41
|
+
n >>= 7
|
|
42
|
+
if n == 0
|
|
43
|
+
out << (0x80 | x).chr
|
|
44
|
+
break
|
|
45
|
+
end
|
|
46
|
+
out << x.chr
|
|
47
|
+
n -= 1
|
|
48
|
+
end
|
|
49
|
+
out
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build a BPS patch using only TargetRead records (writes literal target data).
|
|
53
|
+
# Simplest valid BPS: ignores source entirely, just emits target bytes.
|
|
54
|
+
def build_bps(source, target)
|
|
55
|
+
source = source.b
|
|
56
|
+
target = target.b
|
|
57
|
+
body = StringIO.new.tap { |s| s.binmode }
|
|
58
|
+
body.write("BPS1")
|
|
59
|
+
body.write(bps_varint(source.bytesize))
|
|
60
|
+
body.write(bps_varint(target.bytesize))
|
|
61
|
+
body.write(bps_varint(0)) # metadata_size = 0
|
|
62
|
+
# One TargetRead record covering the entire target
|
|
63
|
+
word = ((target.bytesize - 1) << 2) | 1
|
|
64
|
+
body.write(bps_varint(word))
|
|
65
|
+
body.write(target)
|
|
66
|
+
payload = body.string.b
|
|
67
|
+
src_crc = Zlib.crc32(source)
|
|
68
|
+
tgt_crc = Zlib.crc32(target)
|
|
69
|
+
patch_crc = Zlib.crc32(payload)
|
|
70
|
+
payload + [src_crc, tgt_crc, patch_crc].pack("VVV")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Encode a UPS varint (simple bitshift encoding).
|
|
74
|
+
def ups_varint(n)
|
|
75
|
+
out = "".b
|
|
76
|
+
loop do
|
|
77
|
+
x = n & 0x7f
|
|
78
|
+
n >>= 7
|
|
79
|
+
if n == 0
|
|
80
|
+
out << (0x80 | x).chr
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
out << x.chr
|
|
84
|
+
end
|
|
85
|
+
out
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build a UPS patch from source → target.
|
|
89
|
+
def build_ups(source, target)
|
|
90
|
+
source = source.b
|
|
91
|
+
target = target.b
|
|
92
|
+
max_size = [source.bytesize, target.bytesize].max
|
|
93
|
+
|
|
94
|
+
# Collect diff hunks: each is {start:, xor_bytes:}
|
|
95
|
+
hunks = []
|
|
96
|
+
i = 0
|
|
97
|
+
while i < max_size
|
|
98
|
+
s = source.getbyte(i) || 0
|
|
99
|
+
t = target.getbyte(i) || 0
|
|
100
|
+
if s != t
|
|
101
|
+
hunk_start = i
|
|
102
|
+
xor_bytes = "".b
|
|
103
|
+
while i < max_size
|
|
104
|
+
s = source.getbyte(i) || 0
|
|
105
|
+
t = target.getbyte(i) || 0
|
|
106
|
+
break if s == t
|
|
107
|
+
xor_bytes << (s ^ t).chr
|
|
108
|
+
i += 1
|
|
109
|
+
end
|
|
110
|
+
hunks << { start: hunk_start, xor_bytes: xor_bytes }
|
|
111
|
+
else
|
|
112
|
+
i += 1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Build body
|
|
117
|
+
body = StringIO.new.tap { |s| s.binmode }
|
|
118
|
+
body.write("UPS1")
|
|
119
|
+
body.write(ups_varint(source.bytesize))
|
|
120
|
+
body.write(ups_varint(target.bytesize))
|
|
121
|
+
|
|
122
|
+
pos = 0
|
|
123
|
+
hunks.each do |h|
|
|
124
|
+
skip = h[:start] - pos
|
|
125
|
+
body.write(ups_varint(skip))
|
|
126
|
+
body.write(h[:xor_bytes])
|
|
127
|
+
body.write("\x00")
|
|
128
|
+
pos = h[:start] + h[:xor_bytes].bytesize + 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
payload = body.string.b
|
|
132
|
+
src_crc = Zlib.crc32(source)
|
|
133
|
+
tgt_crc = Zlib.crc32(target)
|
|
134
|
+
patch_crc = Zlib.crc32(payload)
|
|
135
|
+
payload + [src_crc, tgt_crc, patch_crc].pack("VVV")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# A small fake ROM — 64 zero bytes, like a blank cartridge header area.
|
|
139
|
+
def blank_rom(size = 64)
|
|
140
|
+
"\x00".b * size
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# RomPatcher (dispatcher)
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def test_detect_format_ips
|
|
148
|
+
patch = "PATCH" + "EOF"
|
|
149
|
+
assert_equal :ips, Gemba::RomPatcher.detect_format(patch)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_detect_format_bps
|
|
153
|
+
patch = "BPS1\x00"
|
|
154
|
+
assert_equal :bps, Gemba::RomPatcher.detect_format(patch)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_detect_format_ups
|
|
158
|
+
patch = "UPS1\x00"
|
|
159
|
+
assert_equal :ups, Gemba::RomPatcher.detect_format(patch)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_detect_format_unknown
|
|
163
|
+
assert_nil Gemba::RomPatcher.detect_format("JUNK")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_safe_out_path_no_collision
|
|
167
|
+
path = "/tmp/nonexistent_gemba_test_#{Process.pid}.gba"
|
|
168
|
+
assert_equal path, Gemba::RomPatcher.safe_out_path(path)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_safe_out_path_collision
|
|
172
|
+
Dir.mktmpdir do |dir|
|
|
173
|
+
base = File.join(dir, "game.gba")
|
|
174
|
+
File.write(base, "x")
|
|
175
|
+
result = Gemba::RomPatcher.safe_out_path(base)
|
|
176
|
+
assert_equal File.join(dir, "game-(2).gba"), result
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_safe_out_path_multiple_collisions
|
|
181
|
+
Dir.mktmpdir do |dir|
|
|
182
|
+
File.write(File.join(dir, "game.gba"), "x")
|
|
183
|
+
File.write(File.join(dir, "game-(2).gba"), "x")
|
|
184
|
+
result = Gemba::RomPatcher.safe_out_path(File.join(dir, "game.gba"))
|
|
185
|
+
assert_equal File.join(dir, "game-(3).gba"), result
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_patch_dispatches_to_ips
|
|
190
|
+
Dir.mktmpdir do |dir|
|
|
191
|
+
rom_path = File.join(dir, "rom.gba")
|
|
192
|
+
patch_path = File.join(dir, "fix.ips")
|
|
193
|
+
out_path = File.join(dir, "rom-patched.gba")
|
|
194
|
+
|
|
195
|
+
source = blank_rom
|
|
196
|
+
File.binwrite(rom_path, source)
|
|
197
|
+
File.binwrite(patch_path, build_ips([{ offset: 0, data: "\xFF\xFE\xFD\xFC" }]))
|
|
198
|
+
|
|
199
|
+
Gemba::RomPatcher.patch(rom_path: rom_path, patch_path: patch_path, out_path: out_path)
|
|
200
|
+
result = File.binread(out_path)
|
|
201
|
+
assert_equal "\xFF".b, result[0, 1]
|
|
202
|
+
assert_equal "\xFE".b, result[1, 1]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_patch_raises_on_unknown_format
|
|
207
|
+
Dir.mktmpdir do |dir|
|
|
208
|
+
File.binwrite(File.join(dir, "rom.gba"), "X" * 16)
|
|
209
|
+
File.binwrite(File.join(dir, "bad.xyz"), "JUNK")
|
|
210
|
+
assert_raises(RuntimeError) do
|
|
211
|
+
Gemba::RomPatcher.patch(
|
|
212
|
+
rom_path: File.join(dir, "rom.gba"),
|
|
213
|
+
patch_path: File.join(dir, "bad.xyz"),
|
|
214
|
+
out_path: File.join(dir, "out.gba")
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# IPS
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def test_ips_overwrites_bytes_at_offset
|
|
225
|
+
source = blank_rom
|
|
226
|
+
patch = build_ips([{ offset: 4, data: "\xFF\xFE\xFD" }])
|
|
227
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
228
|
+
assert_equal "\x00".b * 4, result[0, 4], "bytes before offset unchanged"
|
|
229
|
+
assert_equal "\xFF\xFE\xFD".b, result[4, 3], "patch bytes applied"
|
|
230
|
+
assert_equal "\x00".b, result[7, 1], "bytes after patch unchanged"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_ips_rle_record_fills_region
|
|
234
|
+
source = blank_rom
|
|
235
|
+
patch = build_ips([{ offset: 8, rle_count: 4, rle_val: 0xAB }])
|
|
236
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
237
|
+
assert_equal "\xAB".b * 4, result[8, 4]
|
|
238
|
+
assert_equal "\x00".b, result[7, 1], "byte before RLE unchanged"
|
|
239
|
+
assert_equal "\x00".b, result[12, 1], "byte after RLE unchanged"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def test_ips_multiple_records
|
|
243
|
+
source = blank_rom
|
|
244
|
+
patch = build_ips([
|
|
245
|
+
{ offset: 0, data: "\x01\x02" },
|
|
246
|
+
{ offset: 10, data: "\x03\x04" },
|
|
247
|
+
])
|
|
248
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
249
|
+
assert_equal "\x01\x02".b, result[0, 2]
|
|
250
|
+
assert_equal "\x03\x04".b, result[10, 2]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_ips_extends_rom_if_patch_exceeds_size
|
|
254
|
+
source = "\x00".b * 4
|
|
255
|
+
patch = build_ips([{ offset: 8, data: "\xFF\xFF" }])
|
|
256
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
257
|
+
assert result.bytesize >= 10, "ROM extended to fit patch"
|
|
258
|
+
assert_equal "\xFF\xFF".b, result[8, 2]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_ips_empty_patch_returns_rom_unchanged
|
|
262
|
+
source = "HELLO".b
|
|
263
|
+
patch = build_ips([])
|
|
264
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
265
|
+
assert_equal source, result
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
# BPS
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def test_bps_target_read_produces_correct_output
|
|
273
|
+
source = blank_rom(8)
|
|
274
|
+
target = "\x11\x22\x33\x44\x55\x66\x77\x88".b
|
|
275
|
+
patch = build_bps(source, target)
|
|
276
|
+
result = Gemba::RomPatcher::BPS.apply(source, patch)
|
|
277
|
+
assert_equal target, result
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def test_bps_crc_mismatch_raises
|
|
281
|
+
source = blank_rom(8)
|
|
282
|
+
target = "\xDE\xAD\xBE\xEF\x00\x00\x00\x00".b
|
|
283
|
+
patch = build_bps(source, target)
|
|
284
|
+
# Corrupt the source CRC (bytes -12..-9)
|
|
285
|
+
bad_patch = patch.dup.b
|
|
286
|
+
bad_patch[-12] = "\xFF".b
|
|
287
|
+
err = assert_raises(RuntimeError) { Gemba::RomPatcher::BPS.apply(source, bad_patch) }
|
|
288
|
+
assert_match(/CRC32/, err.message)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_bps_identical_source_and_target
|
|
292
|
+
source = "GEMBA".b
|
|
293
|
+
target = "GEMBA".b
|
|
294
|
+
patch = build_bps(source, target)
|
|
295
|
+
result = Gemba::RomPatcher::BPS.apply(source, patch)
|
|
296
|
+
assert_equal target, result
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# UPS
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
def test_ups_xors_differing_bytes
|
|
304
|
+
source = "\x00\x00\x00\x00".b
|
|
305
|
+
target = "\xFF\x00\xFF\x00".b
|
|
306
|
+
patch = build_ups(source, target)
|
|
307
|
+
result = Gemba::RomPatcher::UPS.apply(source, patch)
|
|
308
|
+
assert_equal target, result
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def test_ups_multiple_hunks
|
|
312
|
+
source = "\x00" * 16
|
|
313
|
+
target = source.dup.b
|
|
314
|
+
target.setbyte(0, 0xAA)
|
|
315
|
+
target.setbyte(8, 0xBB)
|
|
316
|
+
target.setbyte(15, 0xCC)
|
|
317
|
+
patch = build_ups(source.b, target)
|
|
318
|
+
result = Gemba::RomPatcher::UPS.apply(source.b, patch)
|
|
319
|
+
assert_equal target, result
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_ups_crc_mismatch_raises
|
|
323
|
+
source = blank_rom(8)
|
|
324
|
+
target = "\xCA\xFE\xBA\xBE\x00\x00\x00\x00".b
|
|
325
|
+
patch = build_ups(source, target)
|
|
326
|
+
bad_patch = patch.dup.b
|
|
327
|
+
bad_patch[-12] = "\x00".b
|
|
328
|
+
bad_patch[-11] = "\x00".b
|
|
329
|
+
err = assert_raises(RuntimeError) { Gemba::RomPatcher::UPS.apply(source, bad_patch) }
|
|
330
|
+
assert_match(/CRC32/, err.message)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def test_ups_identical_source_and_target
|
|
334
|
+
source = "GEMBA\x00\x00\x00".b
|
|
335
|
+
target = source.dup
|
|
336
|
+
patch = build_ups(source, target)
|
|
337
|
+
result = Gemba::RomPatcher::UPS.apply(source, patch)
|
|
338
|
+
assert_equal target, result
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def test_ups_pads_target_when_source_is_shorter
|
|
342
|
+
# target_size > source_size — result zero-pads to target_size
|
|
343
|
+
source = "\x01\x02".b
|
|
344
|
+
target = "\x01\x03\x00\x00".b # byte 1 differs; bytes 2-3 are 0 (matching padding)
|
|
345
|
+
patch = build_ups(source, target)
|
|
346
|
+
result = Gemba::RomPatcher::UPS.apply(source, patch)
|
|
347
|
+
assert_equal target, result
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
# ZIP ROM input
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
def test_patch_with_zip_rom_produces_gba_output
|
|
355
|
+
require 'zip'
|
|
356
|
+
dir = Dir.mktmpdir
|
|
357
|
+
begin
|
|
358
|
+
# Build a tiny ROM and wrap it in a zip
|
|
359
|
+
rom_data = blank_rom
|
|
360
|
+
zip_path = File.join(dir, "game.zip")
|
|
361
|
+
patch_path = File.join(dir, "fix.ips")
|
|
362
|
+
out_path = File.join(dir, "game-patched.gba")
|
|
363
|
+
|
|
364
|
+
Zip::OutputStream.open(zip_path) do |zos|
|
|
365
|
+
zos.put_next_entry("game.gba")
|
|
366
|
+
zos.write(rom_data)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
File.binwrite(patch_path, build_ips([{ offset: 0, data: "\xFF\xFE" }]))
|
|
370
|
+
|
|
371
|
+
resolved = Gemba::RomResolver.resolve(zip_path)
|
|
372
|
+
Gemba::RomPatcher.patch(rom_path: resolved, patch_path: patch_path, out_path: out_path)
|
|
373
|
+
|
|
374
|
+
assert File.exist?(out_path), "expected output at #{out_path}"
|
|
375
|
+
assert_equal ".gba", File.extname(out_path)
|
|
376
|
+
assert_equal "\xFF".b, File.binread(out_path, 1)
|
|
377
|
+
ensure
|
|
378
|
+
# Windows may still hold the zip file handle until GC — ignore EACCES on cleanup
|
|
379
|
+
FileUtils.remove_entry(dir) rescue nil
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|