FANDOM


La-Mulana stores its various data across a variety of different files, all of which (except for the various .png, .wav, and .ogg files) seem to use custom file formats. Fortunately none of the files seem to employ any data compression, making understanding their formats a comparatively simple project.

All results listed below are from research conducted on the GOG release of the game. It is unknown if other releases differ in any way.

map##.msd format Edit

Used by data/mapdata/map00.msd through data/mapdata/map25.msd, as well as all the mapdata/map##.msd files for each of the time attack modules. There are a number of other files in the same folder with the same extension (boss##.msd, ending#.msd, opening.msd, shop.msd, title.msd), which have similar (though not quite identical) formats.

  • Nearly all properties are signed and big-endian.
  • .msd files correspond closely enough to Fields that the two terms can be used basically interchangeably, but there is not quite a 1:1 correspondence. For example, the two halves of the Chamber of Birth are divided across map15.msd and map16.msd.
  • A "zone" is a unit smaller than a field but (potentially) larger than a single room. For example, rooms B3-B5 in the Gate of Guidance are a single zone, and likewise rooms D3-F4 in the Temple of the Sun. Zone boundaries are discernible in-game in at least two ways: parallax scrolling only works when going from one room in a zone to another, and more objects respawn when leaving and reentering a zone than when going from one room in a zone to another. The number of rooms in a zone is not explicitly stated but is instead calculated by multiplying the width (divided by 640 pixels) and height (divided by 480 pixels).
  • Additionally, most .msd fields define a number of zones that contain no rooms (as defined as areas that the player can enter, and may contain interact-with-able objects) at all. Most doors and other parts of the game constructed from various tiles in a map##_1.png file are actually roomless zones inserted into a room. The only way of distinguishing zoneless rooms is to check whether or not their dimensions are multiples of room sizes (64 by 48).
  • The function of the GraphicsFilesID byte differs slightly between main game and time attack .msd files. In the main game, a value of 02 means that the field uses map00_1.png and eveg00.png; a value of 05 means that the field uses map03_1.png and eveg03.png, and so on. In time attack modules, the value is a line number for that module's list.dat file, so 02 means that the field uses the images from the first non-comment line, 05 the images from the third non-comment line, and so on.
  • See data/graphics/hit_parts.png for clues about the various possible values of the CollisionMask bytes. The most common are 0x00 (unmasked, can be walked through) and 0x80 (orange, solid wall). The green ladder-looking-structures are ladders. The light blue backdrops are water (and the arrows are water currents), followed by purple lava. The vertical blue lines are waterfalls. The brown checkerboard is an area that Yowies can crawl on. The blue walls are ice/walls that the Grapple Claw doesn't work on. The orange slopes are slippery and require Hermes' Boots to walk up. The dark green arrow tiles are walls that you can die from being smashed into by moving blocks.
struct MapMSD {
	struct AnimatedTile {
		bit1 unk00; // only used in boss06.msd (tiamat), function related to how the animation repeats.
		bit15 NumberOfFrames; // together with the first bit, makes a short word.
		TileID Frames[NumberOfFrames]; //unknown which higher-bit TileID properties are or aren't available in this context
	} AnimatedTiles[];
	short EndOfAnimatedTileSection; //always 00 00, which would be parsed as an animated tile that is zero frames long
	byte GraphicsFilesID; //References two hardcoded arrays. See below.
	short NumberOfZones;
	struct Zone {
		byte UseBossGraphics; //If 1, uses the boss graphics in this Zone. The boss graphics file used is b##.png where ## is the number of the msd file.
		byte NumberOfLayers;
		byte NumberOfPrimeLayer; //Layers before the prime layer are in the foreground, and layers after the prime layer are in the background. The size of the prime layer determines the size of the Zone. The size of the hit tile mask is always the same as the size of the prime layer.
		short HitMaskWidth; //measured in mask tiles, which are 10x10
		short HitMaskHeight; //measured in mask tiles
		byte CollisionMask[HitMaskWidth][HitMaskHeight]; //see discussion above
		struct Layer {
			short LayerWidth; //measured in graphics tiles, which are 20x20
			short LayerHeight; //measured in graphics tiles
			byte NumberOfSublayers;
			struct Sublayer {
				TileID[LayerWidth * LayerHeight];
			} Sublayers[NumberOfSublayers]; //ordered so that the first is in front and the last is in back
		} Layers[NumberOfLayers]; //ordered so that the first is in front and the last is in back
	} Zones[NumberOfZones];
}

struct TileID {
	bit11 RawTileID; //each row in the map##_1.png files is 50 tiles wide, so RawTileID=49 would be tile 49,0 and RawTileID=50 would be tile 0,1. It doesn't matter whether the .png is the full 1024 pixels tall. Shops are hardcoded to use 51 tile width instead.
	bit2 TileType; //0: animated tile. 1: standard tile. 2: color addition. 3: color multiplication.
	bit1 FlippedHorizontally; //boolean
	bit1 Rotated90Degrees; //boolean
	bit1 Rotated180Degrees; //boolean
}

char * GraphicsFileID0[] = { // This array is used in a Zone when UseBossGraphics is 0.
     "",
     "",
     "map00_1.png",
     "map01_1.png",
     "map02_1.png",
     "map03_1.png",
     "map04_1.png",
     "map05_1.png",
     "map06_1.png",
     "map07_1.png",
     "map08_1.png",
     "map09_1.png",
     "map10_1.png",
     "map11_1.png",
     "map12_1.png",
     "map13_1.png",
     "map14_1.png",
     "map15_1.png",
     "map16_1.png",
     "map17_1.png",
     "02shop.png",
     "",
     "",
     "",
     "",
     "",
     "",
     "",
     "",
     "",
     "title01.png",
     "map18_1.png",
     "map19_1.png",
     "map19_1.png"
};

.rcd format Edit

Used by data/mapdata/script.rcd, as well as mapdata/script.rcd for each of the time attack modules.

  • Note that it is impossible to parse an entire .rcd file without simultaneously parsing all the corresponding .msd files, because the .rcd format does not seem to include any explicit markers of how many fields, zones, or rooms there are.
  • Each "field" corresponds to a .msd file, not necessarily a field as is presented to the game player.
  • Again, all properties are to be signed and big-endian.
  • There are 203 different EventIDs, although 4 of them never appear in-game, each of which has the same number of parameters every time it appears, despite the .rcd format stating that number explicitly. For example, a Wall Seal, EventID 52, always has exactly two parameters: one for specifying which key seal is needed to open it (values 0-3), and one for specifying whether it is one of the giant seals in the True Shrine of the Mother (value 1) or only a regular-sized seal (value 0). A full list of these EventIDs, as well as the purposes of their numerous parameters, would be a large undertaking and belongs on a separate wiki page.
  • Events are divided into two types, Objects and Effects, the difference being that Objects are placed in specific positions in specific rooms, whereas Effects don't have specific positions, and may apparently be bound to rooms, zones, or entire fields. An example of an Object would be a Pot; an example of an Effect would be specifying that Chonchons should be continually generated in a room. Any given EventID is either always an Object or always an Effect.
struct ScriptDotRCD {
	short Unknown; //purpose unknown, skipped internally; possibly intended as a version number? always zero?
	struct Field {
		byte LengthOfInternalFieldName;
		short FieldEffectCount; //seemingly always 0 in time attacks
		byte FieldName[LengthOfInternalFieldName]; //uses UTF-16 encoding; does nothing. 
		Effect FieldEffects[FieldEffectCount];
		struct Zone {
			short ZoneEffectCount; //seemingly always 0 for roomless zones or in time attacks
			Effect ZoneEffects[ZoneEffectCount];
			struct Room {
				byte InternalNameLength;
				short NumberOfObjects;
				byte NumberOfEffects;
				Effect Effects[NumberOfEffects];
				Object Objects[NumberOfObjects - NumberOfEffects];
				byte InternalRoomName[InternalNameLength]; //name of zone in UTF-16 encoding. Does nothing
				struct Exit {
					byte FieldID;
					byte ZoneID;
					byte RoomID;
				} Exits[4]; //order is top, right, bottom, left
			} Rooms[];
		} Zones[];
	} Fields[];
}

struct Object {
	short EventID;
	bit4 NumberOfTestFlags;  //NumberOfTestFlags and NumberOfWriteFlags together make up a single byte
	bit4 NumberOfWriteFlags; 
	byte NumberOfParameters;
	short PositionX;
	short PositionY;
	struct TestByteOperations {
		short Flag; // In the save file, flags are found with an offset of 0x11. Flags 0-50 are reset every Room. Flags 51-100 are reset every Zone.
		byte Value;
		TestByteOp Operation; // see below
	} TestByteOperations[NumberOfTestFlags];
	struct WriteByteOperations {
		short Flag; // In the save file, flags are found with an offset of 0x11. Flags 0-50 are reset every Room. Flags 51-100 are reset every Zone.
		byte Value;
		WriteByteOp Operation; // see below
	} WriteByteOperations[NumberOfWriteFlags]
	short Parameters[NumberOfParameters];
}
struct Effect { //exactly the same as Object, but with no Position properties
	short EventID;
	bit4 NumberOfFirstFlags;
	bit4 NumberOfSecondFlags;
	byte NumberOfParameters;
	TestByteOperations  TestByteOperations[NumberOfTestFlags];
       WriteByteOperations WriteByteOperations[NumberOfWriteFlags];
	short Parameters[NumberOfParameters];
}

// The Byte Operations use switch statements to refer to an array of all byte operations that the game uses, though not all are accessible from the Test and Write operations.

enum TestByteOperations {
	EQUALITY_OPERATOR		= 0x00,
	LESS_THAN_OR_EQUAL_OPERATOR	= 0x01,
	GREATER_THAN_OR_EQUAL_OPERATOR	= 0x02,
	BITWISE_AND_IS_NONZERO_OPERATOR	= 0x03, // True if the result of a bitwise and operation is non-zero
	BITWISE_OR_IS_NONZERO_OPERATOR	= 0x04, // ibid
	BITWISE_XOR_IS_NONZERO_OPERATOR	= 0x05, // ibid
	FLAG_IS_ZERO_OPERATOR 		= 0x06,	// bugged and always true.
	NOT_EQUAL_OPERATOR		= 0x40,
	GREATER_THAN_OPERATOR		= 0x41,
	LESS_THAN_OPERATOR		= 0x42,
	BITWISE_AND_IS_ZERO_OPERATOR	= 0x43, // True if the result of a bitwise and operation is zero
	BITWISE_OR_IS_ZERO_OPERATOR	= 0x44, // ibid
	BITWISE_XOR_IS_ZERO_OPERATOR	= 0x45, // bugged and always true.
	FLAG_IS_NONZERO_OPERATOR	= 0x46
} TestByteOperations;
	
enum WriteByteOperations {
	ASSIGNMENT_OPERATOR		= 0,
	ADDITION_ASSIGNMENT_OPERATOR	= 1,
	SUBTRACTION_ASSIGNMENT_OPERATOR	= 2,
	MULTIPLICATION_ASSIGNMENT_OPERATOR = 3,
	DIVISION_ASSIGNMENT_OPERATOR	= 4,
	BITWISE_AND_ASSIGNMENT_OPERATOR	= 5,
	BITWISE_OR_ASSIGNMENT_OPERATOR	= 6,
	BITWISE_XOR_ASSIGNMENT_OPERATOR	= 7
} WriteByteOperations;

Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.