Now Reading
Reverse Engineering Star Wars: Yoda Tales

Reverse Engineering Star Wars: Yoda Tales

2023-01-09 13:11:11

Background

I do not know why, however I’ve at all times gotten a kick out of reverse engineering knowledge information for laptop video games. Though decompiling a sport’s code is a difficult activity, knowledge information are sometimes a lot simpler to determine (as they comprise numerous extremely seen content material like textual content and sprites) and allow you to mod the sport in case you’re capable of determine it out sufficiently.

In 2006, days after the Star Wars: Empire at War demo was launched, I revealed some rudimentary instruments for dumping and repacking the sport’s knowledge information, together with a easy mod that allow you to play because the Empire (which was disabled within the demo). There are barely any traces of it left on the internet, however I managed to attain a free Petroglyph t-shirt on the time so I suppose that is one thing.

A few years earlier than that I owned one other Star Wars sport referred to as Star Wars: Yoda Stories. It seems to have been pretty obscure and poorly obtained, however that did not cease me from taking part in the crap out of it. As a novice sport programmer and die-hard Star Wars fan, I attempted very arduous to find the sport’s assets in order that I may make some form of horrible Star Wars sport of my very own. As an alternative, all I managed to search out had been some sound results and a small variety of sprites that they distributed as icons, as a part of a desktop theme.

Quick ahead sixteen years to me trying by way of my historic CD assortment for some previous video games to play at a 1990’s laptop themed celebration. After popping within the CD I instantly spot what’s clearly a knowledge file, roughly 4 megabytes in measurement, simply ready for me to use my overpriced school diploma and crack it open. Higher late than by no means!

File Construction (Issue: Padawan)

I suppose that the one program it’s worthwhile to reverse engineer one thing like it is a hex editor, though as we’ll see later a decompiler, calculator, and robust working data of the goal program assist as properly. I am an enormous fan of HxD, so we’ll use that.

If you wish to play alongside at house, this is a hyperlink to the sport’s knowledge file: YODESK.DTA

Time to open this file!

Properly, that is undoubtedly not textual content, or actually something remotely meant to be learn by a human, however that is not precisely stunning both. I am positive that sixteen years in the past I opened this exact same file in Notepad and shortly closed it, not remotely understanding what I used to be taking a look at.

Proper off the bat we will see some four-letter ASCII symbols, which look to me like part identifiers. Scrolling additional forward appears to substantiate this: SNDS, TILE, CHAR, PUZ2, and plenty of extra. The file ends with an ENDF part, which suggests that the general file construction is a few sort of hierarchy or listing of tagged sections.

The VERS identifier clearly begins a “model” part, which accommodates the next 4 bytes: 0x00, 0x02, 0x00, 0x00. My guess is that that is model 2.0 of the file format, as Yoda Tales was truly the successor to an Indiana Jones sport that seems to make use of the identical engine. It does not matter a lot, although, as this is not a really fascinating piece of information.

Subsequent up is the STUP (setup?) part, which accommodates numerous mysterious knowledge:

There’s clearly some sort of sample right here, however even with an intensive data of the sport it is not clear what it is for. The larger query on my thoughts is: how can we skip it? Though it would be doable to only assume it is a fastened size part and skip the information, that is most likely not the case.

If we glance again at first of the part (the earlier screenshot) we’ll see that 4 suspicious bytes comply with the STUP identifier: 0x00, 0x44, 0x01, 0x00. If we measure the remainder of the information within the part after these 4 bytes, we’ll discover that it is precisely 0x00014400 bytes lengthy. A coincidence? I believe not!

These 4 bytes are clearly an unsigned, 32-bit integer that specifies the quantity of information that make up the remainder of the STUP part. If it appears to be like just like the bytes are backward, although, it is as a result of they’re: they’re saved in “little-endian” order, the place the less-significant bytes are saved first, a typical conference for x86 and x86-64 processors. If we learn this size worth, we will then skip the remainder of the part regardless of understanding nothing in regards to the knowledge that’s saved inside.

Manually studying by way of binary information, even one as small as 4MB, is not a really productive approach to make progress, so it is a good time to begin writing a program that parses the file and reads and/or dumps out knowledge as we determine the way it’s encoded. My most well-liked programming language is C#, so I’ll use that; assuming the file format is not completely screwy, I ought to be capable of get numerous mileage about of the BinaryReader class and get a fast begin. Here is this system for what we have discovered to date:

static void Most important(string[] args)
{
	utilizing (BinaryReader binaryReader = new BinaryReader(File.OpenRead("YODESK.DTA")))
	{
		bool keepReading = true;
		whereas (keepReading)
		{
			string part = new string(binaryReader.ReadChars(4));
			swap (part)
			{
				case "VERS":
					uint model = binaryReader.ReadUInt32();
					break;
				case "STUP":
					uint size = binaryReader.ReadUInt32();
					byte[] knowledge = binaryReader.ReadBytes((int)size);
					break;
				default:
					throw new Exception("Unknown part: " + part);
			}
		}
	}
}

Sadly, this solely works for the primary two sections: as quickly as we hit the third part, SNDS, it turns into clear that we have to deal with all of the instances that will likely be thrown at us. This finally ends up being is a fairly widespread side of reverse engineering the file format, as there are a lot of situations of values which might be considered one of many sorts that require us to grasp every doable sort that may very well be encountered. Fortuitously, virtually the entire sections within the file have a 32-bit unsigned size following the part identifier, which suggests we will reuse the code from the STUP part to skip over them.

static void Most important(string[] args)
{
	utilizing (BinaryReader binaryReader = new BinaryReader(File.OpenRead("YODESK.DTA")))
	{
		bool keepReading = true;
		whereas (keepReading)
		{
			string part = new string(binaryReader.ReadChars(4));
			swap (part)
			{
				case "VERS":
					uint model = binaryReader.ReadUInt32();
					break;
				case "STUP":
				case "SNDS":
				case "ZONE":
				case "TILE":
				case "PUZ2":
				case "CHAR":
				case "CHWP":
				case "CAUX":
				case "TNAM":
					uint sectionLength = binaryReader.ReadUInt32();
					byte[] sectionData = binaryReader.ReadBytes((int)sectionLength);
					break;
				case "ENDF":
					keepReading = false;
					break;
			}
		}
	}
}

The ZONE part appears to be like prefer it specifies a size instantly after (0x00010292), however in case you leap ahead that many bytes you will find yourself in the midst of nowhere, which suggests we’re most likely deciphering issues incorrectly. Nevertheless, there are clearly extra identifiers shortly after: IZON, IZAX, IZX2, IZX3, IZX4, and nil to many IACTs after that. This set of identifiers repeats once more after that, and finally ends up repeating many occasions. That is the place a bit of information in regards to the sport turns out to be useful: one of many huge “options” of Yoda Tales is that each time you play it generates a random map, however after taking part in just a few occasions it is obvious that these maps are literally a set of fastened smaller maps hooked up collectively. The truth that the IACT sections comprise textual content that appear like scripts additional reinforces the concept that every IZON truly corresponds to a sub-map:

If we will not trivially skip the ZONE part, it means we’ll have to determine extra about its construction in order that we will parse it and proceed parsing the remainder of the doc. Though I ultimately figured it out, this half was a lot trickier than the sooner sections, as there’s knowledge combined in close to the part identifiers which does not even have something to do with size.

There are various IZON sections, so there’s clearly one thing within the knowledge that helps get from one to the following; though it is doable that this system merely scans ahead for the following “IZON” string, that’d be fairly sketchy and unreliable: who is aware of if you’d need to embody “IZON” in an NPC’s dialogue line! Measuring the size from the start of the primary IZON to the following IZON yields a size of 0x064C, which is suspiciously near the 4 bytes after the 4 bytes that adopted the ZONE identifier (0x00000646). If we deal with these 4 bytes as a size identifier and take a look at the 0x646 bytes that comply with it, we will begin to see a sample emerge:

4 bytes: "ZONE"
2 bytes: ???? 
2 bytes: ????
4 bytes: size (X)
X bytes: zone knowledge
2 bytes: ????
4 bytes: size (X)
X bytes: zone knowledge
2 bytes: ????
4 bytes: size (X)
X bytes: zone knowledge
...
2 bytes: ????
4 bytes: size (X)
X bytes: zone knowledge
4 bytes: "PUZ2" (begin of subsequent part)

Extra investigation reveals extra information, some useful, some much less so:

  • The 2 bytes following the ZONE identifier correspond to an unsigned, 16-bit integer that specifies the variety of zones.
  • The 2 bytes earlier than every zone size specifier solely ever have values 0x0001, 0x0002, 0x0003, and 0x0005.
  • The primary two bytes of the zone knowledge are some form of ID quantity for the map, beginning at 0x0000 and rising by one every time.

If we replace our program to learn the ZONE part utilizing our newly found info, we will now learn by way of all the file.

static void Most important(string[] args)
{
	utilizing (BinaryReader binaryReader = new BinaryReader(File.OpenRead("YODESK.DTA")))
	{
		bool keepReading = true;
		whereas (keepReading)
		{
			string part = new string(binaryReader.ReadChars(4));
			swap (part)
			{
				case "VERS":
					uint model = binaryReader.ReadUInt32();
					break;
				case "STUP":
				case "SNDS":
				case "TILE":
				case "PUZ2":
				case "CHAR":
				case "CHWP":
				case "CAUX":
				case "TNAM":
					uint sectionLength = binaryReader.ReadUInt32();
					byte[] sectionData = binaryReader.ReadBytes((int)sectionLength);
					break;
				case "ZONE":
					ushort depend = binaryReader.ReadUInt16();
					for (int i = 0; i < depend; i++)
					{
						ushort unknown = binaryReader.ReadUInt16();
						uint zoneLength = binaryReader.ReadUInt32();
						byte[] zoneData = binaryReader.ReadBytes((int)zoneLength);
					}
					break;
				case "ENDF":
					keepReading = false;
					break;
				default:
					throw new Exception("Unknown part: " + part);
			}
		}
	}
}

With this code we at the moment are capable of learn by way of all the file, with out hitting any exceptions that might point out we have made a nasty assumption in regards to the total construction of the file format. Though we’re not extracting something helpful but, we’re completely on our means.

Tiles (Issue: Jedi Knight)

Now that we’re parsing the general construction of the file, it is time to dig in and begin dumping what we got here right here for: the sport’s gloriously bizarre however exhaustive tile set.

If we scroll ahead into to the center of the TILE part, it turns into instantly obvious that there is one thing occurring right here: bitmap photos! The sport’s photos are 32×32 pixel sprites, so if a 32-byte column width begins exhibiting patterns like these there is a good probability that it is a non-compressed bitmap picture format with one byte per pixel.

If we leap again to the beginning of the TILE part it is simple to identify a 1024-byte (32×32) run of information that occurs to map into ASCII character area, which is presumably sprite knowledge, prefixed by 4 bytes. If we take a look at the vary of values for these 4 bytes over all the set of information, we will see that they are comparatively random however repeat typically, and look lots like an array of bit flags.

4 bytes: flags
1024 bytes: picture knowledge
4 bytes: flags
1024 bytes: picture knowledge
...
4 bytes: flags
1024 bytes: picture knowledge

Perhaps the bit flags describe properties of the tiles? Who cares! We’re near extracting photos! All we have to do is prolong our program to dump out the presumed picture knowledge into precise picture information and…

case "TILE":
{
	Listing.CreateDirectory(@"Tiles");
	uint tileSectionLength = binaryReader.ReadUInt32();
	for (int i = 0; i < tileSectionLength / 0x404; i++)
	{
		uint unknown = binaryReader.ReadUInt32();
		Bitmap tile = new Bitmap(32, 32);
		for (int j = 0; j < 0x400; j++)
		{
			byte pixelData = binaryReader.ReadByte();
			Shade pixelColor = Shade.FromArgb(pixelData, pixelData, pixelData);
			tile.SetPixel(j % 32, j / 32, pixelColor);
		}
		
		tile.Save(string.Format(@"Tiles{0}.png", i));
	}
	break;
}

Victory! Form of…

The sport is clearly in shade, so merely emitting the pixel worth for every RGB element isn’t going to chop it; we might by no means get something aside from grayscale. Wikipedia says {that a} widespread 8-bit true shade scheme is 0bRRRGGGBB, so let’s strive that as an alternative:

case "TILE":
{
	Listing.CreateDirectory(@"Tiles");
	uint tileSectionLength = binaryReader.ReadUInt32();
	for (int i = 0; i < tileSectionLength / 0x404; i++)
	{
		uint unknown = binaryReader.ReadUInt32();
		Bitmap tile = new Bitmap(32, 32);
		for (int j = 0; j < 0x400; j++)
		{
			byte pixelData = binaryReader.ReadByte();
			byte r = (byte)((pixelData & 0xE0) << 0);
			byte g = (byte)((pixelData & 0x1C) << 3);
			byte b = (byte)((pixelData & 0x03) << 6);
			Shade pixelColor = Shade.FromArgb(r, g, b);
			tile.SetPixel(j % 32, j / 32, pixelColor);
		}
		
		tile.Save(string.Format(@"Tiles{0}.png", i));
	}
	break;
}

That is clearly additionally not the fitting pixel format, so it is time to cease guessing and begin analyzing the numbers immediately. Let’s seize a screenshot from the sport and get some examples of what values map to what colours within the ultimate sprite:

These shades of blue are fascinating: they’re very shut, however have fairly just a few distinct shades. If we seize the pixel values from a picture manipulation program, we get the next values:

0x1F	0x00 / 0x00 / 0x00
0x92	0x0B / 0x53 / 0xFB
0x93	0x00 / 0x00 / 0xFB
0x94	0x00 / 0x00 / 0xCB
0x95	0x00 / 0x00 / 0x9F
0x96	0x00 / 0x00 / 0x6F

I used to be overly optimistic that there was some sort of bit mapping from the pixel worth to the colour parts, however seeing 0x1F by some means map to black (all zeroes) factors strongly on the contrary. If lowering the pixel worth from 0x93 to 0x92 magically provides two bytes value of data to the colour, I believe it is time to resign ourselves to the inevitable: there is a shade palette someplace, and we’ve no clue the place it’s.

Though it could be doable to seize a bunch of screenshots and automate the method of mapping pixel values, there actually should be a greater means; the information for the palette must exist someplace. If I had been programming this I would most likely simply retailer it as an array of colours the place the index into the array is the pixel knowledge worth. On a whim, let’s search the file for one of many colours used within the R2D2 sprite, 0x0B53FB…

Nope, nothing.

BGR is one other widespread pixel format, so possibly 0xFB530B will work?

Nope.

There may be one place we’ve not appeared, although: the sport’s executable. We retailer numerous gameplay info as code constants in our video games, so possibly they did it right here as properly. Trying to find 0x0B53FB within the binary yields no outcomes, however once we seek for the BGR worth…

Candy Jesus, it is a shade palette! The subsequent 4 bytes are 0xFB000000, which is shade 0x93 in BGRA, so there is a good probability we’re onto one thing. If we search for the information in a disassembler, we will glean a bit of bit extra into what’s actually occurring:

It is arduous to inform from this picture, however this is our palette knowledge, proper the place we might look forward to finding a compile-time fixed array in a decompiled program. Figuring out that that is shade 0x92, we will work backward to search out the beginning of the palette and duplicate it into our program in order that we will export photos with the right colours:

non-public static readonly byte[] PaletteData = new byte[] 
{ 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x8B, 0x00, 0xC3, 0xCF, 0x4B, 0x00, 
	0x8B, 0xA3, 0x1B, 0x00, 0x57, 0x77, 0x00, 0x00, 0x8B, 0xA3, 0x1B, 0x00, 0xC3, 0xCF, 0x4B, 0x00, 
	0xFB, 0xFB, 0xFB, 0x00, 0xEB, 0xE7, 0xE7, 0x00, 0xDB, 0xD3, 0xD3, 0x00, 0xCB, 0xC3, 0xC3, 0x00, 
	0xBB, 0xB3, 0xB3, 0x00, 0xAB, 0xA3, 0xA3, 0x00, 0x9B, 0x8F, 0x8F, 0x00, 0x8B, 0x7F, 0x7F, 0x00, 
	0x7B, 0x6F, 0x6F, 0x00, 0x67, 0x5B, 0x5B, 0x00, 0x57, 0x4B, 0x4B, 0x00, 0x47, 0x3B, 0x3B, 0x00, 
	0x33, 0x2B, 0x2B, 0x00, 0x23, 0x1B, 0x1B, 0x00, 0x13, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0xC7, 0x43, 0x00, 0x00, 0xB7, 0x43, 0x00, 0x00, 0xAB, 0x3F, 0x00, 0x00, 0x9F, 0x3F, 0x00, 
	0x00, 0x93, 0x3F, 0x00, 0x00, 0x87, 0x3B, 0x00, 0x00, 0x7B, 0x37, 0x00, 0x00, 0x6F, 0x33, 0x00, 
	0x00, 0x63, 0x33, 0x00, 0x00, 0x53, 0x2B, 0x00, 0x00, 0x47, 0x27, 0x00, 0x00, 0x3B, 0x23, 0x00, 
	0x00, 0x2F, 0x1B, 0x00, 0x00, 0x23, 0x13, 0x00, 0x00, 0x17, 0x0F, 0x00, 0x00, 0x0B, 0x07, 0x00, 
	0x4B, 0x7B, 0xBB, 0x00, 0x43, 0x73, 0xB3, 0x00, 0x43, 0x6B, 0xAB, 0x00, 0x3B, 0x63, 0xA3, 0x00, 
	0x3B, 0x63, 0x9B, 0x00, 0x33, 0x5B, 0x93, 0x00, 0x33, 0x5B, 0x8B, 0x00, 0x2B, 0x53, 0x83, 0x00, 
	0x2B, 0x4B, 0x73, 0x00, 0x23, 0x4B, 0x6B, 0x00, 0x23, 0x43, 0x5F, 0x00, 0x1B, 0x3B, 0x53, 0x00, 
	0x1B, 0x37, 0x47, 0x00, 0x1B, 0x33, 0x43, 0x00, 0x13, 0x2B, 0x3B, 0x00, 0x0B, 0x23, 0x2B, 0x00, 
	0xD7, 0xFF, 0xFF, 0x00, 0xBB, 0xEF, 0xEF, 0x00, 0xA3, 0xDF, 0xDF, 0x00, 0x8B, 0xCF, 0xCF, 0x00, 
	0x77, 0xC3, 0xC3, 0x00, 0x63, 0xB3, 0xB3, 0x00, 0x53, 0xA3, 0xA3, 0x00, 0x43, 0x93, 0x93, 0x00, 
	0x33, 0x87, 0x87, 0x00, 0x27, 0x77, 0x77, 0x00, 0x1B, 0x67, 0x67, 0x00, 0x13, 0x5B, 0x5B, 0x00, 
	0x0B, 0x4B, 0x4B, 0x00, 0x07, 0x3B, 0x3B, 0x00, 0x00, 0x2B, 0x2B, 0x00, 0x00, 0x1F, 0x1F, 0x00, 
	0xDB, 0xEB, 0xFB, 0x00, 0xD3, 0xE3, 0xFB, 0x00, 0xC3, 0xDB, 0xFB, 0x00, 0xBB, 0xD3, 0xFB, 0x00, 
	0xB3, 0xCB, 0xFB, 0x00, 0xA3, 0xC3, 0xFB, 0x00, 0x9B, 0xBB, 0xFB, 0x00, 0x8F, 0xB7, 0xFB, 0x00, 
	0x83, 0xB3, 0xF7, 0x00, 0x73, 0xA7, 0xFB, 0x00, 0x63, 0x9B, 0xFB, 0x00, 0x5B, 0x93, 0xF3, 0x00, 
	0x5B, 0x8B, 0xEB, 0x00, 0x53, 0x8B, 0xDB, 0x00, 0x53, 0x83, 0xD3, 0x00, 0x4B, 0x7B, 0xCB, 0x00, 
	0x9B, 0xC7, 0xFF, 0x00, 0x8F, 0xB7, 0xF7, 0x00, 0x87, 0xB3, 0xEF, 0x00, 0x7F, 0xA7, 0xF3, 0x00, 
	0x73, 0x9F, 0xEF, 0x00, 0x53, 0x83, 0xCF, 0x00, 0x3B, 0x6B, 0xB3, 0x00, 0x2F, 0x5B, 0xA3, 0x00, 
	0x23, 0x4F, 0x93, 0x00, 0x1B, 0x43, 0x83, 0x00, 0x13, 0x3B, 0x77, 0x00, 0x0B, 0x2F, 0x67, 0x00, 
	0x07, 0x27, 0x57, 0x00, 0x00, 0x1B, 0x47, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x0F, 0x2B, 0x00, 
	0xFB, 0xFB, 0xE7, 0x00, 0xF3, 0xF3, 0xD3, 0x00, 0xEB, 0xE7, 0xC7, 0x00, 0xE3, 0xDF, 0xB7, 0x00, 
	0xDB, 0xD7, 0xA7, 0x00, 0xD3, 0xCF, 0x97, 0x00, 0xCB, 0xC7, 0x8B, 0x00, 0xC3, 0xBB, 0x7F, 0x00, 
	0xBB, 0xB3, 0x73, 0x00, 0xAF, 0xA7, 0x63, 0x00, 0x9B, 0x93, 0x47, 0x00, 0x87, 0x7B, 0x33, 0x00, 
	0x6F, 0x67, 0x1F, 0x00, 0x5B, 0x53, 0x0F, 0x00, 0x47, 0x43, 0x00, 0x00, 0x37, 0x33, 0x00, 0x00, 
	0xFF, 0xF7, 0xF7, 0x00, 0xEF, 0xDF, 0xDF, 0x00, 0xDF, 0xC7, 0xC7, 0x00, 0xCF, 0xB3, 0xB3, 0x00, 
	0xBF, 0x9F, 0x9F, 0x00, 0xB3, 0x8B, 0x8B, 0x00, 0xA3, 0x7B, 0x7B, 0x00, 0x93, 0x6B, 0x6B, 0x00, 
	0x83, 0x57, 0x57, 0x00, 0x73, 0x4B, 0x4B, 0x00, 0x67, 0x3B, 0x3B, 0x00, 0x57, 0x2F, 0x2F, 0x00, 
	0x47, 0x27, 0x27, 0x00, 0x37, 0x1B, 0x1B, 0x00, 0x27, 0x13, 0x13, 0x00, 0x1B, 0x0B, 0x0B, 0x00, 
	0xF7, 0xB3, 0x37, 0x00, 0xE7, 0x93, 0x07, 0x00, 0xFB, 0x53, 0x0B, 0x00, 0xFB, 0x00, 0x00, 0x00, 
	0xCB, 0x00, 0x00, 0x00, 0x9F, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 
	0xBF, 0xBB, 0xFB, 0x00, 0x8F, 0x8B, 0xFB, 0x00, 0x5F, 0x5B, 0xFB, 0x00, 0x93, 0xBB, 0xFF, 0x00, 
	0x5F, 0x97, 0xF7, 0x00, 0x3B, 0x7B, 0xEF, 0x00, 0x23, 0x63, 0xC3, 0x00, 0x13, 0x53, 0xB3, 0x00, 
	0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xEF, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0xD3, 0x00, 
	0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xB7, 0x00, 0x00, 0x00, 0xA7, 0x00, 0x00, 0x00, 0x9B, 0x00, 
	0x00, 0x00, 0x8B, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x63, 0x00, 
	0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x2B, 0x00, 
	0x00, 0xFF, 0xFF, 0x00, 0x00, 0xE3, 0xF7, 0x00, 0x00, 0xCF, 0xF3, 0x00, 0x00, 0xB7, 0xEF, 0x00, 
	0x00, 0xA3, 0xEB, 0x00, 0x00, 0x8B, 0xE7, 0x00, 0x00, 0x77, 0xDF, 0x00, 0x00, 0x63, 0xDB, 0x00, 
	0x00, 0x4F, 0xD7, 0x00, 0x00, 0x3F, 0xD3, 0x00, 0x00, 0x2F, 0xCF, 0x00, 0x97, 0xFF, 0xFF, 0x00, 
	0x83, 0xDF, 0xEF, 0x00, 0x73, 0xC3, 0xDF, 0x00, 0x5F, 0xA7, 0xCF, 0x00, 0x53, 0x8B, 0xC3, 0x00, 
	0x2B, 0x2B, 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x1B, 0x1B, 0x00, 0x00, 0x13, 0x13, 0x00, 0x00, 
	0xFF, 0x0B, 0x00, 0x00, 0xFF, 0x00, 0x4B, 0x00, 0xFF, 0x00, 0xA3, 0x00, 0xFF, 0x00, 0xFF, 0x00, 
	0x00, 0xFF, 0x00, 0x00, 0x00, 0x4B, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x33, 0x2F, 0x00, 
	0x00, 0x00, 0xFF, 0x00, 0x00, 0x1F, 0x97, 0x00, 0xDF, 0x00, 0xFF, 0x00, 0x73, 0x00, 0x77, 0x00, 
	0x6B, 0x7B, 0xC3, 0x00, 0x57, 0x57, 0xAB, 0x00, 0x57, 0x47, 0x93, 0x00, 0x53, 0x37, 0x7F, 0x00, 
	0x4F, 0x27, 0x67, 0x00, 0x47, 0x1B, 0x4F, 0x00, 0x3B, 0x13, 0x3B, 0x00, 0x27, 0x77, 0x77, 0x00, 
	0x23, 0x73, 0x73, 0x00, 0x1F, 0x6F, 0x6F, 0x00, 0x1B, 0x6B, 0x6B, 0x00, 0x1B, 0x67, 0x67, 0x00, 
	0x1B, 0x6B, 0x6B, 0x00, 0x1F, 0x6F, 0x6F, 0x00, 0x23, 0x73, 0x73, 0x00, 0x27, 0x77, 0x77, 0x00, 
	0xFF, 0xFF, 0xEF, 0x00, 0xF7, 0xF7, 0xDB, 0x00, 0xF3, 0xEF, 0xCB, 0x00, 0xEF, 0xEB, 0xBB, 0x00, 
	0xF3, 0xEF, 0xCB, 0x00, 0xE7, 0x93, 0x07, 0x00, 0xE7, 0x97, 0x0F, 0x00, 0xEB, 0x9F, 0x17, 0x00, 
	0xEF, 0xA3, 0x23, 0x00, 0xF3, 0xAB, 0x2B, 0x00, 0xF7, 0xB3, 0x37, 0x00, 0xEF, 0xA7, 0x27, 0x00, 
	0xEB, 0x9F, 0x1B, 0x00, 0xE7, 0x97, 0x0F, 0x00, 0x0B, 0xCB, 0xFB, 0x00, 0x0B, 0xA3, 0xFB, 0x00, 
	0x0B, 0x73, 0xFB, 0x00, 0x0B, 0x4B, 0xFB, 0x00, 0x0B, 0x23, 0xFB, 0x00, 0x0B, 0x73, 0xFB, 0x00, 
	0x00, 0x13, 0x93, 0x00, 0x00, 0x0B, 0xD3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 
};

case "TILE":
{
	Listing.CreateDirectory(@"Tiles");
	uint tileSectionLength = binaryReader.ReadUInt32();
	for (int i = 0; i < tileSectionLength / 0x404; i++)
	{
		uint unknown = binaryReader.ReadUInt32();
		Bitmap tile = new Bitmap(32, 32);
		for (int j = 0; j < 0x400; j++)
		{
			byte pixelData = binaryReader.ReadByte();
			byte r = PaletteData[pixelData * 4 + 2];
			byte g = PaletteData[pixelData * 4 + 1];
			byte b = PaletteData[pixelData * 4 + 0];
			Shade pixelColor = pixelData == 0 ? Shade.Clear : Shade.FromArgb(r, g, b);
			tile.SetPixel(j % 32, j / 32, pixelColor);
		}
		
		tile.Save(string.Format(@"Tiles{0}.png", i));
	}
	break;
}

It labored! Over 2000 bizarre however lovable sprites have been extracted. It is value mentioning that shade 0x00 is definitely clear, one thing that is apparent from merely trying on the uncooked pixel knowledge.

Though it is much less fascinating than dumping the photographs, it is not arduous to decode the flags if we export them as a part of the picture filename and take a look at all of them in combination. A little bit of deduction and numerous observing tiny footage and their corresponding numbers reveals the next:

TILE TYPES:
bit0 = sport object
bit1 = non-colliding, attracts behind the participant
bit2 = colliding, center layer
bit3 = push/pull block
bit4 = non-colliding, attracts above the participant (for tall objects that the participant can stroll behind)
bit5 = mini map tile
bit6 = weapon
bit7 = merchandise
bit8 = character

FLAGS FOR WEAPONS:
bit16 = mild blaster
bit17 = heavy blaster OR thermal detonator (????)
bit18 = lightsaber
bit19 = the drive

FLAGS FOR ITEMS:
bit16 = keycard
bit17 = puzzle merchandise
bit18 = puzzle merchandise (uncommon???)
bit19 = puzzle merchandise (key merchandise???)
bit20 = locator
bit22 = well being pack

FLAGS FOR CHARACTERS:
bit16 = participant
bit17 = enemy
bit18 = pleasant

FLAGS FOR OTHER TILES:
bit16 = door

FLAGS FOR MINI-MAP TILES:
bit17 = particular mini map tile (house)
bit18 = particular mini map tile (puzzle, unsolved)
bit19 = particular mini map tile (puzzle solved) 
bit20 = particular mini map tile (gateway, unsolved)
bit21 = particular mini map tile (gateway, solved)
bit22 = particular mini map tile (up wall, locked)
bit23 = particular mini map tile (down wall, locked)
bit24 = particular mini map tile (left wall, locked)
bit25 = particular mini map tile (proper wall, locked)
bit26 = particular mini map tile (up wall, unlocked)
bit27 = particular mini map tile (down wall, unlocked)
bit28 = particular mini map tile (left wall, unlocked)
bit29 = particular mini map tile (proper wall, unlocked)
bit30 = particular mini map tile (goal)

Notably, bits 16 and above are reused with completely different meanings relying on the values of bits 0 by way of 8, which is a bit of complicated at first in case you’re anticipating a specific bit to have just one that means.

Maps (Issue: Jedi Knight)

Though at this level we have technically gotten what we got here right here for, the zone maps seem to be an fascinating factor to determine as properly. If we glance again on the zone illustration within the hex editor, we will see that there are a selection of sub-sections for zone entries, all of which we’ll must be taught extra about as a way to parse zones quite than skipping over them:

Though it is arduous to see with out loading up the file your self in a hex editor, the lengths of the IZON sub-sections are in all places. The primary 4 bytes after IZON appear like they is likely to be the size, but when we comply with that it will truly throw us someplace both wanting or past the IZAX sub-section identifier:

Quite a lot of occasions this course of seems like taking part in a cussed puzzle sport (I ought to know, I make them) however ultimately it clicks: there’s a bit of little bit of a set header that specifies, amongst different issues, the dimensions of the map (both 9×9 or 18×18), adopted by 6 bytes per grid sq., adopted by a 16-bit integer that specifies the variety of 12 byte structs that comply with.

The opposite sub-sections are equally cryptic: IZAX specifies its size plus six, as do IZX2 and IZX3, whereas IZX4 has a set size and IACT, which might seem many occasions, immediately specifies its size. Put that every one collectively and we get the next:

4 bytes: "IZON"
4 bytes: unknown
2 bytes: map width (W)
2 bytes: map peak (H)
1 byte: map flags (unknown meanings)
5 bytes: unused (identical values for each map)
1 byte: planet (0x01 = desert, 0x02 = snow, 0x03 = forest, 0x05 = swamp) 
1 byte: unused (identical values for each map)
W*H*6 bytes: map knowledge
2 bytes: object information entry depend (X)
X*12 bytes: object information knowledge
4 bytes: "IZAX"
2 bytes: size (X)
X-6 bytes: IZAX knowledge
4 bytes: "IZX2"
2 bytes: size (X)
X-6 bytes: IZX2data
4 bytes: "IZX3"
2 bytes: size (X)
X-6 bytes: IZX3 knowledge
4 bytes: "IZX4"
8 bytes: IZX4 knowledge
4 bytes: "IACT"
4 bytes: size (X)
X bytes: motion knowledge
4 bytes: "IACT"
4 bytes: size (X)
X bytes: motion knowledge
...
4 bytes: "IACT"
4 bytes: size (X)
X bytes: motion knowledge

Figuring out that there are six bytes per grid sq., there is a good probability they correspond to what goes within the grid sq. when the map is loaded. Merely trying on the first sq. of the primary map (0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00) offers me the robust impression that it is three 16-bit integers, every representing a tile positioned in that location (since there are properly over 256 tiles). Extending this system to blit collectively the extracted tiles utilizing this info yields the next:

Maps! It appears to be like like our guesses had been spot on. It seems that there are over 600 maps within the sport, every with their very own scripting and structure, which makes me admire how a lot effort went into such a easy trying sport.

See Also

The variable size “object information” on the finish of the IZON sub-section is fascinating as properly, and appears to point further properties in regards to the tiles positioned on the map. Wanting on the knowledge as an enormous binary blob within the hex editor is not very useful, however once we tabulate it as 12-byte entries and evaluate it towards a map picture some patterns begin to emerge:

Kind  N/A   X     Y     N/A   Arg
09 00 00 00 04 00 04 00 01 00 01 00
09 00 00 00 09 00 09 00 01 00 05 00
09 00 00 00 0F 00 09 00 01 00 09 00
0F 00 00 00 09 00 0D 00 01 00 5D 00
06 00 00 00 0A 00 03 00 01 00 AE 04
06 00 00 00 0A 00 04 00 01 00 AE 04

If we course of every entry as six 16-bit integers, the primary seems to be a kind flag, whereas the third and fourth look like map coordinates. Though the second and fifth find yourself at all times having the identical values (0x0000 and 0x0001), the final one is in all places, which makes it look an terrible lot like an argument. Once we begin taking a look at what sort of tiles the entries level to, we will see that each entry of sort 0x09 factors to a door, the place the argument is the ID of the map the door results in in-game.

Determining what all of the completely different entry varieties correspond to is a little bit of a grind, however by adapting our program to pause for interplay each time it discovers a not but deciphered entry sort permits us to systematically examine each entry sort within the knowledge file. A little bit of deduction and guessing yields the next:


Kind ID Description Argument
0x00 set off location
0x01 spawn location
0x02 force-related location
0x03 car to secondary map map that car takes you to
0x04 car to major map map that car takes you to
0x05 object that offers you the locator
0x06 crate merchandise merchandise contained inside
0x07 puzzle NPC tile ID for related character
0x08 crate weapon weapon contained inside
0x09 door (in) map that door connects to
0x0A door (out)
0x0B unused
0x0C lock
0x0D teleporter
0x0E x-wing from dagobah map that x-wing takes you to
0x0F x-wing to dagobah map that x-wing takes you to

Scripts (Issue: Jedi Grasp)

After determining the tile format and map format, a logical subsequent step is to determine the scripting language that powers the map-specific gameplay logic of the sport. It is filled with ASCII, together with what appear like instructions, so certainly it could’t be that tough… proper?

Sadly, there’s lots occurring right here that is comparatively troublesome to decode. In contrast to tiles and maps, which had been visually apparent once we figured them out, scripts contain variables and gameplay standing that is not as simply seen or verifiable. If I used to be extraordinarily devoted to reverse engineering this sport I think about that it would be possible to load up the degrees and match the noticed gameplay habits to the information within the scripts, however that is a bit of bit additional than I am prepared to go. Earlier than we wrap this up, nonetheless, we actually should do one thing with the whole lot we have discovered…

Making a Mod

The IZON sub-section for map quantity 95 begins at offset 0x266512 within the knowledge file. We will skip 20 bytes to get to the map knowledge, after which skip 6*(18*9+12) extra bytes to get to location (12, 11). If we then drop the values for tiles 1985 and 1986 into the “center” worth of every grid sq. utilizing our hex editor and overwrite the sport’s knowledge file, we get the next:

I am undecided why there is a sports activities automotive within the sport’s tileset, however I am fairly positive it is what Yoda would use if he went out cruising for a scorching date.

UPDATE: sehugg from Hacker Information identified that it is truly the automotive from Corvette Summer, a 1978 film starring Mark Hamill (who performed Luke Skywalker). What an Easter Egg!

Conclusion

There’s clearly much more that may very well be accomplished right here with sufficient endurance; after reverse engineering the vast majority of the sport’s knowledge file, it would be surprisingly easy to rebuild the sport from scratch (it is caught in a surprisingly small window) or construct instruments to export, edit, and repack the sport’s content material as a way to create mods. I am undecided why you’d need to do both of these issues, however they’re actually potentialities. At a minimal, I now have entry to a completely rad and exhaustive set of bizarre trying Star Wars sprites, and I hope that you have discovered one thing fascinating about reverse engineering knowledge file codecs for previous laptop video games!

Questions? Feedback? Authorized threats? Contact me at zach@zachtronics.com!

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top