Divinitor | Blog

Exploring the depths of Dragon Nest

Navigation

Deep Dive: Map eventareainfo.ini

Apr 17
- Vahr (真理)

This is the first post in a series of blog posts that provide some insight on the internal workings and structure of Dragon Nest’s data files, what they do, how the game works, and how I go about deciphering their meaning. Please note that these posts will be fairly technical although I will attempt to give a more broad and simple overview of what’s going on.

Table of Contents

Some Background

Maps in Dragon Nest (be it a dungeon, nest, town, etc) are the environments that you (the player) play in. They consist of necessary information such as the terrain (height.ini, alphatable.ini, textable.ini), props (propinfo.ini), events/triggers (trigger.ini, triggerdefine.ini, eventareainfo.ini), as well as some other things (water, sound, grass). Collectively these files are placed into directories under /mapdata/grid, with internal names like “mc_icedragon_nesta” for the first map for Ice Dragon Nest. Maps drive a large amount of game flow in dungeons and nests, dictating all sorts of matters such as spawn position, boss spawning, orchestrating mechanics with the boss, spawning summons, cutscenes, item drops, and more.

So far, we can parse and mostly understand the following (I may write about these later, if I have the time or inclination to do so):

  • alphatable.ini: Controls the mixing of terrain textures per grid point
  • default.ini: Unused
  • grasstable.ini: Controls the type, look, distribution, and height of grass per grid point
  • height.ini: Contains the heightmap data for the terrain (format is partially incomplete)
  • propinfo.ini: Prop placement, predefined camera stops, and lighting control
  • sectorsize.ini: Size of the map
  • textable.ini: Terrain texture selection per section of the map
  • trigger.ini: Controls map flow and behavior by defining triggers that are registered to listen for certain events or conditions and fire off various behaviors based on that.
  • triggerdefine.ini: Lists the top-level triggers. The triggers themselves are in trigger.ini

Today, in this blog post, we will be adding eventareainfo.ini to this list.

What is eventareainfo.ini?

Eventareainfo.ini, as its name suggests, defines the areas that are used by events (aka triggers). Many trigger scripts take EventArea IDs as values, telling them where or in what area to perform an action or to check status. As an example, here is the trigger (from trigger.ini) that spawns the first boss in IDN once all players have arrived in the first arena with cutscenes disabled:

TRIGGER 12
	1st_Grizz/1st_01_User_Entered_No_Camera
	4 condition script calls
		0    AllPlayerIsInsideAreal(EVENT_AREA 3078)
		1    FLAGS[0] == 1   // DefineValueOperationInteger(INDEX 0, INTEGER 1, OPERATOR 0)
		2    FLAGS[1] == 0   // DefineValueOperationInteger(INDEX 1, INTEGER 0, OPERATOR 0)
		3    FLAGS[17] == 0  // DefineValueOperationInteger(INDEX 17, INTEGER 0, OPERATOR 0)
	10 action script calls
		0    HideShowProp(PROP 558, SHOW)
		1    HideShowProp(PROP 559, SHOW)
		2    RebirthMonster(EVENT_AREA 3008)
		3    Delay(2 SECONDS)
		4    RebirthMonster(EVENT_AREA 3007)
		5    WarpActorMonster(FROM EVENT_AREA 3007, TO EVENT_AREA 4947)
		6    FLAGS[1] = 1    // DefineValueChangeInteger(INDEX 1, INTEGER 1)
		7    Delay(2 SECONDS)
		8    LogStart(INTEGER 1)
		9    PostUserLogMsg(INTEGER 6, MID:1017443 "Mutated Kodiak has appeared")
	1 event script call
		0    Timer(INTEGER 1)

So interpreting this a little bit:

Every game tick, check that:

  • All players are inside area 3078
  • Flag 0 is set to 1
  • Flag 1 is set to 0 (flag 0 seems to track if the 1st boss fight has started)
  • Flag 17 is set to 0

If all the checks pass, then do the following:

  • Show props 558 and 559
  • Respawn the monster in area 3008
  • Wait 2 seconds
  • Respawn the monster in area 3007
  • Warp the monster in area 3007 to 4947
  • Set flag 1 to 1 (1st boss fight started)
  • Delay 2 seconds
  • Begin battle log (unsure what this is)
  • Show system text “Mutated Kodiak has appeared”

Obviously we will need to know what our event areas are to know what monsters we’re spawning, where we’re moving them, what is considered the “trigger area” for things, etc.

Diving In

We’ll be using Ice Dragon Nest Stage 1’s eventareainfo.ini file to perform our inspection. You can find it in your game files at /mapdata/grid/mc_icedragon_nesta/0_0/eventareainfo.ini.

Opening the file in a hex editor (I use frhed), we can see that we’ve got some names and a lot of NUL (0x00) bytes.

eaiini_divein01.png

Scrolling through quickly we see a pattern of small blocks of data followed by lots of empty bytes.

Oftentimes it is useful to have a visual view of the data. We’ll be using binvis, an online binary data visualization tool that lets us see the structure of a file. Open the file in binvis and play around with some of the tools on the left side.

For those of you who are just following along, here’s a chunk of what we see in the visualizer, in clustering mode. Clustering mode arranges nearby data in roughly rectangular boxes instead of in horizontal lines (as in a hex editor). This makes it easier to see the layout of the data, as people tend to have a harder time correlating patterns when the data is presented in horizontal rows (which tend to wrap around, causing spatial breaks in our visualization when the data is actually adjacent).

eaiini_divein_eventareainfo_binvis01.png

From this, we confirm what we noticed in our initial scroll-down in the hex editor - the file is mostly empty space, containing bursts of data (which includes the name of the area and what look to be coordinates). There are occasional bits of data within the seas of emptiness, which is important to investigate later.

Going back to the hex editor, we start eyeballing structure. Many file formats (especially in games, DN is no exception) open with some sort of header containing information about the file as a whole. This file seems to not have very much of a header - we can see at offset 0x10 that we’re starting a length-prefixed string. This leaves us with bytes 0x00 through 0x0F (16 bytes) as the header at most.

eaiini_divein02.png

Dragon Nest files are for the most part little endian, so interpreting our first 16 bytes as 32-bit integers, we get:

5675, 14, 0, 28

We notice that the file is 154,922 bytes in size, and that the chunks seem to be at fairly regular intervals between each other. Sampling a few random chunks throughout the file, we get sizes of
1600 - 1612 excluding the variable length strings. 154,922 / 1,632 (approximating the average length of the strings) comes out to around 95, which is not any of the numbers we see in the header, which rules out some sort of total entry count. From this we probably will have to write our test parser to boundlessly attempt to parse more entries until we reach End of File (EOF) or when the parser is unable to properly parse entries.

Initial Parsing

My tool, DNPakToolUI, makes it fairly easy to add a trivial text-output viewer for files. But first, we’ll need to implement a test parser in DNSubfile for DNPakToolUI to use. We’ll create the eventareainfo branch on both projects and update their POM versions and dependencies accordingly.

The TestReader lets us print parsing results as it proceeds to standard output or to some buffer. In this case, we’ll set up our basic data structures and a no-op test parser. We’ll also hook up DNPakToolUI to display whatever our test parser spits out.

First up, we’ll go ahead and parse the header. We don’t know what any of the numbers mean, so we’ll call them unknown1-4 followed by a list of event areas.

 @Override
 public StageEventAreas read(ByteBuffer byteBuffer) {
   StageEventAreas areas = new StageEventAreas();
   printInt("unknown1", areas.unknown1 = byteBuffer.getInt());
   printInt("unknown2", areas.unknown2 = byteBuffer.getInt());
   printInt("unknown3", areas.unknown3 = byteBuffer.getInt());
   printInt("unknown4", areas.unknown4 = byteBuffer.getInt());
   //  TODO
   return areas;
 }

Parsing our file gives us the expected output:

unknown1                 I 5,675 0x0000162B
unknown2                 I 14 0x0000000E
unknown3                 I 0
unknown4                 I 28 0x0000001C

Now, we’ll spotcheck a few other maps to see how these values vary.

eaiini_divein03.png

So we can see from this that unknown1 has values all over the place that don’t seem to correlate to anything we can tell. unknown2 seems to be a version number - spotchecking some very old maps show lower values like 10 or 12. unknown3 is always zero, and unknown4 also seems to vary without correlation. We’ll stop worrying about the header and start parsing entries.

Parsing our first entry

To help reduce noise and clutter, we copy only the first entry into its own hex editor window so that we can focus on one entry at a time. From just a basic inspection, we can see that an entry begins with a name followed by numeric values, and then a large block of zeros, and some tailing data.

When it comes to numeric data, an experienced person can often eyeball a hex quad (4 bytes) and guess fairly accurately if it is an integer or a floating point value. You can also use Frhed’s numeric interpretation in the status bar to check - oftentimes floating point values will have nonsensically high/low values or seemingly random or meaningless values. I like to use this online hex to floating point converter when reading values.

So immediately after the entry name, the first value we run into is 0x46AD88AC (remember we’re in little endian, so in the hex editor this will show as AC 88 AD 46). Converted to floating point we get 22212.3.

Doing this for all the values via the parser, we get:

name                     X MonsterGroup 21931 1st Boss Grizz
unknown2                 F 22212.3359 0x46AD88AC
unknown3                 F 0.0000
unknown4                 F 8380.1406 0x4602F090
unknown5                 F 22312.3359 0x46AE50AC
unknown6                 F 0.0000
unknown7                 F 8480.1406 0x46048090
unknown8                 F 170.4000 0x432A6665
unknown9                 I 3,007 0x00000BBF

The floating point values seem to be coordinates of some sort (although there are 7 and there are two zeros in there), but what’s most intriguing is unknown9, which we noticed in our trigger.ini example earlier as a Event Area ID. It would be reasonable to then assume that unknown9 is the event area ID

We then encounter our sea of zeros. Since we’re focused on just parsing the first entry, let’s just take the run of zeros as-is and skip 1,532 bytes to when we run into a non-zero value.

Repeat the inspection process to obtain

name                     X MonsterGroup 21931 1st Boss Grizz
unknown2                 F 22212.3359 0x46AD88AC
unknown3                 F 0.0000
unknown4                 F 8380.1406 0x4602F090
unknown5                 F 22312.3359 0x46AE50AC
unknown6                 F 0.0000
unknown7                 F 8480.1406 0x46048090
unknown8                 F 170.4000 0x432A6665
EventAreaID              I 3,007 0x00000BBF
unknown10                X A All 0x00 (byte array)
unknown11                I 1 0x00000001
unknown12                I 0
unknown13                F 10.0000 0x41200000
unknown14                F 10.0000 0x41200000
unknown15                I 1 0x00000001
unknown16                I 4 0x00000004
unknown17                F 9762.3359 0x46188958
unknown18                I 0
unknown19                F -4069.8594 0xC57E5DC0
unknown20                I 0
unknown21                I 0

Good, we’ve parsed our first entry!

Let it rip

Now, we let the parser read in as many blocks as it can until it chokes.

Things look pretty good, and we’re seeing some non-zero values in unknown10 but they don’t seem to be affecting our parsing at all. However, when we scroll down… we’re met with some ugliness

=== AREA ===             I 27 0x0000001B
	name                     X Monster 506178 2nd EnergyBall Set2 02
	unknown2                 F 13809.7461 0x4657C6FC
	unknown3                 F 0.0000
	unknown4                 F 10191.7568 0x461F3F07
	unknown5                 F 13909.7461 0x465956FC
	unknown6                 F 0.0000
	unknown7                 F 10291.7568 0x4620CF07
	unknown8                 F 0.0000
	EventAreaID              I 5,301 0x000014B5
	unknown10                X A [516] = 0x0A 10
	unknown11                I 1 0x00000001
	unknown12                I 0
	unknown13                F 1.0000 0x3F800000
	unknown14                F 1.0000 0x3F800000
	unknown15                I 1 0x00000001
	unknown16                I 4 0x00000004
	unknown17                F 1359.7461 0x44A9F7E0
	unknown18                I 0
	unknown19                F -2258.2432 0xC50D23E4
	unknown20                I 0
	unknown21                I 0
=== AREA ===             I 28 0x0000001C
	name                     X B
	unknown2                 F 0.0000 0x10000000
	unknown3                 F 549755813888.0000 0x53000000
	unknown4                 F 76813502439198860000000000000000.0000 0x74726174
	unknown5                 F 18393414536592457000000000.0000 0x69736F50
	unknown6                 F 18523600588218255000000000000.0000 0x6E6F6974
	unknown7                 F -144331242110713856.0000 0xDC003120
	unknown8                 F 0.0000 0x0046624F
	EventAreaID              I 0
	unknown10                X A [0] = 0x09 9, [1] = 0xEF 239, [2] = 0x45 69, [3] = 0xDD 221, [4] = 0xDF 223, [5] = 0x63 99, [6] = 0x46 70, [11] = 0x01 1, [12] = 0x29 41, [13] = 0xF2 242, [14] = 0x45 69, [15] = 0x65 101, [16] = 0x66 102, [17] = 0xE6 230, [18] = 0xC1 193, [19] = 0x02 2
	unknown11                I 0
	unknown12                I 0
	unknown13                F 0.0000
	unknown14                F 0.0000
	unknown15                I 0
	unknown16                I 0
	unknown17                F 0.0000 0x01000000
	unknown18                I 16,777,216 0x01000000
	unknown19                F -36893488147419103000.0000 0xE0000000
	unknown20                I 4,521,534 0x0044FE3E
	unknown21                I 0
=== AREA ===             I 29 0x0000001D
java.nio.BufferUnderflowException
	at java.nio.HeapByteBuffer.get(Unknown Source)
	at java.nio.ByteBuffer.get(Unknown Source)
	at co.phoenixlab.dn.util.DnStringUtils.readFixedBufferString(DnStringUtils.java:63)
	at co.phoenixlab.dn.util.DnStringUtils.readIntLengthPrefixedString(DnStringUtils.java:131)
	at co.phoenixlab.dn.util.DnStringUtils.readIntLengthPrefixedString(DnStringUtils.java:121)
	at co.phoenixlab.dn.subfile.stage.eventarea.StageEventAreasTestReader.readEventArea(StageEventAreasTestReader.java:76)

After Area 27 (which is actually #28, since we start counting with 0) we’re met with garbage and then our parser finally calls it quits when it tries to read in a string that isn’t a string and tries to read multiple megabytes of data that doesn’t exist.

Wait - 28?

Where have we seen that number before?

The header!

This means that the 28 in the header indicates some sort of group length. But we still have so much more of the file to go, what about the other groups? We must now revise our spec to take into account groups of areas. And that zero seems pretty suspect too. Let’s jump into the hex editor and take a look at the spot where the parser started choking, with a little help with the Java debugger to actually get the spot where the parser choked, which is offset 0xB4A3.

eaiini_divein04.png

Interestingly enough we see that we run into a 1 followed by a 66. At the start we ran into a 0 followed by a 28. Developers LOVE using IDs, so we can make an educated guess that the first integer is the group ID and the second is the number of areas in the group. Let’s update the parser to add an EventAreaGroup structure and parse accordingly.

Group differences

So now, our structures look kinda like this

StageEventAreas
    int unknown1;
    int unknown2;
    List<EventAreaGroup> groups;
    
EventAreaGroup
    int groupId;
    int numAreas;
    List<EventArea> areas;
    
EventArea
    String areaName;
    float unknown2;
    float unknown3;
    float unknown4;
    float unknown5;
    float unknown6;
    float unknown7;
    float unknown8;
    int eventAreaId;
    byte[] unknown10;
    int unknown11;
    int unknown12;
    float unknown13;
    float unknown14;
    int unknown15;
    int unknown16;
    float unknown17;
    int unknown18;
    float unknown19;
    int unknown20;
    int unknown21;

We run the parser aaaaaaand… the parser starts getting garbage values in the tail end of the first area of the 2nd group and then calls it quits shortly afterwards.

=== GROUP ===            I 1 0x00000001
	GroupID                  I 1 0x00000001
	AreaCount                I 66 0x00000042
	=== AREA ===             I 0
		name                     X StartPosition 1
		unknown2                 F 14483.9648 0x46624FDC
		unknown3                 F 0.0000
		unknown4                 F 7649.1250 0x45EF0900
		unknown5                 F 14583.9658 0x4663DFDD
		unknown6                 F 0.0000
		unknown7                 F 7749.1255 0x45F22901
		unknown8                 F -28.8000 0xC1E66665
		EventAreaID              I 2 0x00000002
		unknown10                X A All 0x00
		unknown11                I 0
		unknown12                I 1 0x00000001
		unknown13                F 0.0000 0x00000001
		unknown14                F 2033.9648 0x44FE3EE0
		unknown15                I 0
		unknown16                I -980,023,552 0xC5960700
		unknown17                F 0.0000
		unknown18                I 0
		unknown19                F 0.0000 0x00000010
		unknown20                I 1,918,989,395 0x72617453
		unknown21                I 1,936,674,932 0x736F5074
	=== AREA ===             I 1 0x00000001
java.nio.BufferUnderflowException

Our EventArea format seems to only work for the first group, or we’re missing some sort of structure that we can’t tell with just the first group areas.

One thing that is a fairly common pattern is the use of type or count values for dynamic parameters. In the tail section of each area block we notice there are a lot of low-value (0, 1, 4) integers interspersed with zeros and floating point values.

The new tail we run into is actually shorter than all the previous tails - only 32 bytes long as compared to the 44 byte long tail we have currently coded up, and is consistent throughout Group 1.

Finally, the last 5 quads seem to always have the same format (float, 0, float, 0, 0). We can then eliminate that from our samples and focus only on the sections that differ.

I can’t figure out anything with this. So, instead, we’ll change up the parser and structures based on the group ID. Group 0 will use the original tail, and Group 1 will use the new format. We can change this later or figure out the meaning of each field, but first we need to be able to at least parse everything before we attempt to derive details.

And it works… until we hit Group 2.

=== GROUP ===            I 2 0x00000002
	GroupID                  I 2 0x00000002
	AreaCount                I 1 0x00000001
	=== AREA ===             I 0
		name                     X VolumeFog - 000
		unknown2                 F 21282.3340 0x46A644AB
		unknown3                 F 1862.0000 0x44E8C000
		unknown4                 F 2482.8320 0x451B2D50
		unknown5                 F 23251.6621 0x46B5A753
		unknown6                 F 2957.0000 0x4538D000
		unknown7                 F 4444.1191 0x458AE0F4
		unknown8                 F 0.0000
		EventAreaID              I 4,785 0x000012B1
		unknown10                X A [515] = 0x3F 63, [520] = 0x01 1, [524] = 0x01 1, [528] = 0x01 1, [532] = 0x01 1, [536] = 0x01 1, [540] = 0x01 1, [544] = 0x01 1, [548] = 0x01 1, [552] = 0x01 1, [556] = 0x01 1, [560] = 0x01 1, [564] = 0x01 1
java.lang.UnsupportedOperationException: Unsupported group ID2

But that’s okay. Let’s check some other maps to see if this works up to Group 2 as well. This isn’t useful if the only map we can parse is Ice Dragon Nest Stage 1.

Spot checking, we get…

  • mc_icedragon_nestb_boss: OK
  • merca_worldzone: OK
  • aa_volcanonest_1a: OK (this one is interesting - there are 13 groups before we hit EOF, but groups 2-12 are empty. We’ll hit on this shortly)
  • aa_reddragno_nestb: OK (same as aa_volcanonest_1a)
  • anuarendel_worldzone: OK
  • 110_1a: OK (some random dungeon)

Group Count

Remember that weird unknown2 in the header? After spotchecking and doing manual inspection on the ends of each file, turns out its the number of groups present in the file, which makes sense - reading until EOF is highly unusual in DN, so we’ll update our spec accordingly and change our parser’s logic to read in that many groups instead of running until there’s no more data left.

Volume Fog

Group 2 (volumetric fog) appears to have many parameters in the usually mostly emtpy unknown10 block, starting at the 512th byte. We’ll need to split unknown10 into two and move the second portion in with the group specific blocks. Some Group 1 areas also have a value starting at the 512th byte. Conveniently enough, this leaves unknown10 with a size of 512, which is a common block size, and the remainder being 1020 bytes, which is fairly close to 1024, another common block size. In any case, we’re not terribly interested in fog, so we’ll just get enough to parse this entry and move on.

=== GROUP ===            I 2 0x00000002
	GroupID                  I 2 0x00000002
	AreaCount                I 1 0x00000001
	=== AREA ===             I 0
		name                     X VolumeFog - 000
		unknown2                 F 21282.3340 0x46A644AB
		unknown3                 F 1862.0000 0x44E8C000
		unknown4                 F 2482.8320 0x451B2D50
		unknown5                 F 23251.6621 0x46B5A753
		unknown6                 F 2957.0000 0x4538D000
		unknown7                 F 4444.1191 0x458AE0F4
		unknown8                 F 0.0000
		EventAreaID              I 4,785 0x000012B1
		unknown10a               X A All 0x00
		unknown10b               X A [3] = 0x3F 63, [8] = 0x01 1, [12] = 0x01 1, [16] = 0x01 1, [20] = 0x01 1, [24] = 0x01 1, [28] = 0x01 1, [32] = 0x01 1, [36] = 0x01 1, [40] = 0x01 1, [44] = 0x01 1, [48] = 0x01 1, [52] = 0x01 1
		unknown11                I 0
		unknown12                I 1 0x00000001
		unknown13                I 15 0x0000000F
		unknown14                F 9766.9980 0x46189BFE
		unknown15                F 2409.5000 0x45169800
		unknown16                F -9036.5244 0xC60D3219
		unknown17                I 1 0x00000001
		unknown18                I 2 0x00000002
		unknown19                F 0.4000 0x3ECCCCCD
		unknown20                F 1.0000 0x3F800000
		unknown21                F 1.0000 0x3F800000
		unknown22                F 0.7000 0x3F333333
		unknown23                I 1 0x00000001
		unknown24                I 0
		textureName              X VolumeFog.dds

With volumetric fog entries handled, we now can parse the majority (if not all) of the eventareainfo.ini files in the game.

Making Sense from Chaos

Now, while we have the structure and a couple key or obvious parameters identified, we still don’t understand a significant portion of the parameters and what they do. That will be saved for the next blog post. Hopefully this has been insightful on how people like me are able to figure out the innards of games such as DN, enabling deeper understandings of the game.

If you have questions or feedback, feel free to contact me on Discord (Vahr#9959), Twitter, or in game (IGNs Vahr, Vahrdian - Dragon Nest North America server).

Subscribe via RSS
Hosted with on GitHub Pages
Editing tools provided by prose.io.
About/Contact/Privacy