최근 이 문제를 깊이, AI 친구들과 함께 고민하였다.
증상은 ES-DE를 프론트엔드로 쓰고 있을 때 Retroarch로 구동하는 게임은 문제가 없지만, 외부의 에뮬레이터를 사용해야 하는 경우에는 제대로 작동하지 않고 에뮬레이터의 메인 화면 등으로 연결되는 문제이다. 내 경우에는 Duckstation(PS1), PPSSPP(PSP), Redream(Dreamcast), NetherSX2(PS2)가 말썽이었다.
핵심은 ES-DE가 ROM의 주소(위치)를 개별 에뮬레이터에 넘겨 줄 때, 어떤 형식으로 넘겨줄 것인가의 문제이다. 크게 보면 ES-DE는 3가지 변수를 사용할 수 있다.
1) ROMSAF
일괄 디폴트로 잡혀 있는 변수다. content://com.android.externalstorage.documents/… 와 같은 형태로 주소를 넘겨준다.
2) ROMPROVIDER
ES-DE가 생성하는 변수로 content://org.es_de.frontend.files/…의 형태이다.
3) ROMRAW
/storage/xxxx-xxxx/Games/ps2/xxx.iso와 같이 파일 문자열을 그대로 보낸다.
문제는 에뮬레이터에 따라서 1)로 해결되는 경우, 2)만 먹히는 경우, 3)만 먹히는 경우가 제각각이라는 것이다. 즉, 1)로 주소 전달이 실패하는 경우 2)나 3)으로 시도해봐야 한다는 것이다. 그러면 이 변수는 어디서 어떻게 바꿔줄 수 있는가? es_systems.xml에서 규정해야 한다. 이 파일은 바꾸고 싶은 부분만 발췌한 후 파일로 만든 다음에 ES-DE/custom_systems(처음 ES-DE 구동할 때에 어디에 폴더를 만들 것인지 설정했을 것이다. 바로 그 폴더이다…) 폴더에 저장하면 된다. 가령 기본 설정에 어떻게 반영되는지를 찾아보면 플레이스테이션 2의 경우는 다음과 같은 내용이다.
<system>
<name>ps2</name>
<fullname>Sony PlayStation 2</fullname>
<path>%ROMPATH%/ps2</path>
<extension>.bin .BIN .chd .CHD .ciso .CISO .cso .CSO .dump .DUMP .elf .ELF .gz .GZ .m3u .M3U .mdf .MDF .img .IMG .iso .ISO .isz .ISZ .ngr .NRG</extension>
<command label="AetherSX2 (Standalone)">%EMULATOR_AETHERSX2% %ACTIVITY_CLEAR_TASK% %ACTIVITY_CLEAR_TOP% %ACTION%=android.intent.action.MAIN %EXTRA_bootPath%=%ROMSAF%</command>
<command label="Play! (Standalone)">%EMULATOR_PLAY!% %ACTION%=android.intent.action.VIEW %DATA%=%ROMSAF%</command>
<platform>ps2</platform>
<theme>ps2</theme>
</system>
이대로 실행하면 NetherSX2는 파일 이름이 필요하다든지 못 찾겠다고 하면서 아무런 일도 하지 않는다. %EXTRA_bootPath%=%ROMSAF% 부분에서 ‘content://com.android.externalstorage.documents/… ‘을 해석해 ROM 파일 위치를 추출하는 게 아니라, 그냥 문자열 그대로를 파일 이름으로 받아서 ROM 위치를 못 찾는 것이다. 따라서 해당 변수를 ROMRAW로 바꿔 파일명 그대로를 전달해야 한다. 이를 적용하면 es_system.xml은 이렇게 만들어야 할 것이다.
<systemList>
<system>
<name>ps2</name>
<fullname>Sony PlayStation 2</fullname>
<path>%ROMPATH%/ps2</path>
<extension>.bin .BIN .chd .CHD .ciso .CISO .cso .CSO .dump .DUMP .elf .ELF .gz .GZ .m3u .M3U .mdf .MDF .img .IMG .iso .ISO .isz .ISZ .ngr .NRG</extension>
<command label="NetherSX2(AetherSX2) (Standalone)">%EMULATOR_AETHERSX2% %ACTIVITY_CLEAR_TASK% %ACTIVITY_CLEAR_TOP% %ACTION%=android.intent.action.MAIN %EXTRA_bootPath%=%ROMRAW%</command>
<command label="Play! (Standalone)">%EMULATOR_PLAY!% %ACTION%=android.intent.action.VIEW %DATA%=%ROMSAF%</command>
<platform>ps2</platform>
<theme>ps2</theme>
</system>
</systemList>
나머지도 마찬가지 원리에 따라서 진행하면 된다.
PPSSPP는 ROMPROVIDER, Redream도 ROMPROVIDER, DuckStation은 ROMRAW 변수가 먹혔다. 이를 다 적용하려면 아래와 같은 내용으로 es_system.xml 파일을 작성하면 된다.
<systemList>
<system>
<name>ps2</name>
<fullname>Sony PlayStation 2</fullname>
<path>%ROMPATH%/ps2</path>
<extension>.bin .BIN .chd .CHD .ciso .CISO .cso .CSO .dump .DUMP .elf .ELF .gz .GZ .m3u .M3U .mdf .MDF .img .IMG .iso .ISO .isz .ISZ .ngr .NRG</extension>
<command label="NetherSX2(AetherSX2) (Standalone)">%EMULATOR_AETHERSX2% %ACTIVITY_CLEAR_TASK% %ACTIVITY_CLEAR_TOP% %ACTION%=android.intent.action.MAIN %EXTRA_bootPath%=%ROMRAW%</command>
<command label="Play! (Standalone)">%EMULATOR_PLAY!% %ACTION%=android.intent.action.VIEW %DATA%=%ROMSAF%</command>
<platform>ps2</platform>
<theme>ps2</theme>
</system>
<system>
<name>psx</name>
<fullname>Sony PlayStation</fullname>
<path>%ROMPATH%/psx</path> <extension>.bin .BIN .cbn .CBN .ccd .CCD .chd .CHD .cue .CUE .ecm .ECM .exe .EXE .img .IMG .iso .ISO .m3u .M3U .mdf .MDF .mds .MDS .minipsf .MINIPSF .pbp .PBP .psexe .PSEXE .psf .PSF .toc .TOC .z .Z .znx .ZNX .7z .7Z .zip .ZIP</extension>
<command label="Beetle PSX">%EMULATOR_RETROARCH% %EXTRA_CONFIGFILE%=/storage/emulated/0/Android/data/%ANDROIDPACKAGE%/files/retroarch.cfg %EXTRA_LIBRETRO%=mednafen_psx_libretro_android.so %EXTRA_ROM%=%ROM%</command>
<command label="Beetle PSX HW">%EMULATOR_RETROARCH% %EXTRA_CONFIGFILE%=/storage/emulated/0/Android/data/%ANDROIDPACKAGE%/files/retroarch.cfg %EXTRA_LIBRETRO%=mednafen_psx_hw_libretro_android.so %EXTRA_ROM%=%ROM%</command>
<command label="PCSX ReARMed">%EMULATOR_RETROARCH% %EXTRA_CONFIGFILE%=/storage/emulated/0/Android/data/%ANDROIDPACKAGE%/files/retroarch.cfg %EXTRA_LIBRETRO%=pcsx_rearmed_libretro_android.so %EXTRA_ROM%=%ROM%</command>
<command label="SwanStation">%EMULATOR_RETROARCH% %EXTRA_CONFIGFILE%=/storage/emulated/0/Android/data/%ANDROIDPACKAGE%/files/retroarch.cfg %EXTRA_LIBRETRO%=swanstation_libretro_android.so %EXTRA_ROM%=%ROM%</command>
<command label="DuckStation (Standalone)">%EMULATOR_DUCKSTATION% %ACTIVITY_CLEAR_TASK% %ACTIVITY_CLEAR_TOP% %EXTRABOOL_resumeState%=false %EXTRA_bootPath%=%ROMRAW%</command>
<command label="ePSXe (Standalone)">%EMULATOR_EPSXE% %ACTION%=android.intent.action.MAIN %EXTRA_com.epsxe.ePSXe.isoName%=%ROMSAF%</command>
<command label="FPseNG (Standalone)">%EMULATOR_FPSE-NG% %ACTION%=android.intent.action.VIEW %DATA%=%ROMPROVIDER%</command>
<command label="FPse (Standalone)">%EMULATOR_FPSE% %ACTION%=android.intent.action.VIEW %DATA%=%ROMPROVIDER%</command>
<platform>psx</platform>
<theme>psx</theme>
</system>
<system>
<name>psp</name>
<fullname>Sony PlayStation Portable</fullname>
<path>%ROMPATH%/psp</path>
<extension>.chd .CHD .cso .CSO .elf .ELF .iso .ISO .pbp .PBP .prx .PRX .7z .7Z .zip .ZIP</extension>
<command label="PPSSPP">%EMULATOR_RETROARCH% %EXTRA_CONFIGFILE%=/storage/emulated/0/Android/data/%ANDROIDPACKAGE%/files/retroarch.cfg %EXTRA_LIBRETRO%=ppsspp_libretro_android.so %EXTRA_ROM%=%ROM%</command>
<command label="PPSSPP (Standalone)">%EMULATOR_PPSSPP% %ACTION%=android.intent.action.VIEW %CATEGORY%=android.intent.category.DEFAULT %DATA%=%ROMPROVIDER%</command>
<platform>psp</platform>
<theme>psp</theme>
</system>
<system>
<name>dreamcast</name>
<fullname>Sega Dreamcast</fullname>
<path>%ROMPATH%/dreamcast</path>
<extension>.cdi .CDI .chd .CHD .cue .CUE .dat .DAT .elf .ELF .gdi .GDI .iso .ISO .lst .LST .m3u .M3U .7z .7Z .zip .ZIP</extension>
<command label="Flycast">%EMULATOR_RETROARCH% %EXTRA_CONFIGFILE%=/storage/emulated/0/Android/data/%ANDROIDPACKAGE%/files/retroarch.cfg %EXTRA_LIBRETRO%=flycast_libretro_android.so %EXTRA_ROM%=%ROM%</command>
<command label="Flycast (Standalone)">%EMULATOR_FLYCAST% %ACTION%=android.intent.action.VIEW %DATA%=%ROM%</command>
<command label="Redream (Standalone)">%EMULATOR_REDREAM% %ACTION%=android.intent.action.VIEW %DATA%=%ROMPROVIDER%</command>
<platform>dreamcast</platform>
<theme>dreamcast</theme>
</system>
</systemList>
위의 파일은 오타가 있을 수 있으니 신중하게 고려하시기 바라고, 아무튼 이렇게 작성한 파일을 아까 파일이 있어야 할 경로로 넣으면 대체로 될 것이다. 다른 에뮬레이터의 경우, 뭐가 되고 뭐가 안 되는지는 맨땅에 헤딩하기로 찾아야 한다.
Azahar(NDS)와 melonDS Nightly(3DS)에 대해 추가 (정리 요약은 챗GPT가 했다)
ES-DE(안드로이드) Standalone 에뮬 연결 기록
대상
-
Nintendo DS: melonDS Nightly
-
Nintendo 3DS: Azahar
핵심 결론
-
melonDS Nightly는 ES-DE에서 android.intent.action.VIEW로 실행하면서, ROM을 DATA에 ROMPROVIDER로 넣었을 때 성공했다.
-
Azahar는 ES-DE에서 android.intent.action.VIEW로 실행하면서, ROM을 EXTRA_SelectedGame에 ROMSAF로 넣었을 때 성공했다.
왜 이 방식이 먹혔는가(관찰 기반 요약)
-
같은 “ROM 주소”라도, 앱이 실제로 읽는 자리가 다르다.
-
melonDS Nightly는 “DATA로 들어온 URI” 경로에서 정상 실행이 되었고,
-
Azahar는 “특정 extra 키(SelectedGame)로 들어온 값”에서 정상 실행이 되었다.
적용 방법
-
ES-DE의 커스텀 시스템 폴더(ES-DE 첫 실행 때 지정했던 저장소 경로)로 이동한다.
-
custom_systems/es_systems.xml 파일을 만든다(또는 기존 커스텀 파일에 아래 시스템 블록을 추가한다).
-
ES-DE를 완전히 종료 후 재실행한다.
-
해당 시스템(NDS, N3DS)에서 “대체 에뮬레이터” 또는 “실행기 선택”에서 방금 추가한 라벨을 선택해 실행한다.
es_systems.xml 예시
<?xml version="1.0"?>
<systemList>
<system>
<name>nds</name>
<fullname>Nintendo DS</fullname>
<path>%ROMPATH%/nds</path>
<extension>.nds .NDS .zip .ZIP .7z .7Z</extension>
<command label="melonDS Nightly (Standalone) VIEW DATA=ROMPROVIDER">%EMULATOR_MELONDS-NIGHTLY% %ACTIVITY_CLEAR_TASK% %ACTIVITY_CLEAR_TOP% %ACTION%=android.intent.action.VIEW %CATEGORY%=android.intent.category.DEFAULT %DATA%=%ROMPROVIDER%</command>
<platform>nds</platform>
<theme>nds</theme>
</system>
<system>
<name>n3ds</name>
<fullname>Nintendo 3DS</fullname>
<path>%ROMPATH%/3DS</path>
<extension>.3ds .3DS .3dsx .3DSX .app .APP .axf .AXF .cci .CCI .cxi .CXI .elf .ELF .7z .7Z .zip .ZIP</extension>
<command label="Azahar (Standalone) VIEW EXTRA_SelectedGame=ROMSAF">%EMULATOR_AZAHAR% %ACTIVITY_CLEAR_TASK% %ACTIVITY_CLEAR_TOP% %ACTION%=android.intent.action.VIEW %CATEGORY%=android.intent.category.DEFAULT %EXTRA_SelectedGame%=%ROMSAF%</command>
<platform>n3ds</platform>
<theme>n3ds</theme>
</system>
</systemList>
세가 새턴 Yaba Sanshiro 2 Pro 세팅에 대해 추가
-
문제를 한 문장으로 요약하면
ES-DE가 “이 게임 파일을 열어라”라는 지시를 에뮬레이터로 보낼 때, 보통은 DATA 칸에 ROM 주소를 넣어 보냅니다.
그런데 Yaba Sanshiro 2 Pro는 ROM 주소를 DATA 칸에서 읽지 않고, 특정 EXTRA 칸(정해진 이름의 추가 입력란)에서 읽습니다.
그래서 ES-DE가 그 EXTRA 칸에 정확히 넣어 주도록 커맨드를 바꿔야 성공합니다.
-
DATA와 EXTRA는 무엇이 다른가
Android에서 다른 앱을 실행시키는 방식은 “인텐트”라는 전달 봉투로 생각하시면 됩니다.
(1) DATA는 봉투 겉면의 “대표 주소” 같은 자리입니다.
많은 앱들이 여기(예: DATA)에 파일 경로나 URI를 넣어 주면 그걸 읽고 실행합니다.
(2) EXTRA는 봉투 안에 들어있는 “추가 입력칸”들입니다.
예를 들어 “파일 경로는 이 칸”, “게임 코드(식별자)는 저 칸”처럼, 앱 개발자가 미리 정해둔 칸 이름으로 값을 받습니다.
Yaba Sanshiro 2 Pro는 “대표 주소(DATA)”를 보는 쪽이 아니라, “추가 입력칸(EXTRA)”에서 파일 경로를 읽는 쪽이라는 것이 이번 해결의 핵심입니다.
-
왜 Android 15에서 특히 기본 설정이 더 실패하기 쉬운가
ES-DE 기본 설정은 보통 ROMSAF나 ROMPROVIDER 같은 content:// 형태(권한을 포함한 URI)로 DATA에 넘깁니다.
Android 15에서는 저장소 권한과 URI 처리 방식이 더 엄격해져서, 어떤 앱은 이 content:// 주소를 제대로 해석하지 못하거나 권한을 못 받아서 실패하는 경우가 늘어납니다.
반면 ROMRAW는 /storage/…/파일명.chd 같은 “실제 파일 경로 문자열”이라, Yaba가 기대하는 방식에 더 가깝습니다.
그래서 이번 성공 커맨드도 ROM 경로는 ROMRAW를 쓰는 쪽으로 정리됩니다.
-
그래서 무엇을 어떻게 바꿔야 하나
바꾸는 파일은 ES-DE의 커스텀 시스템 정의 파일인 es_systems.xml 입니다.
(1) 파일 위치
ES-DE 첫 실행 때 지정했던 저장소 경로 아래 custom_systems 폴더에 둡니다.
즉 “ES-DE가 쓰는 폴더/custom_systems/es_systems.xml” 입니다.
(2) 바꿀 대상
그 파일에서 새턴 시스템 블록, 즉 <name>saturn</name> 이 들어있는 덩어리를 찾습니다.
(3) 핵심 수정
Yaba용 실행 커맨드 한 줄을 추가하거나 수정합니다.
핵심은 ROM 경로를 DATA에 넣지 말고, Yaba가 정해둔 EXTRA 키 이름으로 넣는 것입니다.
-
성공한 커맨드가 하는 일(뜻풀이)
성공 커맨드의 요점은 아래 두 값을 “EXTRA”로 넣는 것입니다.
(1) ROM 파일 경로를 넣는 칸
키 이름: org.uoyabause.android.FileNameEx
여기에 넣을 값: 실제 파일 경로 문자열, 즉 %ROMRAW%
(2) 게임 식별값을 넣는 칸
키 이름: org.uoyabause.android.gamecode
여기에 넣을 값: ES-DE에서 임시로 %BASENAME%를 사용(파일 이름 기반)
즉, 논리 구조는 이런 뜻입니다.
“야바산시로야, 이 파일(FileNameEx)을 열어라. 그리고 식별값(gamecode)은 이 값이다.”
-
따옴표가 왜 그렇게 중요하나
파일 경로에는 공백이 들어갈 수 있습니다. 예를 들어
/My Games/Saturn/Game Title.chd
따옴표가 없으면 Android 명령줄에서 공백을 기준으로 잘려서,
앞부분만 파일명으로 인식되는 문제가 생깁니다.
그래서 %ROMRAW% 같은 경로 값은 반드시 큰따옴표로 감싸
“%ROMRAW%” 형태로 전달해야 안전합니다.
-
적용 후 확인은 어디서 하나
문제가 남으면 가장 먼저 ES-DE 로그(es_log.txt)를 봅니다.
로그에 다음처럼 찍히면 “EXTRA로 전달 자체는 됐다”는 뜻입니다.
Extra name: org.uoyabause.android.FileNameEx
게임파일.chd
여기에서 공백이 있는 파일명이 중간에서 잘려 보이면, 거의 확실하게 따옴표 처리 문제입니다.
즉 커맨드에 “%ROMRAW%” 형태로 들어갔는지부터 점검하면 됩니다.
-
정리
이번 해결법의 핵심은 단순합니다.
(1) Yaba Sanshiro 2 Pro는 ROM 경로를 DATA가 아니라 EXTRA(정해진 키 이름)로 받는다.
(2) ES-DE의 saturn 시스템 커맨드에서 그 EXTRA 키에 ROMRAW 경로를, 따옴표로 감싸서 넣어 준다.
(3) 적용 후 ES-DE를 완전히 종료하고 재실행한 다음, 새턴에서 해당 라벨을 선택해 실행한다.
(4) 문제 시 로그에서 EXTRA 전달과 따옴표(공백 잘림)를 먼저 확인한다.
<system>
<name>saturn</name>
<fullname>Sega Saturn</fullname>
<path>%ROMPATH%/saturn</path>
<extension>.bin .BIN .ccd .CCD .chd .CHD .cue .CUE .img .IMG .iso .ISO .m3u .M3U .mdf .MDF .mds .MDS .7z .7Z .zip .ZIP</extension>
<command label="Yaba Sanshiro 2 Pro (Standalone)">%EMULATOR_YABASANSHIRO-2% %EXTRA_org.uoyabause.android.FileNameEx%="%ROMRAW%" %EXTRA_org.uoyabause.android.gamecode%="%BASENAME%"</command>
<platform>saturn</platform>
<theme>saturn</theme>
</system>