OpenTESArena icon indicating copy to clipboard operation
OpenTESArena copied to clipboard

MIF format support

Open Carmina16 opened this issue 7 years ago • 48 comments

I have some data on the MIF/RMD formats; are you interested?

Carmina16 avatar Jun 26 '17 13:06 Carmina16

Sure! I did some preliminary investigation a few months ago to get an idea of the layout. They are still not completely understood, but I believe they are used for defining interior locations (especially main quest areas), as well as for generating chunks of cities. They're kind of like "prefabs" in a way, I guess.

All that the .MIF parser can retrieve right now is just the map dimensions (width and depth). Each file appears to contain a map header and an array of levels. I think that "flor" represents the voxels in the ground floor and "map1" represents the voxels in the main floor. There's a lot of other miscellaneous data like trigger locations, probably for displaying messages when the player walks into a voxel, and coordinates for defining which doors are locked. Are there entity definitions as well, like positions of creatures and torches? I'm pretty sure all of the creatures in main quest dungeons have pre-defined spawn points.

I assume that each voxel is like 4 bits, and each floor uses some kind of compression?

I haven't looked into .RMD files very much, but I think they define "chunks" of wilderness. I was planning on programming the wilderness only after all the city and dungeon generation works.

afritz1 avatar Jun 26 '17 14:06 afritz1

Yes, it's the same compression as in type 8 images.

There is a WORD for each voxel on the level, stored right-to-left from the top right corner. They refer the objects and textures defined in the corresponding INF file. NUMF stores the number of the floor textures. MAP1 contains the walls of dungeons and the 1st story of buildings. If the 0x8000 bit is not set: 0000 means empty block XXYY, if XX&0x7F == YY&0x7F defines a solid block with the XX-1 texture on the walls. XXYZ otherwise defines a raised platform: X apparently is the height, Y is the texture on the top, defined by *BOXCAP Y in the INF file, Z is the texture on the side, from *BOXSIDE Z. If the 0x8000 bit is set: If the most significant nibble is 8, the lower byte is the index of the FLAT that defines a loot pile, a monster, a key, or a decoration. 9 is a transparent block which shows a 1-sided texture on all its sides. A is a transparent block which shows a 2-sided texture on one of its sides. B is a door with the texture defined by the 6 lowest bits-1 C is unknown D is a diagonal wall: D0 /, D1 , lowest bits are the texture index+1

FLOR values contain the floor texture index in the high byte, or C for the dry chasm, D for the water chasm and E for the lava chasm. The low byte contains the index of FLAT at this location + 1, or 0 if empty (used for placing objects on platforms).

MAP2 contains the second story tiles. Two additional flags are used: 0x80 used to expand the block to the additional story, 0x8000 to expand it to 2 additional stories. So 0x8080 will display as 4 stories for 5 stories in total.

TRIG section consists of 4-byte records: X coordinate, Y coordinate, *TEXT index, and sound index.

LOCK section consists of 3-byte records: X, Y and lock level. The key name is calculated as (locklevel-1) mod 12.

In RMD files, the first word is the uncompressed length. The file itself is RLE-compressed using WORDs instead of bytes. If NNNN is positive, N literal words follow, if negative, the next word is repeated -N times. The data represent 3 arrays: FLOR, MAP1 and MAP2 data for a 64x64 block.

Carmina16 avatar Jun 26 '17 19:06 Carmina16

All the cities use the predefined INF file, ABC.inf, where A is the tileset (M, T, or D), B is City or Wilderness, and C is the weather type: Normal, Rain, Snow, or W.

Carmina16 avatar Jun 26 '17 19:06 Carmina16

This is great information! Thanks for the details. I'll look into .MIF files again soon and see how much more of the decoder I can implement. It's fun demystifying these arcane formats.

My first guess on the wilderness' layout was that it's split up into 64x64 chunks, and that seems to be verified based on your explanation. I guessed those numbers in particular because, if you continually press F2 while walking through the wilderness, you can see the player's coordinates looping between 32 and 96 (presumably a quirk of the coordinate system used).

Any idea what TARG means in the .MIF files? I assume it's for a target of some kind. Also, any details on the other numbers in MHDR besides the dimensions of the map?

afritz1 avatar Jun 26 '17 21:06 afritz1

TARG records look like X,Y coordinates to place random loot and quest monsters.

I couldn't find what most numbers in the header are for:

struct MHDR { BYTE unknown1; BYTE nEntries; // valid entries that follow WORD X[4]; // no idea what coordinate is this WORD Y[4]; BYTE iStartingLevelIndex; WORD nLevels; WORD wWidth; WORD wHeight; BYTE unknown[]; }

Carmina16 avatar Jun 28 '17 19:06 Carmina16

We've made quite a lot of progress on .INF and .MIF files since this issue was opened, and I can't thank you enough for your assistance so far. I have a few more questions still:

  1. How are *BOXSIDE textures for floors handled? Currently, the floor sides are just using the texture index from the floor top. I don't know when to use the dry/wet/lava chasm or pit textures.
  2. How are city blocks generated? It looks like some permutation of the BSBD___.MIF, TVBD___.MIF, etc. files, probably using a similar formula to the main quest .MIF filenames (bit shifting, rotation, ...). I believe that villages are 4x4, towns are 5x5, and city-states are 6x6.
  3. How are .INF sounds indexed? Currently, some places like Murkwood cause an out-of-bounds access for certain sounds (like index 32). Are some sound indices supposed to have a modification applied?

On another note, I've been wondering about the depth of your knowledge regarding Arena. You have quite a few details in certain places, especially with compressed formats like .MIF files, which makes me wonder how you came across it to begin with. Maybe you figured it out on your own years ago, or maybe you have a connection to the original developers? 😄

afritz1 avatar Nov 30 '17 03:11 afritz1

  • Floors do not have walls, only the chasms have small textures on the walls, taken from *XXXCHASM. They are transparent on the bottom, so they allow to see the water/lava/unrendered image in the bottom part of the screen.
  • That one is quite complicated. Maybe you can give me the wiki access to document it?
  • The numbers in the INF file after sound filenames correspond to one of the 64 slots for sounds. The unassigned slots are ignored by the sound routine. There are some shenanigans around the slot 32, but I haven't figured that one out yet.

Carmina16 avatar Nov 30 '17 08:11 Carmina16

I sent you collaborator access.

So the floors themselves only have their top face textured? If that's the case, then chasms would appear to be context-sensitive (correct me if I'm wrong). In other words, they need to look at adjacent voxels to determine which faces are textured. I wasn't sure if this is what happens because this pattern isn't found anywhere else in Arena (I think. I've been looking into how doors are rendered, and they seem to depend on surrounding voxels for determining which door faces to display). So if chasms are context-sensitive, then there would need to be a texture index for each of the wall faces on the chasm, and a second pass over the floor data would need to be done for determining each adjacency.

With regards to .INF sounds, I'll just add a warning when an out-of-range access is attempted, and it'll resort to some default sound for now.

Are you familiar with C++11? You're free to make changes and do a pull request if there's something you know how to do in the code. I'm assuming you haven't tried to compile anything in the project yet.

afritz1 avatar Nov 30 '17 17:11 afritz1

Well, you can see whether the floor tile is adjacent to a chasm, when constructing the floor, and set its walls accordingly?

Unfortunately, I don't know C++.

Carmina16 avatar Dec 01 '17 09:12 Carmina16

Well, you can see whether the floor tile is adjacent to a chasm, when constructing the floor, and set its walls accordingly?

Right. There are probably a few ways to do this. I just wanted to brainstorm a bit before I start implementing something, since this case seems a little peculiar and I wanted to look before I leap. I'm just framing in my mind how it'll be done based on the fact that non-chasm floors don't have boxsides. According to what you said above, each chasm should be implemented by having an optional wall on each of the four sides. This data will likely also be used with collision detection at some point.

Once we get to where we're generating wilderness, the perimeter of each chunk will need to be checked against adjacent chunks for updating the walls of chasms.

afritz1 avatar Dec 01 '17 17:12 afritz1

I just added renderer support for type 0xA voxels (store signs, bed curtains, etc.) in commit f3202ca93815bc62b7ad9aafffd21e90aad1f970, but they don't have the correct texture IDs. This is how I'm currently obtaining data from their .MIF voxel:

// (map1Voxel & 0xF000) == 0xA000.
int textureIndex = map1Voxel & 0x000F; // Wrong.
int orientation = (map1Voxel & 0x00F0) >> 4; // 0: North, 4: West, 8: South, C: East.

The bed curtains in STKEEP.INF are at index 11 in the file, but with the method above I get 12 from the voxel data. Any idea how Arena calculates the texture index there? Maybe with a slightly different mask?

Edit: I experimented with this:

int textureIndex = max((map1Voxel & 0x003F) - 1, 0);
int orientation = (map1Voxel & 0x00C0) >> 4;

which is how door textures are calculated (lowest 6 bits), and edge textures are 99% correct then. But there's one type 0xA voxel in IMPERIAL.MIF with a texture index of 0 that forces the usage of max() because it otherwise goes negative.

afritz1 avatar Dec 26 '17 19:12 afritz1

Well, the game renders a gray square at that place, so it's better just to ignore that invalid 0 index.

Carmina16 avatar Dec 27 '17 08:12 Carmina16

Yeah, that's a better idea. I'll make that voxel assignment depend on a condition instead.

afritz1 avatar Dec 27 '17 16:12 afritz1

I saw the "not sure what this is" comment in the source about the hyphen before some FLAT declarations in .INF files, so I experimented with removing and adding them in EQUIP.INF.

When I removed all the hyphens, there wasn't any visual difference (that I noticed) in an equipment store, but DOSBox's logging showed the files that normally have hyphens before them being opened by A.EXE, whereas when running with an unmodified EQUIP.INF they weren't opened.

I also tried hyphening declarations of files that were used for visible sprites in the equipment store. It caused the files to not be loaded. In one case the relevant sprite was replaced in-game by another (contextually nonsensical) one, and in the other the sprite was invisible and the program crashed.

Tentative conclusion is that the hyphens just comment out these lines and cause the files to not be loaded.

Allofich avatar Jan 16 '18 12:01 Allofich

Added support for wilderness chunks (.RMD files) in commit ec4549c6c30e5cef50e6d29b6cedf0de5696c1be. It's able to load four chunks into a fixed 128x128 grid for testing. The WILD###.RMD files are picked using some simple random integers for now.

afritz1 avatar Feb 08 '18 22:02 afritz1

@Carmina16, I'd like to start chipping away at city generation soon. I see your notes on the wiki, and they are great! Could you tell me some more about where the data comes from for values like:

  • cityId in templateCount <- cityId in coastalCities ? (isCity(cityId) ? 3 : 2) : 5
  • cityX and cityY in citySeed <- (cityX << 16) + cityY

Also, how is isCity() implemented?

Are cityX and cityY the location values stored in CITYDATA.00?

Also, I'm not sure, but is WATER1 at template ID 5 in the reserved block list a typo? Should it be CITYW1, TOWNW1, or VILLAGW1?

afritz1 avatar Feb 13 '18 05:02 afritz1

Each province has 32 settlements, first 8 are "cities" (internally 0), next 8 are "towns" (1), and the last 16 are "villages" (2). isCity is for example ((cityId & 0x1F) < 8).

Yes, cityX/Y are coordinates from CITYDATA.0x.

"Water1" just means that it is the first template for coastal settlements: either CITYW1, TOWNW1 or VILLAGW1.

Carmina16 avatar Feb 13 '18 07:02 Carmina16

Oh, okay. I see now. I thought maybe cityId was a global value, like 0-200 or something, but it's actually just an index into the province locations array. And isCity() is easier than I thought, too. I'll look into city generation soon.

afritz1 avatar Feb 13 '18 15:02 afritz1

cityId is (province << 5) + localId, so every settlement in Arena has its id in 0..256 range, with 256 being the Imperial City.

Carmina16 avatar Feb 13 '18 16:02 Carmina16

For the testing purposes, I propose North Hall, a town in Hammerfell. Here's its properties: cityId: 0x2E Coordinates: 250, 131 Global coordinates: 120, 85 Terrain: 3 (desert) City Seed: 0x00780055 Ruler seed: 0x00550078 Map: 8, 4, 2, 6, 2, 8, .... Blocks: bsbd10c, nbbd3b, eqbd6b, tvbd6c, eqbd1b, bsbd8b, ....

Carmina16 avatar Feb 13 '18 16:02 Carmina16

Made some progress. I was able to generate city blocks, but some of them were incorrect and/or had incorrect voxel data (probably my fault). Need to look into it some more.

I used your test properties and got:

  • cityId: 0x2E
  • Name: North Hall
  • Coordinates: 250, 131
  • Global coordinates: (where do these come from?)
  • Terrain: (where does this come from? Province quadrant?)
  • Seed: 0x00FA0083 (incorrect)
  • Ruler seed: nothing yet
  • Map: 8, 4, 2, 6, 2, 8, ...
  • Blocks: bsbd14c, nbbd3d, eqbd7d, tvbd10d, eqbd3a, bsbd9a (all incorrect)

I'm not sure why the seed is wrong because I did (cityX << 16) + cityY, with cityX=250 and cityY=131.

Also I'm not sure why my cityId is 0x2E and not 0x2D.

afritz1 avatar Feb 14 '18 07:02 afritz1

My bad! The seed derived from the global coordinates is used elsewhere. The value used for the generation is indeed 0xFA0083, and the cityId is 0x2E.

As global coordinates, rulers and terrain are not of immediate importance, I will document them in wiki.

More control values: After generating the plan, the seed value is 0xe91d2657 After generating all the blocks, it is 0x94a4697f Complete plan (mirrored):

8 4 2 6 2
8 2 5 8 2
8 5 8 5 5
1 6 3 8 6
7 1 8 6 8

Carmina16 avatar Feb 14 '18 09:02 Carmina16

Alright, city generation seems to be more or less working in commit 90574fd4cd2b9d9dd42cdb5d439498b6ba8c1254. There are still a couple problems with some starting positions of blocks, and floor voxels being air instead of a wet chasm, but for the most part, Arena's cities can now be loaded! Thanks for helping me through it, @Carmina16.

I was getting the wrong block .MIF names before because I was accidentally getting the variation's random value before the rotation's random value, but all of that seems to be working now.

So about the starting positions of blocks, could you check that the values are correct here? I think I'm using the wrong index for each location type.

And the missing wet chasms, I'm not sure how Arena clears a block before it writes to it (because some blocks get written into more than once), so I'm just zeroing the floor voxels for now as part of the clearing.

afritz1 avatar Feb 16 '18 01:02 afritz1

I found out the array in the executable has different ordering: towns, villages, and finally cities. So the first element is 3, 4 TOWN1 , sixth is 3, 6 TOWNW1, etc. ending in 3, 5 CITYW3. Same ordering applies to the filenames too.

I'm not sure what the second issue is; the level data are just overwritten by the block copied.

Carmina16 avatar Feb 17 '18 06:02 Carmina16

Oh, the problem with starting positions was from an off-by-one bug in the executable reading, so TOWN1 was 0, 3 instead of 3, 4, etc..

And the second issue, I think I just need to change the block writing a little bit so it checks adjacent blocks when determining chasm walls (maybe?).

afritz1 avatar Feb 17 '18 21:02 afritz1

For blocks in the reserved block list that go outside the plan, are those just ignored? I.e.:

for block in reservedBlocks
   plan[block] <- RESERVED

There are some blocks with too high a value that then cause out-of-bounds writes; i.e., in villages with 4x4 blocks (writing to index 21 when there's only 16 blocks).

afritz1 avatar Feb 17 '18 22:02 afritz1

Yes, just ignore those if you use a dynamic array to hold the city plan.

Carmina16 avatar Feb 18 '18 06:02 Carmina16

I'm looking into dungeon generation now. One small question: what does getRandomSeed() mean in this? How is it defined?

newSeed <- getRandomSeed()
transitions <- []
for i <- 0 to depth - 1
...

afritz1 avatar Mar 08 '18 04:03 afritz1

Oh, it's just the current seed value. I've also noticed the small error in the algorithm, so watch for the change.

Carmina16 avatar Mar 08 '18 05:03 Carmina16

Does Arena use one integer for all location IDs (that is, both cities and dungeons)? I'm not sure if I can have a locationID that's 0-47 instead of a localCityID that's 0-31 and a dungeonID that's 0-15. I'm wondering about this because I'm about to implement the travel time code and localToGlobal() takes a local X and Y that comes from a 0-31 local city, but it looks like it could also take a 0-47 value (for all possibilities in a province, not just cities).

afritz1 avatar Mar 17 '18 02:03 afritz1