안드로이드 에뮬 게임기에서 ES-DE와 Standalone 에뮬레이터 연결 문제
최근 이 문제를 깊이, 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>
