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.
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):
Today, in this blog post, we will be adding eventareainfo.ini to this list.
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:
If all the checks pass, then do the following:
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.
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.
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).
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.
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.
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.
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.
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!
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.
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.
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…
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.
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.
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).