Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/EU_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,6 @@ SCES-01331 builds also accept the En/Es/Pt/Sv/Da **Rev 1** track (SHA-1
revision automatically; `--eu2-revision rev1 --plan` previews its commands.
Each build and native cache binds one revision. See the [EU2 guide](../eu2/README.md)
for both original inputs, contracts and the bounded EN/ES runtime sample.

Cover gallery, dashboard quick controls and SCES-01331 language confirmation:
see [European launcher additions](regional/README.md).
43 changes: 43 additions & 0 deletions docs/regional/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# European launcher additions (October 2026)

Contributed by [michaelm-afk](https://github.com/michaelm-afk) in the
24 October 2026 handoff and integrated through the shared framework and
launcher. The German notes in this folder are the contributor's earlier test
records. This page covers only what changed.

## Cover gallery

The launcher shows the bundled box art (`eu/launcher_assets/img/boxart.tga`,
from the libretro PS1 box-art set) until you choose your own covers.
**Browse Covers** opens a folder picker for the verified disc; the launcher reads
PNG, JPG, JPEG and TGA files from that folder without copying or changing them.
Files are ordered front, back, disc, then by name; arrows step through them and a
click enlarges the current one. The choice is saved per disc serial in
`artwork/<serial>/cover-folder.txt` beside the launcher, relative when the folder
is inside the installation. Cover scans and manuals are never part of this
repository or a release package.

## Quick controls

EU1 and EU2 set `[launcher] quick_controls = true`. The dashboard then shows a
**Quick Mods** card next to the memory cards, listing Widescreen and Seamless
Loading (their manifests set `quick = true`), and a **Quick Settings** card
with VSync, Internal resolution, Screen mode and Screen model. Any listed mod can
be added or removed with **Show in Quick Mods** on the Mods page.

## Wait for Language Confirmation (SCES-01331)

The original SCES-01331 release's language selector continues on its own after a
countdown. This mod holds the countdown until you press Cross, like the existing
SCES-01330 mod. It checks the selector's original code before every write and
does nothing after the selector. `eu2/boot_profile.py` verifies the executable at build time; the mod
is omitted for the Rev 1 disc, whose selector binding is not verified.

## Not integrated

- RetroAchievements: held until RetroAchievements approves a recompiled client.
- The Windows-only multi-region entry program (`launcher_entry/`): a
multi-region chooser belongs in the shared launcher on every platform.
- Ore! Tomba (Japan, SLPS-01144): kept on its own branch until it reaches
feature parity with the USA and European builds.
- USA default changes (1080p, VSync off, SPU HQ on): the USA build is unchanged.
2 changes: 2 additions & 0 deletions eu/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ psxrecomp_add_game_runtime(${tombi_target}
DEFAULT_GAME_CONFIG_PATH "${tombi_config}"
VERSION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/VERSION"
MAX_PLAYERS 1
LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"
GEN_MARKER "${TOMBA_ROOT}/build-eu-prototype/generated/SCES_013.30_dispatch.c"
GEN_FULL_GLOB "${TOMBA_ROOT}/build-eu-prototype/generated/SCES_013.30_full_*.c"
PRELOADED_MODS_DIR "${tombi_mods}"
Expand Down Expand Up @@ -117,6 +118,7 @@ if(TOMBI_PC_FEATURES)
add_executable(tombi-host-sha-tests tests/test_host_sha256.cpp "${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c")
target_include_directories(tombi-host-sha-tests PRIVATE "${PSXRECOMP_ROOT}/runtime/include")
target_compile_options(tombi-host-sha-tests PRIVATE -UNDEBUG)
add_test(NAME tombi_host_sha256 COMMAND tombi-host-sha-tests)
add_executable(tombi-language-confirmation-tests tests/test_language_confirmation.cpp boot.c)
target_include_directories(tombi-language-confirmation-tests PRIVATE
"${PSXRECOMP_ROOT}/runtime/include" "${TOMBA_ROOT}/build-eu-seamless/generated")
Expand Down
10 changes: 5 additions & 5 deletions eu/boot.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
#include "eu_boot_profile.inc"

static void selector_tick(void) {
if (psx_mod_read_word(0x80010E50u)!=0x27BDFF88u ||
psx_mod_read_word(0x800112D4u)!=0x14620039u) return;
if (psx_mod_read_word(TOMBI_SELECTOR_START)!=0x27BDFF88u ||
psx_mod_read_word(TOMBI_SELECTOR_BRANCH)!=0x14620039u) return;
uint32_t hash=2166136261u;
for (uint32_t pc=0x80010E50u;pc<0x800114A0u;pc+=4) {
for (uint32_t pc=TOMBI_SELECTOR_START;pc<TOMBI_SELECTOR_END;pc+=4) {
uint32_t word=psx_mod_read_word(pc);
for(unsigned i=0;i<4;++i){hash=(hash^(word&255u))*16777619u;word>>=8;}
}
if(hash!=TOMBI_SELECTOR_HASH) return;
const uint32_t remaining=psx_mod_read_word(0x800AF064u);
const uint32_t remaining=psx_mod_read_word(TOMBI_SELECTOR_TIMER);
if(remaining>0 && remaining<=1000)
psx_mod_write_word(0x800AF064u,1000);
psx_mod_write_word(TOMBI_SELECTOR_TIMER,1000);
}

PSX_MOD_CONSTRUCTOR(tombi_register_language_confirmation) {
Expand Down
2 changes: 1 addition & 1 deletion eu/boot_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def word(pc):return struct.unpack_from('<I',data,2048+pc-base)[0]
for byte in body:value=((value^byte)*16777619)&0xffffffff
out=ROOT/'build-eu-seamless/generated/eu_boot_profile.inc'
out.parent.mkdir(parents=True,exist_ok=True)
out.write_text(f'#define TOMBI_SELECTOR_HASH 0x{value:08X}u\n')
out.write_text(f'#define TOMBI_SELECTOR_HASH 0x{value:08X}u\n#define TOMBI_SELECTOR_START 0x80010E50u\n#define TOMBI_SELECTOR_END 0x800114A0u\n#define TOMBI_SELECTOR_BRANCH 0x800112D4u\n#define TOMBI_SELECTOR_TIMER 0x800AF064u\n')
print('PASS: original EU selector timeout, manual confirmation and complete body guard',flush=True)


Expand Down
4 changes: 4 additions & 0 deletions eu/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ default_mode = "digital"

[widescreen]
offer = false

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
4 changes: 4 additions & 0 deletions eu/launcher_assets/img/BOXART_SOURCE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Tombi! (Europe) box cover (SCES-01330 and SCES-01331 share the artwork).
Sourced from the libretro PS1 boxart set (libretro-thumbnails/Sony_-_PlayStation,
Named_Boxarts/Tombi! (Europe) (En,Fr,De,It,Nl) (Rev 1).png) and converted to a
24-bit top-origin TGA, matching launcher_assets/img/boxart.tga for Tomba! (USA).
Binary file added eu/launcher_assets/img/boxart.tga
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ name = "Widescreen"
description = "Enable Tomba's native-wide gameplay view. The render target and resident-object culling expand with the current window; extreme-width content and enemy spawning are experimental."
group = "Display"
default_enabled = false
quick = true

[[option]]
feature = "widescreen"
Expand Down
4 changes: 4 additions & 0 deletions eu/release/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ a1_sites = ["0x800200A4", "0x800201D8", "0x8002056C"]
auto_screen_x = true
screen_h_imms = ["0x100", "0x101", "0xE0", "0xF1"]
auto_backdrop = true

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
4 changes: 4 additions & 0 deletions eu/seamless/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ a1_sites = ["0x800200A4", "0x800201D8", "0x8002056C"]
auto_screen_x = true
screen_h_imms = ["0x100", "0x101", "0xE0", "0xF1"]
auto_backdrop = true

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ name = "Seamless Loading"
description = "Reduces area loading interruptions. Uses extra memory and prepares a local cache from your own disc on first use. Unsupported requests use the original loader; saves and game speed are unchanged."
group = "Quality of Life"
default_enabled = true
quick = true

[[plugin]]
feature = "resident-assets"
Expand Down
26 changes: 26 additions & 0 deletions eu2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ if(TOMBI_EU2_SEAMLESS)
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B
"${CMAKE_CURRENT_SOURCE_DIR}/geometry_profile.py" ${geometry_args} COMMAND_ERROR_IS_FATAL ANY)
set(eu2_mods "${TOMBA_ROOT}/build-eu2-seamless/generated/mods")
# Wait for Language Confirmation binds the original release's selector;
# revisions.py drops the package for Rev 1, which is not verified.
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B -c
"import sys;sys.path.insert(0,r'${CMAKE_CURRENT_SOURCE_DIR}');from revisions import active;print(active())"
OUTPUT_VARIABLE eu2_selector_revision OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
if(eu2_selector_revision STREQUAL "original")
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B "${CMAKE_CURRENT_SOURCE_DIR}/boot_profile.py" COMMAND_ERROR_IS_FATAL ANY)
endif()
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B
"${CMAKE_CURRENT_SOURCE_DIR}/widescreen_profile.py" COMMAND_ERROR_IS_FATAL ANY)
endif()
Expand All @@ -35,6 +43,8 @@ psxrecomp_add_game_runtime(tombi-eu2
DEFAULT_GAME_CONFIG_PATH "game.toml"
VERSION_FILE "${TOMBA_ROOT}/VERSION"
MAX_PLAYERS 1
# SCES-01331 shares the SCES-01330 cover artwork.
LAUNCHER_BOXART "${TOMBA_ROOT}/eu/launcher_assets/img/boxart.tga"
GEN_MARKER "${TOMBA_ROOT}/build-eu2/generated/SCES_013.31_dispatch.c"
GEN_FULL_GLOB "${TOMBA_ROOT}/build-eu2/generated/SCES_013.31_full_*.c"
PRELOADED_MODS_DIR "${eu2_mods}"
Expand All @@ -43,6 +53,9 @@ psxrecomp_add_game_runtime(tombi-eu2
set_target_properties(tombi-eu2 PROPERTIES OUTPUT_NAME "TombiRecompEU2")
if(TOMBI_EU2_SEAMLESS)
set(eu2_generated "${TOMBA_ROOT}/build-eu2-seamless/generated")
if(eu2_selector_revision STREQUAL "original")
target_sources(tombi-eu2 PRIVATE "${TOMBA_ROOT}/eu/boot.c")
endif()
set(eu2_widescreen "${eu2_generated}/widescreen")
target_sources(tombi-eu2 PRIVATE "${eu2_generated}/eu2_loader.c"
"${eu2_generated}/eu2_geometry.c"
Expand All @@ -62,6 +75,7 @@ if(TOMBI_EU2_SEAMLESS)
set_source_files_properties("${TOMBA_ROOT}/src/mods/tomba_seamless_prepare.cpp" PROPERTIES
COMPILE_DEFINITIONS "TOMBA_SEAMLESS_CATALOG_HEADER=\"${eu2_generated}/eu_catalog.inc\";TOMBA_SEAMLESS_PACK_FORMAT=\"sces01331-five-languages-gam-v1\"")
if(NOT TOMBI_EU2_PLAYER_RELEASE)
include(CTest)
add_executable(tombi-eu2-widescreen-tests "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_widescreen_identity.c"
"${eu2_widescreen}/eu_widescreen.c")
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B -c
Expand All @@ -72,6 +86,8 @@ if(TOMBI_EU2_SEAMLESS)
endif()
target_include_directories(tombi-eu2-widescreen-tests PRIVATE
"${eu2_widescreen}" "${PSXRECOMP_ROOT}/runtime/include")
add_test(NAME tombi_eu2_widescreen_identity
COMMAND tombi-eu2-widescreen-tests "${TOMBA_ROOT}/disc/eu2")
add_executable(tombi-eu2-hd-install-tests "${TOMBA_ROOT}/eu/tests/test_hd_install.cpp"
"${PSXRECOMP_ROOT}/runtime/src/texture_pack_install.cpp"
"${PSXRECOMP_ROOT}/runtime/src/duckstation_texture_pack.cpp")
Expand All @@ -84,5 +100,15 @@ if(TOMBI_EU2_SEAMLESS)
TOMBI_TEST_LOADER_SOURCE="eu2_loader.c" TOMBI_TEST_ADDRESS=eu2_address)
target_compile_options(tombi-eu2-loader-tests PRIVATE -O2 -UNDEBUG -ffunction-sections -fdata-sections)
target_link_options(tombi-eu2-loader-tests PRIVATE -Wl,--gc-sections)
add_test(NAME tombi_eu2_loader COMMAND tombi-eu2-loader-tests)
if(eu2_selector_revision STREQUAL "original")
add_executable(tombi-eu2-language-confirmation-tests tests/test_language_confirmation.cpp
"${TOMBA_ROOT}/eu/boot.c")
target_include_directories(tombi-eu2-language-confirmation-tests PRIVATE
"${PSXRECOMP_ROOT}/runtime/include" "${eu2_generated}")
target_compile_options(tombi-eu2-language-confirmation-tests PRIVATE -UNDEBUG)
add_test(NAME tombi_eu2_language_confirmation
COMMAND tombi-eu2-language-confirmation-tests "${TOMBA_ROOT}/disc/eu2")
endif()
endif()
endif()
16 changes: 16 additions & 0 deletions eu2/boot_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Original EU2 selector metadata; never write to the original executable."""
from pathlib import Path
import hashlib,struct
ROOT=Path(__file__).resolve().parents[1]
def generate():
data=(ROOT/'disc/eu2/SCES_013.31').read_bytes()
assert hashlib.sha256(data).hexdigest()=='dc65210e6601cbb4ab1cc0878a73f926a2ffd1bd3d8d7923d781164fd2835f29'
def word(pc):return struct.unpack_from('<I',data,2048+pc-0x80010000)[0]
expected={0x80010e50:0x27bdff88,0x80010f98:0x240203e8,0x80010fb0:0xac22f0a4,0x800111c0:0x30624000,0x800111c4:0x10400038,0x800112c4:0x8c63f0a4,0x800112cc:0x2463ffff,0x800112d4:0xac23f0a4,0x800112d8:0x14620039}
for pc,v in expected.items():assert word(pc)==v,hex(pc)
value=2166136261
for b in data[2048+0xe50:2048+0x14a4]:value=((value^b)*16777619)&0xffffffff
out=ROOT/'build-eu2-seamless/generated/eu_boot_profile.inc';out.parent.mkdir(parents=True,exist_ok=True)
out.write_text(f'#define TOMBI_SELECTOR_HASH 0x{value:08X}u\n#define TOMBI_SELECTOR_START 0x80010E50u\n#define TOMBI_SELECTOR_END 0x800114A4u\n#define TOMBI_SELECTOR_BRANCH 0x800112D8u\n#define TOMBI_SELECTOR_TIMER 0x800AF0A4u\n')
print('PASS original EU2 selector/explicit Cross path verified')
if __name__=='__main__':generate()
4 changes: 4 additions & 0 deletions eu2/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ default_mode = "digital"

[widescreen]
offer = false

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ name = "Widescreen"
description = "Enable Tomba's native-wide gameplay view. The render target and resident-object culling expand with the current window; extreme-width content and enemy spawning are experimental."
group = "Display"
default_enabled = false
quick = true

[[option]]
feature = "widescreen"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
format_version = 5
id = "tombi.enhancement.language-confirmation"
version = "1.0.0"
name = "Wait for Language Confirmation"
author = "TombaRecomp contributors"
description = "Keeps the original EU language selector open until you confirm. No external files required."
resolver = "declarative"
save_compatibility = "shared"

[[target]]
game_id = "SCES-01331"
disc_sha256 = "2eba3dfdd25f5d270d47f5fe408391971aa25ee09c6aac9874bba4e3bf6f32cc"

[[feature]]
id = "wait"
name = "Wait for Language Confirmation"
description = "Disables the original selector's idle countdown. Choose a language and press Cross to continue. Does not change menus or progress after boot."
group = "Quality of Life"
default_enabled = true

[[plugin]]
feature = "wait"
id = "tombi.language-confirmation"
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ name = "Seamless Loading"
description = "Uses a verified local asset cache; unsupported requests retain the original loader. Original card operations and PAL timing remain in place."
group = "Quality of Life"
default_enabled = true
quick = true

[[plugin]]
feature = "resident-assets"
Expand Down
4 changes: 4 additions & 0 deletions eu2/release/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ auto_screen_x = true
# PAL funnels compare Y against 256; keep the shared 224/241 variants too.
screen_h_imms = ["0x100", "0x101", "0xE0", "0xF1"]
auto_backdrop = true

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
4 changes: 4 additions & 0 deletions eu2/rev1/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ default_mode = "digital"

[widescreen]
offer = false

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
4 changes: 4 additions & 0 deletions eu2/rev1/release/game.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ auto_screen_x = true
# PAL funnels compare Y against 256; keep the shared 224/241 variants too.
screen_h_imms = ["0x100", "0x101", "0xE0", "0xF1"]
auto_backdrop = true

[launcher]
# Quick Mods / Quick Settings cards beside the memory cards (recomp-ui).
quick_controls = true
3 changes: 3 additions & 0 deletions eu2/revisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def stage_mods():
target = ROOT / 'build-eu2-seamless/generated/mods'
shutil.copytree(ROOT/'eu2/mods', target, dirs_exist_ok=True)
for manifest in target.glob('packages/*/*/manifest.toml'):
if 'tombi.enhancement.language-confirmation' in manifest.parts and active()!='original':
manifest.unlink()
continue # The original selector binding has not been verified for Rev 1.
text = manifest.read_text(encoding='utf-8')
text = text.replace(PROFILES['original']['sha256'], PROFILES[active()]['sha256'])
manifest.write_text(text, encoding='utf-8', newline='\n')
Expand Down
38 changes: 38 additions & 0 deletions eu2/tests/test_language_confirmation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Synthetic unit memory, not a gameplay/save test. Code fixtures are read
* from the owner's verified local files; no game bytes are compiled in. */
#include "mod_plugins.h"
#include <cassert>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include <cstdio>

static uint8_t ram[2*1024*1024];
static PSXModVBlankCallback tick;
static unsigned writes;
extern "C" int psx_mod_register_vblank_plugin(const char* id, PSXModVBlankCallback callback) {
assert(std::strcmp(id,"tombi.language-confirmation")==0);tick=callback;return 1;
}
extern "C" uint32_t psx_mod_read_word(uint32_t pc) {
uint32_t v;std::memcpy(&v,ram+(pc&0x1fffff),4);return v;
}
static void put(uint32_t pc,uint32_t v){std::memcpy(ram+(pc&0x1fffff),&v,4);}
extern "C" void psx_mod_write_word(uint32_t pc,uint32_t v){assert(pc==0x800AF0A4u && v==1000);++writes;put(pc,v);}
static void load(const std::string& path) {
std::ifstream stream(path,std::ios::binary);assert(stream);
std::vector<uint8_t> bytes{std::istreambuf_iterator<char>(stream),{}};
assert(bytes.size()>2048);std::memset(ram,0,sizeof ram);
assert(bytes.size()-2048<sizeof ram-0x10000);
std::memcpy(ram+0x10000,bytes.data()+2048,bytes.size()-2048);
}
int main(int argc,char**argv) {
assert(argc==2 && tick);const std::string disc=argv[1];load(disc+"/SCES_013.31");
for(uint32_t value:{1u,999u,1000u}){put(0x800AF0A4,value);writes=0;tick();assert(writes==1 && psx_mod_read_word(0x800AF0A4)==1000);}
for(uint32_t value:{0u,1001u,0xffffffffu}){put(0x800AF0A4,value);writes=0;tick();assert(!writes);}
put(0x800AF0A4,1);put(0x80011040,psx_mod_read_word(0x80011040)^1);writes=0;tick();assert(!writes);
for(unsigned lang:{0u,5u,6u,7u,8u}){load(disc+"/EXE/MAIN"+std::to_string(lang)+".EXE");put(0x800AF0A4,1);writes=0;tick();assert(!writes);}
std::puts("PASS: selector-only timer hold, boundary cases, complete-code mismatch and all five MAIN exclusions");
}
2 changes: 1 addition & 1 deletion psxrecomp
2 changes: 1 addition & 1 deletion recomp-ui