{"id":395,"date":"2016-06-03T07:00:38","date_gmt":"2016-06-03T14:00:38","guid":{"rendered":"http:\/\/eriknovales.com\/blog\/?p=395"},"modified":"2016-06-03T07:00:38","modified_gmt":"2016-06-03T14:00:38","slug":"ludum-linguarum-aurora","status":"publish","type":"post","link":"https:\/\/eriknovales.com\/?p=395","title":{"rendered":"Ludum Linguarum: Aurora"},"content":{"rendered":"<p><em>(Ludum Linguarum is an open source project that I recently started, and whose creation I\u2019ve been documenting in a series of posts. Its purpose is to let you pull localized content from games, and make flash cards for learning another language. <a href=\"https:\/\/github.com\/enovales\/LudumLinguarum\">It can be found on GitHub<\/a>.)<\/em><\/p>\n<p>In this post, I\u2019ll talk a little bit about Ludum Linguarum\u2019s support for some of the Aurora-engine based games that are out there. The Aurora engine was <a href=\"http:\/\/www.bioware.com\/\">Bioware<\/a>\u2019s evolution of its earlier Infinity Engine, and was used in quite a few games overall.<\/p>\n<h3>Motivation<\/h3>\n<p>There are quite a few games with large amounts of text that were produced with the Aurora engine (including <a href=\"https:\/\/en.wikipedia.org\/wiki\/Neverwinter_Nights_2\">one that I worked on<\/a>), so it seems quite natural to try and target it for extraction. The text in these games can also be categorized in some ways that I think are interesting, in the context of language learning \u2013 there are really short snippets or words (item names, spell names, skill names, etc.), as well as really lengthy bits of dialogue that might be good translation exercises. Additionally, there\u2019s <a href=\"http:\/\/neverwintervault.org\/project\/nwn1\/other\/bioware-aurora-engine-file-format-specifications\">quite a bit of official and unofficial documentation out there around its file formats<\/a>.<\/p>\n<h3>Goals for Extraction<\/h3>\n<p>The raw strings for the game are (mostly) located inside the talk table files. However, just extracting the talk tables would lose all context around how the strings are actually used in the game. For example, the spell names, feat names, creature names, dialogues, and so on, are all jumbled together in the talk table. It sounds like a small thing, but I feel that creating a taxonomy (in the form of \u201clessons\u201d) would make a big difference in the usefulness of the end product. Unfortunately, it also makes a <em>huge <\/em>difference in the amount of effort needed to extract all of this data!<\/p>\n<h3>How it all went<\/h3>\n<p>I spent quite a bit of time writing file-format-specific code, for things like the TLK table format, the BIF and KEY packed resource formats, the ERF archive format, and the generic GFF format. On top of that, then there was code to deal with the dialogue format that gets serialized into a GFF.<\/p>\n<p>I started with the original Neverwinter Nights, and then moved on to Jade Empire. The console-based Aurora engine games used some variant file formats (binary 2DAs, RIM files, etc.) that needed a little extra work to deal with, but there was enough information about these available on the Internet that I was able to support them without too much hassle.<\/p>\n<p>Once I had the basic file parsing code in place, it was just a matter of constructing the \u201crecipe\u201d of how to extract the game strings. This mostly involved sifting through all of the 2DA files for each game, looking for columns that represented \u201cstring refs\u201d (i.e. keys into the talk table database) \u2013 extracting dialogues was much simpler since they were already in their own files, and their contents were unambiguous.<\/p>\n<h3>Comparison between C# and F# implementations<\/h3>\n<p>I had basically written all of this file parsing code before (in the C# 2.0 era, so without <a href=\"https:\/\/en.wikipedia.org\/wiki\/Language_Integrated_Query\">LINQ<\/a>), but this time around I was writing it with <a href=\"http:\/\/www.fsharp.org\/\">F#<\/a>. I found it very interesting to compare the process of writing the new implementation, with what I remember from working on Neverwinter Nights 2 more than 10 years ago.<\/p>\n<p>The F# code is a <em>lot<\/em> more concise \u2013 I would estimate on the order of 5-7x. It isn\u2019t quite an apples-to-apples comparison with what I did earlier (for example, serialization is not supported, only deserializataion), but it\u2019s still much, much smaller. I suspect that adding serialization support wouldn\u2019t be a huge amount of additional code, for what it\u2019s worth.<\/p>\n<p><a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/dd233184.aspx\">Record types<\/a> and <a href=\"https:\/\/en.wikipedia.org\/wiki\/List_comprehension\">list comprehensions<\/a> really help condense a lot of the boilerplate code involved in supporting a new file format, and <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/dd233242.aspx\">match expressions<\/a> are both more compact, and safer when dealing with enumerated types and other sets of conditional expressions. I also got lots of good usage out of <a href=\"http:\/\/fsharpforfunandprofit.com\/posts\/the-option-type\/\">Option types<\/a>, particularly within the 2DA handling, where it very neatly encapsulated default cell functionality.<\/p>\n<p>But I think the thing that accounts for the <em>biggest<\/em> difference between my old C# implementation and the new F# implementation, is the range of functional abstract data types available \u2013 or, to put it another way, the lack of LINQ in my C# implementation. If LINQ were available at the time I was working on Neverwinter Nights 2, I think my code would have looked a lot more like the F# version, with liberal use of Select()\/map() and Where()\/filter(). These operations replace very verbose blocks of object construction and selective copying, often in a single line, which is an enormous savings in code size and improvement in clarity.<\/p>\n<p>I feel like there is still a lot of bespoke logic involved, for extracting the individual bits and pieces of each format, but that doesn\u2019t seem to be avoidable \u2013 the formats are not self-describing, and it seemed like it would be overkill to try and construct a meta-definition of the GFF-based formats.<\/p>\n<h3>Summary<\/h3>\n<p>Overall, I was pretty pleased with how this went. While it was a decent amount of work to support each file format, once that code was all written, the process of creating the game-specific recipe to extract strings was pretty straightforward. There weren\u2019t really any surprises in the implementation process, which was definitely <strong><em>not<\/em><\/strong> the case for the game that I\u2019ll talk about in my next set of posts.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>(Ludum Linguarum is an open source project that I recently started, and whose creation I\u2019ve been documenting in a series of posts. Its purpose is to let you pull localized content from games, and make flash cards for learning another language. It can be found on GitHub.) In this post, I\u2019ll talk a little bit [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[7,9,10],"tags":[],"class_list":["post-395","post","type-post","status-publish","format-standard","hentry","category-development","category-games","category-ludum-linguarum"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/eriknovales.com\/index.php?rest_route=\/wp\/v2\/posts\/395","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/eriknovales.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/eriknovales.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/eriknovales.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/eriknovales.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=395"}],"version-history":[{"count":0,"href":"https:\/\/eriknovales.com\/index.php?rest_route=\/wp\/v2\/posts\/395\/revisions"}],"wp:attachment":[{"href":"https:\/\/eriknovales.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=395"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/eriknovales.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=395"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/eriknovales.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=395"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}