Previously in this series of articles, we’ve answered some philosophical questions about reverse-engineering and practical ones about Tenchu: Stealth Assassins. It’s time to start progress on this project by acquiring files to reverse-engineer, starting with the games themselves.

Acquiring artifacts

First order of business is to acquire the original media in a format we can work with. There are a couple of options available here:

  • buying then downloading it from a digital store if available ;
  • acquiring a physical copy (most likely on the second hand market in this case) and using a CD drive to rip it to an image file.

Other ways of “acquiring” old video games may have legal issues surrounding them depending on your jurisdiction. Such issues will not be covered here.

That being said, let’s assume that we do have a complete set of ISOs for the various versions of the game. We could go straight for the reverse-engineering part, but where’s the fun in that?

Running the artifacts

To run Tenchu: Stealth Assassins in a controlled environment, I’ll use DuckStation, a modern PlayStation 1 emulator. The main reason is that I contributed a GDB stub a couple of years ago, which will be useful later on to introspect the game at runtime through a debugger.

Getting it to run on a Debian 11 system is a bit of a chore:

$ sudo apt-get install \
    build-essential \
    cmake \
    ninja-build \
    libsdl2-dev \
    libcurl4-openssl-dev \
    libqt6-base-dev \
    qt6-base-dev \
    qt6-tools-dev \
    qt6-base-private-dev

It takes some amount of patching to manage to get it to build on this system:

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 47d00879..6d5fe750 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -47,7 +47,7 @@ endif()
 
 # Required libraries.
 if(ENABLE_SDL2)
-  find_package(SDL2 2.28.5 REQUIRED)
+  find_package(SDL2 2.0 REQUIRED)
 endif()
 if(NOT WIN32 AND NOT ANDROID)
   find_package(CURL REQUIRED)
@@ -59,7 +59,7 @@ if(NOT WIN32 AND NOT ANDROID)
   endif()
 endif()
 if(BUILD_QT_FRONTEND)
-  find_package(Qt6 6.5.3 COMPONENTS Core Gui Widgets Network LinguistTools REQUIRED)
+  find_package(Qt6 6.4.0 COMPONENTS Core Gui Widgets Network LinguistTools REQUIRED)
 endif()
 
 
diff --git a/src/duckstation-qt/logwindow.cpp b/src/duckstation-qt/logwindow.cpp
index 64b95616..7d4cdcbf 100644
--- a/src/duckstation-qt/logwindow.cpp
+++ b/src/duckstation-qt/logwindow.cpp
@@ -274,7 +274,7 @@ void LogWindow::logCallback(void* pUserParam, const char* channelName, const cha
 
   QString qmessage;
   qmessage.reserve(message.length() + 1);
-  qmessage.append(QUtf8StringView(message.data(), message.length()));
+  qmessage.append(message.data());
   qmessage.append(QChar('\n'));
 
   const QLatin1StringView qchannel((level <= LOGLEVEL_PERF) ? functionName : channelName);
diff --git a/src/util/sdl_input_source.cpp b/src/util/sdl_input_source.cpp
index 4db534bb..a07a3209 100644
--- a/src/util/sdl_input_source.cpp
+++ b/src/util/sdl_input_source.cpp
@@ -255,7 +255,7 @@ void SDLInputSource::SetHints()
   }
 
   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
-  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
+  //SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
   // Enable Wii U Pro Controller support
   // New as of SDL 2.26, so use string
   SDL_SetHint("SDL_JOYSTICK_HIDAPI_WII", "1");

But after that, it builds and runs without too many issues:

$ cmake -B build/ -G Ninja .
$ ninja -C build/
$ ln -s ~/Documents/duckstation/build/bin/duckstation-qt ~/.local/bin/duckstation-qt

The main reason for this janky setup instead of using the official prebuilt artifacts is to enable local development of DuckStation. In particular, I anticipate that the bare-bones GDB stub I wrote several years ago will probably need some improvements to support this decompilation project.

Extracting a PlayStation ISO

So, we have an image of a video game and we even checked that it works. At the moment it’s just a big blob of bytes, but we can start tearing it apart with jPSXdec, a tool to extract and convert files from PlayStation titles, free for non-commercial uses.

After grabbing the latest release, we can run the tool on the first track of Rittai Ninja Katsugeki Tenchu: Shinobi Gaisen and extract the files:

We get a whole bunch of files, which we can quickly triage as follows:

  • SYSTEM.CNF: this text file contains instructions for the BIOS on how to launch the game ;
  • SLPS_019.01: this is the PSX-EXE executable to launch as specified by SYSTEM.CNF ;
  • TENCHU/*.EXE: these are executable files ;
  • TENCHU/MOVIE/*: these are full motion videos ;
  • TENCHU/XA/*: these are audio files used for music and cutscenes ;
  • TENCHU/DATA.VOL: this seems to be an archive file in an unknown format, as jPSXdec detects over 880 images contained inside of it.

Most, but not all versions of Tenchu: Stealth Assassins follow this naming pattern.

What’s that file?

There is one file that jPSXdec can’t process: TENCHU/CD.CA. This file is located inside the second track and therefore jPSXdec can’t extract it when opening the first track. It also fails to recognize any file or data when we open the second track.

Hmmm… What does the cue file for this CD tells us?

$ cat 'Rittai Ninja Katsugeki - Tenchu - Shinobi Gaisen (Japan).cue'
FILE "Rittai Ninja Katsugeki - Tenchu - Shinobi Gaisen (Japan) (Track 1).bin" BINARY
  TRACK 01 MODE2/2352
    INDEX 01 00:00:00
FILE "Rittai Ninja Katsugeki - Tenchu - Shinobi Gaisen (Japan) (Track 2).bin" BINARY
  TRACK 02 AUDIO
    INDEX 00 00:00:00
    INDEX 01 00:02:00

It’s most likely a CD audio track. Assuming that the track file contains raw CD audio data, we can play it using the play tool from sox with the right parameters:

$ sudo apt-get install sox
$ play --type raw --channels 2 --rate 44100 --encoding signed --bits 16 --endian little 'Rittai Ninja Katsugeki - Tenchu - Shinobi Gaisen (Japan) (Track 2).bin'
play WARN alsa: can't encode 0-bit Unknown or not applicable

Rittai Ninja Katsugeki - Tenchu - Shinobi Gaisen (Japan) (Track 2).bin:

 File Size: 7.81M     Bit Rate: 1.41M
  Encoding: Signed PCM    
  Channels: 2 @ 16-bit   
Samplerate: 44100Hz      
Replaygain: off         
  Duration: 00:00:44.29
In:100%  00:00:44.29 [00:00:00.00] Out:1.95M [      |      ] Hd:0.1 Clip:0    
Done.

Be very careful when playing raw files as audio, especially when using headphones. A wrong guess can produce ear-shattering static noise.

It’s a voice recording of a conversation between Rikimaru, Ayame and Princess Kiku in Japanese. I do not speak this language and therefore can’t understand what is being said, but I can hear the words “game disc”, “PlayStation” and “CD player”. This is most likely a message played when the disc is inserted in a CD player, telling the user to put it in a PlayStation system.

This was a common type of easter egg for CD-ROM video games in the 1990s. Some well-known examples are Skies of Arcadia for the Dreamcast or Lego Island for the PC.

Conclusion

We have the game, we have extracted its files and we have uncovered an audio easter egg when triaging them. Next time, we’ll set up a reverse-engineering environment with Ghidra so that we can start figuring out how this game actually runs.