Hipnosis' Stuff
Hipnosis' Stuff

Bringing back to modern life – Tonic Trouble

Introduction

Some old games tend to be a bit tricky to make work on modern systems, and Rayman 2 Tonic Trouble is no exception. May it be because the game itself, or some of its dependencies, always something holds it back from working, and it’s generally the job of the community (or just people interested on it) to bring it back from the darkness to the modern technological life.


Background

This is the case for the often forgotten (and shadowed by its sister project, Rayman 2) Tonic Trouble. It’s worth mentioning that this game release was kind of a mess, with two different versions: one completed (Retail), and one unfinished (Special Edition). Because Ubisoft fucked up the release across Europe, we’ll be focusing just on the intended release, the Retail version, despite that, later on, the same treatment could be applied to the Special Edition as well, since a lot of people seem to like that one more.

At the same time, the Retail release had several versions released. It mostly has to do with the releases on different countries, each with a different one depending on the date it was released. I don’t know the changes across versions, but most likely these were just bug fixes, nothing big. While the plan here is to cover all of them, we need to start somewhere, so we’ll begin with the latest of the latest, the Review English version, released in Italy and Spain, of course. Later on it’ll make more sense and be part of the several re-releases all over Europe.


The objective here

Before we begin, I should explain the objective here. There’s no science in using a wrapper and presto, the game is working (well not exactly, you’ll see why). But this is an attempt to ease and automatize the setup and configuration process, mostly for the average user that just want to play the game without any hassle. The goal is to make an all-in-one (AIO) program that deals with everything (a launcher, if you like), including the more straightforward stuff like the pre-configurated graphics wrapper as well as the more technical stuff, which will be the main focus of the article and deeply explained in what remains of it.

The contents will include very basic and simple x86 analysis and reverse engineering. But as a beginner on this field, I think it could be of interest to some in a similar situation, or who think this kind of stuff is very complex. Once you understand the basics on how assembly works and get familiar with the tools and software needed, is very straightforward (but not always easy) and logical to work with. So if something (maybe the whole article) seems like a stupid and pointless thing to explain, keep that aspect in mind. With that said, let’s begin.


Analyzing the situation and setting up a working environment

So, the Review English version is very particular, since unlike the others, for whatever reason, it has the executable packed. So we just unpack it, right?. Don’t be silly, of course there’s a catch. Through normal analysis, we can easily tell the executable is packed, since from the PE Header to the Sections Table there’s garbled data:


How it looks (left), and how a normal section should look (right).


Ok, so now that we know this, let’s try to unpack it:


Fuck.


Fantastic, we don’t know with what it was compressed. You’ll say, ‘okay, no big deal’, but the thing is that whatever is used to unpack the file, it doesn’t work on anything past Windows XP, making the program crash during the unpack process in modern systems, just like the one you’re using right now. Besides, this wouldn’t allow us to statically decompile and analyze the program.

The only solution I could think of at this point is the brute way, dumping the memory. After hours of debugging and research (keep in mind, I’m a newbie when it comes to reversing, let alone knowing about the PE format structure and rebuilding an executable) on a real Windows XP machine (not virtual machines, since the virtual GPUs don’t meet the game requirements), I gave up. While the data was there, there were missing imports, rendering the executable unusable, and I’m not experienced enough to find and redirect that manually.

Thankfully, the community I mentioned at the beginning comes at rescue. In the Rayman Pirate-Community (by the way it’s a Rayman 2 pun, nothing to do with piracy), the user RibShark had already achieved it, sharing the unpacked executable. As I stated before, it was done by manual unpacking, so shoutouts to him for it.

Now with the unprotected executable, and the temporal help of a video wrapper, we can begin to work.


Enhancing the executable

With everything in a minimum working state, we can begin adding some nice features from a more technical aspect, since the game already works as is, although in a sketchy way. First the game needs some files to start, which are generated by the included setup program, which in turn also needs files created when the game is installed from the CD.

We need to keep in mind that the game dates from 1999, and around that time a lot of software used the Windows folder to store most of its configuration and other data as well. This case is no exception, and the game stores its settings under Windows/UbiSoft (remember when they were stylized LikeThat?). One of our tasks is to change that and make it use the game root folder. This will make the game fully portable, and will avoid having to install the game in first place. Also, some other quality-of-life (QOL) enhancements will be added, like removing the log system and redirect some folders, rendering the necessity of the CD non-existent.

For the graphics department, a video wrapper will be included (the magnificent dgVoodoo), to max the resolution to desktop. Additionally, a widescreen patch will be available, which involves some nice math. All of this will be implemented in a dynamic way so no manual configuration should be involved, it’s just a matter of activating or deactivating each option.

Now that we have an idea of what we want to do and how everything will work, we can begin with the neat stuff.


Getting dirty

Okay, so this step will require some decompiling and debugging. The two aren’t really necessary together, but I like to see things from both perspectives. Still, I use a lot more decompilation for static code analyzing, and hex editors for the actual test of the patches. Pretty unconventional I know, but I’m more comfortable working that way for simple stuff like this. But don’t get me wrong, I do like breakpoints.

For this, we’ll use Ghidra for decompilation and x64dbg (x32dbg actually) for debugging, in addition of a hex editor (HxD) and Process Monitor, core for our success.

The first obvious thing to try is to see if we can modify the values of interest directly from the binary with a hex editor, mainly stored strings. But it looks like it’s just possible to change filenames, not directories. This lets us change quite some things, like logs and other files, but not the game configuration path.

With that, we start right away analyzing the executable with Ghidra. We’re trying to redirect the file operations with the Windows folder (in regards to the game settings, in this case under the path UbiSoft/Ubi.ini) to the current working directory of the program, so the first thing that comes to mind is to search for calls to the function GetWindowsDirectoryA, and find a way to replace it with GetCurrentDirectoryA. After a quick search 4 results are found, all with a similar structure:



We can see the UbiSoft/Ubi.ini string, also present in the binary. This means that we found what we were looking for, repeating in the four calls, all at the beginning of their respective functions. Now it’s time to see what to do.

At first, in an unsuccessful attempt, I tried to tweak them into GetCurrentDirectoryA calls. By the logic of the function, it should’ve worked, but computers are smarter than me. The string ended up cut for some reason, checked in x32dbg and Process Monitor. After more stupid attempts, I realized something very important.

Remember at the beginning when I mentioned Rayman 2 as a sister project? Turns out that Rayman 2 uses the same base game engine as Tonic Trouble, with quiet some more polishing on top. The cool part is that it behaves exactly the same as Tonic Trouble file management-wise, which means it also stores files under the Windows folder. This would mean nothing if Rayman 2 wasn’t a very popular game, but that’s not the case. In 2011, Ubisoft re-launched Rayman 2 on GOG, making some fixes along the way for its proper functioning in the systems at the time. One of those changes was the one we are treating right here, file redirection from the Windows folder to the game folder.

With this information, now we are going to do the exact same process with these two executables, the original, unpatched Rayman 2 executable, and the new, patched GOG executable. Since it’s patched, is very likely that we won’t found any matches for a GetWindowsDirectoryA search. Here’s when the original executable comes to play: we can search for the function over there, then search its address in the patched executable. When search for the function, something familiar can be seen:


Can you spot the difference?


Jokes aside, is literally the same code. Now, when we search it in the patched executable, the function looks like this:


Nice NOPs.


So, turns out that leaving the string empty is the key. The game searches for the file in the working directory, so no need to use GetCurrentDirectoryA. Some values are adjusted so the registers and stack don’t fuck up and everything keeps the flow.

Okay, let’s try something stupid (I’m starting to see a pattern over here). Let’s replace the Tonic Trouble executable functions with the block of MOV and NOPs instructions:


Surprise!


It wasn’t a very stupid idea after all. As Process Monitor shows, the file is being correctly read from the game directory, and now everything is working as expected. Also, the same applies to the setup program, which shares the same code. Patching this file is also important.

This finishes this first part, so now we can move to the second feature to implement, the widescreen patch.


Wide that beautiful view

This feature is rather simple to implement, since it follows a common and well-known practice in video game hacking. To put it simple, the game outputs on the screen at a defined resolution, and determines the visual range in the boundaries of that screen resolution with a field-of-view (FOV) value. There’re a series of common hex values that can be found in the game binary, and by modifying those, we can change the game options.

In this case, we’re going to modify the resolution option, with the integer value of 800x600 (640x480, while ideal since is the default resolution of the game, crashes if its value is modified), and the FOV, with the float value of 1. Remember that we’re always working on hexadecimal, so all those values should be converted respectively.

Before any modification, let me explain the process. First we change the resolution values. This is to get a proper vertical image (Ver-) display, but in turn, the horizontal gets stretched (depending on the game, it will stretch the image or adjust it). That’s when the FOV comes in place. After treating the resolution with some math, we can get the proper FOV and horizontal image value (Hor+), for then get a correct aspect ratio and image display.

This process is very easy, we just find the offsets for those formatted values and modify them:


  • 800 (int/2 bytes) (0x320, 20 03 formatted) @0x11235
  • 600 (int/2 bytes) (0x258, 58 02 formatted) @0x1123B
  • 1 (float/4 bytes) (0x3F800000, 00 00 80 3F formatted) @0xD0B50



Changing the resolution values is straightforward, but the FOV is quite more complex:


Press to show the code
double FovHorizontal = Math.Round((2 * Math.Atan(((double)ScreenWidth / ScreenHeight) / ((double)4 / 3) * Math.Tan((double)1 / 2 * (Math.PI / 180)))) * (180 / Math.PI) * Math.Pow(10, 2)) / Math.Pow(10, 2);

double FovVertical = Math.Round((2 * Math.Atan(Math.Tan((FovHorizontal * (Math.PI / 180)) / 2) * ((double)ScreenHeight / ScreenWidth))) * (180 / Math.PI) * Math.Pow(10, 2)) / Math.Pow(10, 2);

Implementation in C#.


We first calculate the horizontal FOV, and with that, the vertical FOV. This gives us a decimal value, which then we convert to hex. To understand it more easily, take it as a cross-multiplication: if 1.33 (4/3) is 1 (FOV), then what FOV will 1.78 (16/9) have? Obviously this is oversimplified, and is not used because one formula may not work in all the possible cases.

It’s worth mentioning that some people like to use generic values, and while covering most scenarios, isn’t always the best choice, since it can display incorrectly if an uncommon aspect ratio is used. Also, we want to be the most dynamic we can get.


Ready for some coding

Now that we sorted out the file management and widescreen, it’s time to start working on the program that will reflect all the research and improvements that we have done until now.

The program will be a very simple Windows Forms application, with a main and a settings window. I’ll be using .NET Framework 3.5 to max the compatibility across systems, or at least enough to make it run on a Windows XP machine, since it’ll have features and enhancements that these old systems can also take advantage of.

The only parts worth showing might be the file packaging and patching, so I’ll center on those.

The first is designed very simple, with 3 bundled binaries: one for the app settings (App.bin), other with the game executables (Data.bin), and the last one with the video wrapper files (Video.bin), knowing each of the offsets and lengths of the files to extract them at runtime, depending on the options selected:


Press to show the code
// Prepare the program configuration.
public static void InitialSetup() {
  SettingsName = Path.GetFileNameWithoutExtension(Default.GetType().Assembly.Location)+ ".bin";
  if (!File.Exists(SettingsName))
    Launcher.ExtractFileFromResources("TTLauncher.Files.App.bin", SettingsName, 0, 16);
  SettingsLoaded = File.ReadAllBytes(SettingsName);
}

private static void UnpackFiles() {
  ExtractFileFromResources("TTLauncher.Files.Data.bin", "TonicTrouble.exe", 16, 967168);
  ExtractFileFromResources("TTLauncher.Files.Data.bin", "SetupTT.exe", 967200, 95744);
  UnpackedFiles.Add("TonicTrouble.exe");
  UnpackedFiles.Add("SetupTT.exe");
}


Then we have the patches, in the form of byte arrays (byte[]):


Press to show the code
private static byte[] VideosPath = new byte[] {
  0x56, 0x69, 0x64, 0x65, 0x6F, 0x73, 0x00, 0x00
};
private static byte[] ConfigurationFile = new byte[] {
  0x2E, 0x69, 0x6E, 0x69, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static byte[] ConfigurationPath1 = new byte[] {
  0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90
};
private static byte[] ConfigurationPath2 = new byte[] {
  0x8D, 0x84, 0x24, 0x00, 0x01, 0x00, 0x00, 0x53,
  0x56, 0x57, 0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBF, 0x80,
  0x83, 0x4D, 0x00, 0x83, 0xC9, 0xFF, 0x33, 0xC0
};


And some functions to apply them:


Press to show the code
public static void PatcherPortable(string FileNameInput) {
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = 0xD5ACC;
  BinWrite.Write(VideosPath);
  BinWrite.BaseStream.Position = 0xD5B84;
  BinWrite.Write(ConfigurationFile);
  BinWrite.BaseStream.Position = 0xFDD0;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.BaseStream.Position = 0x12D4D;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.BaseStream.Position = 0x41AA6;
  BinWrite.Write(ConfigurationPath2);
  BinWrite.Close();
}

public static void PatcherSetup(string FileNameInput) {
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = 0x62E8;
  BinWrite.Write(ConfigurationFile);
  BinWrite.BaseStream.Position = 0x42D;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.BaseStream.Position = 0x5C0;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.Close();
}


Also a bleacher function, since we need to fill some parts with empty bytes:


Press to show the code
public static void PatcherBleacher(string FileNameInput, int Offset, int Lenght) {
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = Offset;
  for (int i = 0; i < Lenght; i++) {
    BinWrite.Write((byte)0x00);
  } BinWrite.Close();
}


Then again, depending on the settings, those’ll be called like this:


Press to show the code
// Patch the executable.
Patches.PatcherPortable("TonicTrouble.exe");
// Patch the setup.
Patches.PatcherSetup("SetupTT.exe");


// Make some initial patches, regardless of the configuration set.
private static void InitialPatching() {
  Patches.PatcherBleacher("TonicTrouble.exe", 0xD28A8, 11);
  Patches.PatcherBleacher("TonicTrouble.exe", 0xD28EC, 9);
  Patches.PatcherBleacher("SetupTT.exe", 0x69C4, 9);
}


Finally, we run the game. Note the use of the -cd-rom: parameter, which will be activated if the portable mode option is selected, and will tell the game the CD drive location. Leaving it empty uses the current directory:


Press to show the code
private static void RunGame() {
  Process TonicTrouble = new Process();
  TonicTrouble.StartInfo.FileName = "TonicTrouble.exe";
  if (SettingsLoaded[2] == 0x01)
    TonicTrouble.StartInfo.Arguments = "-cdrom:";
  TonicTrouble.Start();
  CheckGame();
}

private static void CheckGame() {
  System.Threading.Thread.Sleep(1000);
  Process[] CurrentProcess = Process.GetProcessesByName("TonicTrouble");
  foreach (Process Process in CurrentProcess)
    if (Process.ProcessName == "TonicTrouble")
      Process.WaitForExit();
}


If life gives you tonics, do… ugh…

At the end, the application ended up like this:



Mind that this is temporal and incomplete. The version combo is just a placeholder; the plan is to support the other versions as well, but that will require to do everything again, most likely. A fun task, indeed.


Update 06/07/20: Well, it wasn’t as bad as I tought, and it was a nice opportunity to do some cleaning and polishing.


Finishing it up

As expected, it required quite some refactoring, since now we have two different parts that will work with the same code, separated into the protected/packed versions and the unprotected/unpacked versions. The first group have been covered already, but we need to adjust everything to also work with the others.

To determine this, first we need a way to identify the executable version, for which I tracked all the releases of the game out in the wild, and to my surprise, most of them used the same executable, even the same data. In code, we just calculate the hash (I chose the SHA-256 algorithm) and compare with a list:


Press to show the code
private static string CalculateCheckSum(string FileName) {
  using (Stream Executable = File.OpenRead(FileName)) {
    byte[] CheckSum = SHA256.Create().ComputeHash(Executable);
    string Formatted = BitConverter.ToString(CheckSum).Replace("-", String.Empty);
    return Formatted; // Then we check for the string on the list.
  }
}

public static List<string> VersionCheckSum = new List<string> {
  // REVIEW ENGLISH : TT221099-PC - SPANISH - PROTECTED
  "17457C330D11621474A50B2852618D7DF53468C24899176CB2AFC951B51518FB",
  // REVIEW ENGLISH : TT221099-PC - ITALIAN - PROTECTED
  "C379FFADD35C738C0041CEB9916CCA31C35C6EC096B1B3F257F2C66000F0BBFA",
  // RETAIL MASTER V5 : TT181099-PC - FRENCH - UNPROTECTED
  "6AD8506714FC86856369FFE834BB22792AEBCD0FF4FAB780E03AA0ADB47643B3",
  // RETAIL MASTER GERMAN V3: TT221099-PC - GERMAN - PROTECTED
  "EA4BE88CEB9BB7C438BEE7F97767CF12C0F9439707A2CDAEE362FFA01C88FDA6",
  // RETAIL MASTER V3 : TT131099-PC - BRAZILIAN|CHINESE|EUROPE|USA - PROTECTED
  "37631F2FE37C07DD4CCDE32C0981685E152AC016920BEB01CCC0E8FC0E53DC57"
};


Now we know exactly what needs to be supported. After a simple test, the program already works with all the protected versions, so we can forget about those (for now), but it doesn’t with the unprotected, french version. Just for this one most functions and structures needed to be changed, since patches aren’t the same neither.

Now patches look like this:


Press to show the code
private static byte[] VideosPath = new byte[] {
  0x56, 0x69, 0x64, 0x65, 0x6F, 0x73, 0x00, 0x00
};
private static byte[] ConfigurationFile = new byte[] {
  0x2E, 0x69, 0x6E, 0x69, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static byte[] ConfigurationPathProtected1 = new byte[] {
  0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90
};
private static byte[] ConfigurationPathProtected2 = new byte[] {
  0x8D, 0x84, 0x24, 0x00, 0x01, 0x00, 0x00, 0x53,
  0x56, 0x57, 0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBF, 0x80,
  0x83, 0x4D, 0x00, 0x83, 0xC9, 0xFF, 0x33, 0xC0
};
private static byte[] ConfigurationPathUnProtected1 = new byte[] {
  0x90, 0x90, 0x90, 0x90, 0x90, 0x8D,
  0x85, 0xFC, 0xFD, 0xFF, 0xFF, 0x66,
  0xC7, 0x00, 0x2E, 0x00, 0x90, 0x90
};
private static byte[] ConfigurationPathUnProtected2 = new byte[] {
  0x8D, 0x84, 0x24, 0x00, 0x01, 0x00, 0x00, 0x53,
  0x56, 0x57, 0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBF, 0x80,
  0x73, 0x4D, 0x00, 0x83, 0xC9, 0xFF, 0x33, 0xC0
};


And functions now admit addresses as parameters:


Press to show the code
public static void PatcherPortable(string FileNameInput, int Address1, int Address2, int Address3, int Address4, int Address5) {
  byte[] ConfigurationPath = SetConfiguration(2);
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = Address1;
  BinWrite.Write(VideosPath);
  BinWrite.BaseStream.Position = Address2;
  BinWrite.Write(ConfigurationFile);
  BinWrite.BaseStream.Position = Address3;
  BinWrite.Write(ConfigurationPathProtected1);
  BinWrite.BaseStream.Position = Address4;
  BinWrite.Write(ConfigurationPathProtected1);
  BinWrite.BaseStream.Position = Address5;
  BinWrite.Write(ConfigurationPath);
  BinWrite.Close();
}

// Set the correct patch to apply depending on the protection level.
private static byte[] SetConfiguration(int Configuration) {
  if (Launcher.IsProtected())
    if (Configuration == 1)
      return ConfigurationPathProtected1;
    else
      return ConfigurationPathProtected2;
  else
    if (Configuration == 1)
      return ConfigurationPathUnProtected1;
    else
      return ConfigurationPathUnProtected2;
}


Those are called like this:


Press to show the code
// Patch the game and setup executables.
private static void PatchExecutables() {
  // For unprotected versions.
  if (SettingsLoaded[0] == 0x02) {
    Patches.PatcherPortable(ExeGame, 0xD56CC, 0xD5784, 0xFCF0, 0x12C5D, 0x40EF6);
    Patches.PatcherSetup(ExeSetup, 0x70E8, 0x439, 0x5D9);
  }
  // For protected versions.
  else {
    Patches.PatcherPortable(ExeGame, 0xD5ACC, 0xD5B84, 0xFDD0, 0x12D4D, 0x41AA6);
    Patches.PatcherSetup(ExeSetup, 0x62E8, 0x42D, 0x5C0);
  }
}


With this now everything is working as expected. The game version is detected and the correct patches are applied. But we’re not done yet, since this whole thing made me realize of a detail that I missed at some point.

The intro video is something special. If it’s not detected, it just doesn’t play, and the game continues normally. Because of this, there’s the video path patch, but that’s not enough. Each version has a different language, and there’s one folder that’s named before it (English, French, German, Italian and Spanish). If the folder has not the correct name, the intro (and some sounds) won’t play neither. It was a pretty lame oversight, since I tested the video patch with the english version and everything worked fine (English is the default value), but later moved to the italian version and didn’t notice that the video didn’t play at all. After inspecting the executable all of them have the same English directory, but changing it didn’t do anything, so the configuration has to be somewhere else. It turns out I was missing the entry Language in the Ubi.ini file, which overrides the source path with the one specified, so it was actually important to include (there’re a lot of useless settings written in the Ubi.ini file, so unless necessary I just skip its inclusion).


Press to show the code
if (!File.Exists("Ubi.ini"))
  using (StreamWriter Stream = File.CreateText("Ubi.ini")) {
    Stream.WriteLine("[TONICT]");
    // Needed for the game and setup executables.
    Stream.WriteLine("Directory=");
    // Necessary to play the intro video and some sounds.
    Stream.WriteLine("Language=" + CheckSum.VersionLanguage[SettingsLoaded[0]]);
  }

public static List<string> VersionLanguage = new List<string> {
  "Spanish",
  "Italian",
  "French",
  "German",
  "English"
};


And now, this time for real, we’re pretty much done. Every feature has been tested with every version, and everything is working and looking nice and good.



The launcher (TTLauncher) is available on GitHub. After this I’ll re-think about supporting the Special Edition…

Regresando a la vida moderna – Tonic Trouble

Introducción

Algunos títulos viejos suelen ser bastante problemáticos para hacerlos funcionar en sistemas modernos, y Rayman 2 Tonic Trouble no es la excepción. Ya sea por el mismo programa o alguna de sus dependencias, siempre hay algo que les impide funcionar, y generalmente es trabajo de la comunidad (o simplemente gente interesada) regresarlo de la oscuridad hacia la vida tecnológica moderna.


Antecedentes

Este es el caso del comúnmente olvidado (y siempre en la sombra de su proyecto hermano, Rayman 2) Tonic Trouble. Es importante mencionar que el lanzamiento de este título fue un poco especial, con dos versiones diferentes publicadas: una completa (Retail), y otra sin terminar (Edición Especial). Como Ubisoft jodió el lanzamiento en Europa, solo nos vamos a enfocar en la versión de lanzamiento intencionada, la Retail, aunque más adelante es posible que se aplique el mismo tratamiento a la Edición Especial, ya que parece que a mucha gente le gusta más esa.

A su vez, la versión Retail tuvo varios lanzamientos diferentes. Esto tiene que ver con las publicaciones en distintos países, donde cada uno cuenta con una versión diferente dependiendo de su fecha de lanzamiento. Aunque no se exactamente las diferencias entre ellas, puedo suponer que fueron arreglos menores y nada mayor. Si bien el plan es cubrir todas las versiones, necesitamos comenzar por algún lugar, así que vamos a enfocarnos el la última lanzada, la Review English (revisión inglesa), lanzada en Italia y España, por supuesto. Más adelante le haría justicia a su nombre y formaría parte de varios relanzamientos por toda Europa.


Nuestro objetivo

Antes de comenzar, es necesario explicar cuál es nuestro objetivo. No hay ninguna ciencia en usar un wrapper y presto, el programa funciona perfectamente (bueno, no exactamente, ya veremos por qué). Sin embargo, este es un intento de facilitar y automatizar el proceso de instalación y configuración, principalmente para el usuario promedio que solo quiere jugar sin ningún problema. El objetivo es producir un programa todo-en-uno (TEU/AIO) que se encargue de, bueno, todo (un cargador, si se quiere), incluyendo lo más directo como el wrapper gráfico ya configurado, al igual que la parte más técnica, que va a ser explicada detalladamente y por lo tanto el eje principal del artículo.

El contenido va a incluir análisis e ingeniería inversa de código x86 simple y básico. Pero como principiante en todo esto, creo que puede ser de mayor interés para aquellos en una situación similar, o que creen que este tipo de cosas son muy complicadas. Una vez que comprendés el funcionamiento básico del lenguaje ensamblador y te familiarizás con las herramientas y el software necesarios, es bastante sencillo (aunque no siempre) y lógico de trabajar. Así que si algo (tal vez el artículo entero) parece estúpido o innecesario de explicar, mantené este aspecto en cuenta. Dicho esto, empecemos.


Analizando la situación y preparando un entorno de trabajo

La versión Review English es bastante peculiar, ya que a diferencia de las otras, por algún motivo, tiene el ejecutable empaquetado. Entonces simplemente lo desempaquetamos, ¿verdad?. Por supuesto que no, sino no estaríamos hablando de esto. Al analizar el ejecutable podemos ver claramente que está empaquetado, ya que el bloque desde la PE Header (cabecera) hasta la Sections Table (tabla de secciones) está llena de información corrupta:


Como se ve (izquierda) y como se debería ver una sección normal (derecha).


Ahora que sabemos esto, vamos a desempaquetarlo:


Carajo.


Genial, no sabemos con qué fue empaquetado. Y me dirás, ‘bueno, tampoco es para tanto’, pero el problema es que sea lo que sea que estén usando para desempaquetar, no funciona en nada más nuevo que Windows XP, provocando que el programa se cuelgue durante el proceso de desempaquetamiento en sistemas modernos. Además, esto no nos permitiría descompilar y analizar estáticamente el programa.

La única solución que se me ocurre a estas alturas es a la fuerza bruta, volcando la memoria. Después de horas depurando e investigando (tengamos en cuenta que soy un principiante cuando hablamos de retroingeniería, ni que hablar de saber sobre el formato PE y reconstruir un ejecutable) en una máquina con Windows XP real (no máquinas virtuales, ya que las GPUs virtuales no cumplen con los requerimientos del programa), me rendí. Si bien los datos estaban ahí, quedaban importaciones sin reconstruir, dejando el ejecutable obsoleto, y no tengo la experiencia suficiente como para arreglarlo manualmente.

Para mi suerte, la comunidad que mencioné al comienzo vino al rescate. En la Rayman Pirate-Community (es un juego de palabras de Rayman 2, no tiene que ver con piratería), el usuario RibShark ya lo había conseguido, compartiendo el ejecutable desempaquetado. Como comenté anteriormente, esto fue hecho manualmente, así que una mención especial por haberlo conseguido.

Ahora que ya tenemos el ejecutable desprotegido, y la ayuda temporal de un wrapper de video, estamos listos para comenzar el trabajo.


Mejorando el ejecutable

Con todo listo y corriendo, ya podemos empezar a añadir nuevas funcionalidades desde un punto de vista más técnico, ya que si bien el juego ya funciona tal y como está, aún tiene algunas complicaciones. Primero el programa necesita algunos archivos para comenzar, los cuales son generados por el programa de configuración incluído, el cual a su vez necesita más archivos creados durante la instalación desde el CD.

Tenemos que tener en cuenta que este juego es del año 1999, y por aquel entonces un montón de software usaba la carpeta Windows para guardar la mayoría de su configuración y demás información. Este caso no es una excepción, y el programa almacena la configuración en Windows/UbiSoft (¿te acordás cuando su nombre se EscribíaAsí?). Una de nuestras tareas es cambiar eso, y que se use la carpeta de raíz en su lugar. Esto va a permitir que el programa sea completamente portable, además de evitar tener que instalarlo de manera tradicional. Adicionalmente, se implementarán otras mejoras de calidad de vida, como eliminar el sistema de registros y redirigir algunos directorios, haciendo así la necesidad del CD obsoleta.

Para el apartado de gráficos se va a incluír un wrapper (el increíble dgVoodoo), para poder aumentar la resolución a la pantalla. Además, se añadirá un parche de panalla panorámica. Todo esto va a ser implementado de forma dinámica, así nada tiene que ser configurado manualmente, solo es cuestión de activar o desactivar cada opción.

Ahora que tenemos una idea de lo que queremos hacer y como va a funcionar todo, ya podemos comenzar con la parte entretenida.


Ensuciándonos las manos

Bien, ahora este paso va a requerir un poco de descompilación y depuración. No es necesario tener las dos, pero me gusta poder ver las cosas desde ambas perspectivas. Aún así, siempre me encuentro usando más la descompilación para analizar el código estáticamente, y editores hexadecimales para probar los parches. Sé que no es la manera más conveniente, pero me siento cómodo trabajando así, además de que son cosas dentro de todo sencillas.

Para esta tarea vamos a usar Ghidra para descompilar y x64dbg (en realidad x32dbg) para depurar, además de un editor hexadecimal (HxD) y Process Monitor, fundamentales para nuestro éxito.

Lo más lógico en probar primero es ver si podemos modificar los valores que queremos directamente desde el archivo binario con el editor hexadecimal, almacenados como cadenas de texto. Pero parece que solo es posible cambiar nombres de archivos, no directorios. Esto nos permite cambiar ciertas cosas, como los registros y otros archivos, pero no las rutas de la configuración.

Ahora empezamos directamente analizando el ejecutable con Ghidra. Estamos intentando redirigir las operaciones de archivos en la carpeta Windows (hablando de la configuración, en este caso el archivo UbiSoft/Ubi.ini) hacia la carpeta de raíz del programa, así que una de las cosas que me vienen a la cabeza es buscar por llamadas a la función GetWindowsDirectoryA, y encontrar una manera de cambiarlo a GetCurrentDirectoryA. Después de una búsqueda rápida se encontraron 4 resultados, todos con una estructura similar:



Podemos ver la cadena de texto UbiSoft/Ubi.ini, también presente en el archivo binario. Esto significa que encontramos lo que buscábamos, repitiéndose en las cuatro llamadas, todas al comienzo de sus respectivas funciones. Ahora tenemos que ver que hacemos con esto.

Al principio, fracasando en el intento, probé en cambiarlas a llamadas de GetCurrentDirectoryA. Por la lógica de la función, debería haber funcionado, pero resulta que las computadoras son mas inteligentes que yo. La cadena se cortaba por algún motivo, comprobado con x32dbg y Process Monitor. Después de más intentos inútiles, me dí cuenta de algo muy importante.

¿Te acordás cuando al principio mencioné a Rayman 2 como un proyecto hermano? Resulta que Rayman 2 usa el mismo motor base que Tonic Trouble, aunque mucho más pulido. La mejor parte es que se comporta exactamente igual que Tonic Trouble en cuestión de manejo de archivos, lo que significa que también almacena sus datos en la carpeta Windows. Esto no sería muy relevante si Rayman 2 no fuera un juego popular, pero ese no es el caso. En 2011, Ubisoft relanzó Rayman 2 en GOG, con unos cuantos arreglos en el camino para que funcione apropiadamente en sistemas modernos al momento. Uno de esos arreglos es lo que nos reúne el día de hoy, redirigir los archivos de la carpeta Windows hacia la carpeta raíz.

Con esta información, ahora vamos a hacer el mismo proceso con estos dos ejecutables, el original de Rayman 2 sin parchear, y el ya parcheado de GOG. Como el arreglo ya esta parcheado, podemos asumir que no vamos a encontrar resultados para GetWindowsDirectoryA. Ahora es cuando el original entra en juego: buscamos la función ahí y después buscamos su dirección en el ejecutable parcheado. Cuando buscamos la función podemos ver algo familiar:


¿Podés encontrar la diferencia?


Hablando en serio, es literalmente el mismo código. Ahora cuando buscamos la dirección en el ejecutable parcheado, la función se ve así:


Unos buenos NOPs.


Resulta que dejando la cadena de texto vacía es la clave. El programa busca los archivos en el directorio raíz, así que no hay necesidad de usar GetCurrentDirectoryA. Se ajustan algunos valores para que los registros y el stack no se jodan y todo siga en funcionamiento.

Bien, ahora vamos a probar algo estúpido (estoy empezando a ver un patrón). Vamos a reemplazar las funciones en el ejecutable de Tonic Trouble con el bloque de instrucciones con MOV y NOPs:


¡Sorpresa!


Al final no era una muy mala idea. Como muestra Process Monitor, el archivo está siendo leído correctamente desde el directorio raíz, y ahora todo funciona correctamente. Además, pasa lo mismo con el programa de configuración, el cual comparte el mismo código. Parchear este archivo también es importante.

Con esto terminamos esta primera parte, así que ahora podemos avanzar a la segunda funcionalidad a implementar, el parche de pantalla panorámica.


Extendamos esa vista hermosa

Esta funcionalidad es bastante sencilla de implementar, ya que sigue una técnica muy común y conocida en hackeo de videojuegos. Para explicarlo de manera simple, el programa muestra la imagen en pantalla en una resolución definida, y determina el rango visual dentro del límite de esa resolución con un valor de campo de visión (field-of-view o FOV). Existen una serie de valores hexadecimales que se pueden encontrar en el ejecutable, que al modificarlos, podemos cambiar la configuración del programa.

En este caso, vamos a modificar la opción de resolución, con un valor entero de 800x600 (si bien 640x480 sería el ideal ya que es el valor por defecto, el programa se cuelga si este valor es modificado), y el FOV con un valor de punto flotante 1. Recordemos que siempre trabajamos en hexadecimal, así que estos valores deben ser convertidos respectivamente.

Antes de cualquier modificación veamos como funciona el proceso. Primero cambiamos los valores de la resolución. Con esto obtenemos una salida de imagen vertical (Ver-) correcta, pero en consecuencia, la horizontal se estira (dependiendo del juego se puede estirar o ajustar adecuadamente). Ahí es cuando el campo de visión entra en juego. Después de someter la resolución a unos cálculos matemáticos, obtenemos un valor de FOV e imagen horizontal (Hor+) correctos, para así conseguir una relación de aspecto e imagen de salida correctas.

Este proceso es muy sencillo, solamente encontramos las direcciones de estos valores formateados y los modificamos:


  • 800 (int/2 bytes) (0x320, 20 03 formateado) @0x11235
  • 600 (int/2 bytes) (0x258, 58 02 formateado) @0x1123B
  • 1 (float/4 bytes) (0x3F800000, 00 00 80 3F formateado) @0xD0B50



Cambiar los valores de la resolución es bastante directo, pero el FOV es un poco más complejo:


Presiona para mostrar el código
double FovHorizontal = Math.Round((2 * Math.Atan(((double)ScreenWidth / ScreenHeight) / ((double)4 / 3) * Math.Tan((double)1 / 2 * (Math.PI / 180)))) * (180 / Math.PI) * Math.Pow(10, 2)) / Math.Pow(10, 2);

double FovVertical = Math.Round((2 * Math.Atan(Math.Tan((FovHorizontal * (Math.PI / 180)) / 2) * ((double)ScreenHeight / ScreenWidth))) * (180 / Math.PI) * Math.Pow(10, 2)) / Math.Pow(10, 2);

Implementación en C#.


Primero calculamos el FOV horizontal, y con eso, el FOV vertical. Esto nos da un valor decimal, el cual convertimos a hexadecimal. Para entenderlo más fácil, mirémoslo como una regla de tres: si 1.33 (4/3) es 1 (FOV), entonces ¿qué FOV va a tener 1.78 (16/9)? Obviamente estamos simplificando, y no usamos esto porque una fórmula puede no funcionar para todos los casos.

Cabe aclarar que mucha gente suele usar valores generales, y si bien cubren la mayoría de los escenarios, no siempre es preciso hacerlo, ya que puede mostrar la imagen incorrectamente si se usa una relación de aspecto poco común. Además, queremos ser lo más dinámico posible.


Listo para un poco de código

Ahora que tenemos los temas del manejo de archivos y la pantalla panorámica resueltos, es momento de comenzar a trabajar en el programa que va a reflejar toda la investigación y mejoras que hicimos hasta ahora.

El programa va a ser una aplicación de Windows Forms muy sencilla, con una ventana principal y otra de opciones. Voy a usar .NET Framework 3.5 para maximizar la compatibilidad entre sistemas, o al menos conseguir que el programa corra en máquinas con Windows XP, ya que va a tener mejoras que inclusive estos viejos sistemas pueden aprovechar.

Las partes más interesantes puede que sean el empaquetado de los archivos y los parches, así que me voy a centrar solo en eso.

El primero es simple, con 3 archivos binarios empaquetados: uno para las opciones de la aplicación (App.bin), otro con los ejecutables del juego (Data.bin), y el último con los archivos del wrapper de video (Video.bin), sabiendo el punto de comienzo y longitud de cada archivo para extraerlos durante la ejecución dependiendo de las opciones seleccionadas:


Presiona para mostrar el código
// Preparar la configuración del programa.
public static void InitialSetup() {
  SettingsName = Path.GetFileNameWithoutExtension(Default.GetType().Assembly.Location)+ ".bin";
  if (!File.Exists(SettingsName))
    Launcher.ExtractFileFromResources("TTLauncher.Files.App.bin", SettingsName, 0, 16);
  SettingsLoaded = File.ReadAllBytes(SettingsName);
}

private static void UnpackFiles() {
  ExtractFileFromResources("TTLauncher.Files.Data.bin", "TonicTrouble.exe", 16, 967168);
  ExtractFileFromResources("TTLauncher.Files.Data.bin", "SetupTT.exe", 967200, 95744);
  UnpackedFiles.Add("TonicTrouble.exe");
  UnpackedFiles.Add("SetupTT.exe");
}


Después tenemos los parches, en forma de arrays de bytes (byte[]):


Presiona para mostrar el código
private static byte[] VideosPath = new byte[] {
  0x56, 0x69, 0x64, 0x65, 0x6F, 0x73, 0x00, 0x00
};
private static byte[] ConfigurationFile = new byte[] {
  0x2E, 0x69, 0x6E, 0x69, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static byte[] ConfigurationPath1 = new byte[] {
  0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90
};
private static byte[] ConfigurationPath2 = new byte[] {
  0x8D, 0x84, 0x24, 0x00, 0x01, 0x00, 0x00, 0x53,
  0x56, 0x57, 0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBF, 0x80,
  0x83, 0x4D, 0x00, 0x83, 0xC9, 0xFF, 0x33, 0xC0
};


Y algunas funciones para aplicarlos:


Presiona para mostrar el código
public static void PatcherPortable(string FileNameInput) {
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = 0xD5ACC;
  BinWrite.Write(VideosPath);
  BinWrite.BaseStream.Position = 0xD5B84;
  BinWrite.Write(ConfigurationFile);
  BinWrite.BaseStream.Position = 0xFDD0;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.BaseStream.Position = 0x12D4D;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.BaseStream.Position = 0x41AA6;
  BinWrite.Write(ConfigurationPath2);
  BinWrite.Close();
}

public static void PatcherSetup(string FileNameInput) {
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = 0x62E8;
  BinWrite.Write(ConfigurationFile);
  BinWrite.BaseStream.Position = 0x42D;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.BaseStream.Position = 0x5C0;
  BinWrite.Write(ConfigurationPath1);
  BinWrite.Close();
}


También un blanqueador, ya que necesitamos llenar algunas partes con bytes vacíos:


Presiona para mostrar el código
public static void PatcherBleacher(string FileNameInput, int Offset, int Lenght) {
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = Offset;
  for (int i = 0; i < Lenght; i++) {
    BinWrite.Write((byte)0x00);
  } BinWrite.Close();
}


Y de nuevo, dependiendo de las opciones, estos van a ser llamados así:


Presiona para mostrar el código
// Parchear el ejecutable.
Patches.PatcherPortable("TonicTrouble.exe");
// Parchear el configurador.
Patches.PatcherSetup("SetupTT.exe");


// Aplicar algunos parches iniciales, sin importar la configuración.
private static void InitialPatching() {
  Patches.PatcherBleacher("TonicTrouble.exe", 0xD28A8, 11);
  Patches.PatcherBleacher("TonicTrouble.exe", 0xD28EC, 9);
  Patches.PatcherBleacher("SetupTT.exe", 0x69C4, 9);
}


Finalmente, ejecutamos el programa. Importante el uso del parámetro -cd-rom:, el cual va a activarse si el modo portable está seleccionado, y le indica al juego la ubicación del CD. Si lo dejamos vacío entonces va a usar el directorio actual:


Presiona para mostrar el código
private static void RunGame() {
  Process TonicTrouble = new Process();
  TonicTrouble.StartInfo.FileName = "TonicTrouble.exe";
  if (SettingsLoaded[2] == 0x01)
    TonicTrouble.StartInfo.Arguments = "-cdrom:";
  TonicTrouble.Start();
  CheckGame();
}

private static void CheckGame() {
  System.Threading.Thread.Sleep(1000);
  Process[] CurrentProcess = Process.GetProcessesByName("TonicTrouble");
  foreach (Process Process in CurrentProcess)
    if (Process.ProcessName == "TonicTrouble")
      Process.WaitForExit();
}


Si la vida te da tónicos, hace… ehh…

Al final, la aplicación quedo así:



Hay que tener en consideración que esto es temporal e incompleto. El texto de versión es solo de muestra; el plan es soportar otras versiones por igual, pero eso probablemente requiera hacer todo de nuevo.


Actualización 06/07/20: Bueno, no fue tan malo como pensé, y fue una buena oportunidad para hacer algo de limpieza y pulido.


Terminando lo empezado

Como era de esperar, fue necesario refactorizar, ya que ahora tenemos dos partes diferentes que van a utilizar el mismo código, separadas en las versiones protegidas/empaquetadas y las versiones desprotegidas/desempaquetadas. Ya hablamos de las primeras antes, pero ahora tenemos que ajustar todo para que funcionen las otras.

Para determinar esto, primero necesitamos una forma de identifica la versión del ejecutable, para lo que me tomé el trabajo de buscar todos los lanzamientos disponibles, y para mi sorpresa, muchos de ellos usan los mismos ejecutables, e inclusive los mismos archivos de datos. En el código, solamente calculamos el hash (elegí el algoritmo SHA-256) y lo comparamos con una lista:


Presiona para mostrar el código
private static string CalculateCheckSum(string FileName) {
  using (Stream Executable = File.OpenRead(FileName)) {
    byte[] CheckSum = SHA256.Create().ComputeHash(Executable);
    string Formatted = BitConverter.ToString(CheckSum).Replace("-", String.Empty);
    return Formatted; // Después chequeamos por el texto en la lista.
  }
}

public static List<string> VersionCheckSum = new List<string> {
  // REVIEW ENGLISH : TT221099-PC - SPANISH - PROTECTED
  "17457C330D11621474A50B2852618D7DF53468C24899176CB2AFC951B51518FB",
  // REVIEW ENGLISH : TT221099-PC - ITALIAN - PROTECTED
  "C379FFADD35C738C0041CEB9916CCA31C35C6EC096B1B3F257F2C66000F0BBFA",
  // RETAIL MASTER V5 : TT181099-PC - FRENCH - UNPROTECTED
  "6AD8506714FC86856369FFE834BB22792AEBCD0FF4FAB780E03AA0ADB47643B3",
  // RETAIL MASTER GERMAN V3: TT221099-PC - GERMAN - PROTECTED
  "EA4BE88CEB9BB7C438BEE7F97767CF12C0F9439707A2CDAEE362FFA01C88FDA6",
  // RETAIL MASTER V3 : TT131099-PC - BRAZILIAN|CHINESE|EUROPE|USA - PROTECTED
  "37631F2FE37C07DD4CCDE32C0981685E152AC016920BEB01CCC0E8FC0E53DC57"
};


Ahora sabemos exactamente todo lo que tenemos que soportar. Después de una simple prueba, la aplicación ya funciona con todas las versiones protegidas, así que nos podemos ovidar de ellas (por ahora), pero aún no lo hace con la versión desprotegida francesa. Solo para ésta tenemos que cambiar la mayoría de las estructuras y funciones, ya que los parches no son los mismos.

Después de estos cambios los parches se ven así:


Presiona para mostrar el código
private static byte[] VideosPath = new byte[] {
  0x56, 0x69, 0x64, 0x65, 0x6F, 0x73, 0x00, 0x00
};
private static byte[] ConfigurationFile = new byte[] {
  0x2E, 0x69, 0x6E, 0x69, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static byte[] ConfigurationPathProtected1 = new byte[] {
  0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90
};
private static byte[] ConfigurationPathProtected2 = new byte[] {
  0x8D, 0x84, 0x24, 0x00, 0x01, 0x00, 0x00, 0x53,
  0x56, 0x57, 0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBF, 0x80,
  0x83, 0x4D, 0x00, 0x83, 0xC9, 0xFF, 0x33, 0xC0
};
private static byte[] ConfigurationPathUnProtected1 = new byte[] {
  0x90, 0x90, 0x90, 0x90, 0x90, 0x8D,
  0x85, 0xFC, 0xFD, 0xFF, 0xFF, 0x66,
  0xC7, 0x00, 0x2E, 0x00, 0x90, 0x90
};
private static byte[] ConfigurationPathUnProtected2 = new byte[] {
  0x8D, 0x84, 0x24, 0x00, 0x01, 0x00, 0x00, 0x53,
  0x56, 0x57, 0x66, 0xC7, 0x00, 0x2E, 0x00, 0x90,
  0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBF, 0x80,
  0x73, 0x4D, 0x00, 0x83, 0xC9, 0xFF, 0x33, 0xC0
};


Y las funciones ahora admiten direcciones como parámetros:


Presiona para mostrar el código
public static void PatcherPortable(string FileNameInput, int Address1, int Address2, int Address3, int Address4, int Address5) {
  byte[] ConfigurationPath = SetConfiguration(2);
  BinaryWriter BinWrite = new BinaryWriter(File.Open(FileNameInput, FileMode.Open, FileAccess.ReadWrite));
  BinWrite.BaseStream.Position = Address1;
  BinWrite.Write(VideosPath);
  BinWrite.BaseStream.Position = Address2;
  BinWrite.Write(ConfigurationFile);
  BinWrite.BaseStream.Position = Address3;
  BinWrite.Write(ConfigurationPathProtected1);
  BinWrite.BaseStream.Position = Address4;
  BinWrite.Write(ConfigurationPathProtected1);
  BinWrite.BaseStream.Position = Address5;
  BinWrite.Write(ConfigurationPath);
  BinWrite.Close();
}

// Aplicar el parche indicado dependiendo del nivel de protección.
private static byte[] SetConfiguration(int Configuration) {
  if (Launcher.IsProtected())
    if (Configuration == 1)
      return ConfigurationPathProtected1;
    else
      return ConfigurationPathProtected2;
  else
    if (Configuration == 1)
      return ConfigurationPathUnProtected1;
    else
      return ConfigurationPathUnProtected2;
}


Éstas se llaman así:


Presiona para mostrar el código
// Parchear los ejecutables del juego y configuración.
private static void PatchExecutables() {
  // Para versiones desprotegidas.
  if (SettingsLoaded[0] == 0x02) {
    Patches.PatcherPortable(ExeGame, 0xD56CC, 0xD5784, 0xFCF0, 0x12C5D, 0x40EF6);
    Patches.PatcherSetup(ExeSetup, 0x70E8, 0x439, 0x5D9);
  }
  // Para versiones protegidas.
  else {
    Patches.PatcherPortable(ExeGame, 0xD5ACC, 0xD5B84, 0xFDD0, 0x12D4D, 0x41AA6);
    Patches.PatcherSetup(ExeSetup, 0x62E8, 0x42D, 0x5C0);
  }
}


Ahora con esto todo funciona correctamente. La versión del programa es detectada y los parches correspondientes son aplicados. Pero aún no terminamos, ya que al hacer todo esto me dí cuenta de un detalle que me pasé por alto.

El video de introducción es algo especial. Si no es detectado simplemente no se reproduce, y el juego continúa normalmente. Para eso tenemos el parche de video, pero no es suficiente. Cada versión tiene un idioma diferente, y existe una carpeta nombrada después de éste (Inglés, Francés, Alemán, Italiano y Español, todos en inglés). Si la carpeta no tiene el nombre correcto, la introducción (y algunos sonidos) tampoco se reproducirán. Fue un error bastante estúpido, porque probé el parche de video con la versión inglesa y todo funcionó bien (English es el valor por defecto), pero como después me cambié a la versión italiana no me dí cuenta de que el video no se reproducía. Después de analizar el ejecutable, todas las versiones apuntan al mismo directorio English, pero al cambiarlo no pasa nada, así que la configuración tiene que estar en otro lado. Resulta que me estaba faltando la entrada Language en el archivo Ubi.ini, que sobrescribe el directorio de origen con el especificado, por lo que sí era importante de incluír (hay muchas opciones inútiles en el archivo Ubi.ini, así que no incluyo las que no sean necesarias).


Presiona para mostrar el código
if (!File.Exists("Ubi.ini"))
  using (StreamWriter Stream = File.CreateText("Ubi.ini")) {
    Stream.WriteLine("[TONICT]");
    // Necesario para los ejecutables del juego y configuración.
    Stream.WriteLine("Directory=");
    // Necesario para reproducir el video de introducción y algunos sonidos.
    Stream.WriteLine("Language=" + CheckSum.VersionLanguage[SettingsLoaded[0]]);
  }

public static List<string> VersionLanguage = new List<string> {
  "Spanish",
  "Italian",
  "French",
  "German",
  "English"
};


Y ahora, esta vez enserio, ya terminamos con todo. Todas las funcionalidades fueron probadas con todas las versiones, y todo se vé y funciona muy bien.



El programa (TTLauncher) está disponible en GitHub. Después de todo esto voy a reconsiderar la idea de soportar la Edición Especial…

© Renzo Pigliacampo - 2022