The main components in the GriFFin API are files that you want to read or write, fields contained in GFF files and data types used in these fields (or elsewhere.) These components correspond to the main packages. Apart from those, there is also the concept of resources which are files that may be embedded in other files, such as GFF files embedded in BIF or ERF files. And finally there are convenience objects as wrappers around GFF files for files which are technically GFF but used for certain specific purposes and as such are expected to have certain information in certain places. The only implementation of those convenience objects is, as of writing this, the class for ARE files net.jarre_de_the.griffin.file.impl.Are
.
If you do want to understand the files and data stored inside, at a more technical level, you may want to review the Bioware specs, here. The aim is for you to not need that, if you have a general working knowledge of those files and Java experience. If you meet those criteria and the javadocs provided on the maven site in combination with this wiki are not sufficient, do let us know what you're missing.
Let's jump right into it with some examples.
All examples are also available with the source code in the ExamplesTest class.
To read a GFF file, e. g. a BIC file containing character information, all you need to do is create a File instance pointing to the file in question and pass that to the constructor:
URL input = Gff.class.getResource("/player.bic"); assertNotNull(input); // In this example we construct a File from a URL, because that's // more portable as far as our tests are concerned. Of course, you // can construct your File object in whichever way you like. File f = new File(input.toURI()); Gff inFile = new Gff(f); assertNotNull(inFile); System.out.println("file type: " + inFile.getFileType()); inFile.getRootStruct().dump(System.out);
After the call to the constructor, you're done reading the file and can start to work with it. In GFF files, all actual data is stored in a tree structure rooted at what's called the root struct.
Each file usually has some overall metadata such as the file version or file type. These kind of things you usually don't want to modify, unless you know excactly what you're doing. The reason why you normally don't want to change those things is that they either have very specific values for the various types of files or depend on the data. For example, a GFF file has a piece of metadata that tells you how many fields it contains.. But even if you were to overwrite that number, when writing the file to disk, the API will ignore whatever you put there and use the number of fields that actually are contained in the file.
Consequently, you would normally change actual data, e. g. fields in a GFF file, and then persist the object to a byte[] which you then write to a file.
URL input = Gff.class.getResource("/player.bic"); assertNotNull(input); // In this example we construct a File from a URL, because that's // more portable as far as our tests are concerned. Of course, you // can construct your File object in whichever way you like. File f = new File(input.toURI()); Gff inFile = new Gff(f); assertNotNull(inFile); inFile.setFileVersion("V4.4"); // you likely don't want to do that // let's remove the first field in the root struct, which will // likely break the file. AbstractField field = inFile.getRootStruct().getValueAsList().get(0); inFile.getRootStruct().getValueAsList().remove(field); byte[] out = inFile.persist(); File fWrite = File.createTempFile("griffin-test-" + getClass().getSimpleName(), "tmp"); System.out.println("writing to file: " + fWrite.getAbsolutePath()); try (FileOutputStream fos = new FileOutputStream(fWrite)) { fos.write(out); }
Note that writing a file with GriFFin normalizes it. It is possible for files to contain empty space because some game developer removed data and never compacted the file again, afterwards, or especially in the case of 2DA files there may be trailing whitespace that serves no purpose, or in some cases fields may be stored physically on disk in different order without making a difference semantically. GriFFin will normalize all that. If you read a file and save it again with GriFFin, chances are the resulting file will not be byte-identical to the original. It could be smaller, it could be same-size but have a different checksum. It should never be larger than the original. The resulting file should be semantically equivalent to the original, i. e. if the game treats the resulting file different from the original, it's a bug in the GriFFin API. If you write a file with GriFFin, read it back and write it again to a third file, that third and second file should be byte-identical.
Once you have read (or constructed) a file, you can start navigating its fields and data. If you want to retrieve information without prior knowledge of exactly where the information is stored within your tree, you need a way to query for data. To that end the GriFFin API offers a number of methods on Container
classes (ListData
and StructData
) starting with "find" which will search for fields.
To find all fields of type CExoLocString with the label "Description" in your GFF file, you would do something like this:
// the root struct is also a StructData object and as such one of // two container objects which allow searching for fields contained // in them (the other is ListData.) // // Let's try to retrieve all fields with the label "Description" // List<FoundField<CExoLocStringField>> descriptions = root.findField(CExoLocStringField.class, Util.stringToLabelArray("Description")); assertNotNull(descriptions); assertFalse(descriptions.isEmpty());
As you can see, the result is a list of all fields found, wrapped in a wrapper object of the FoundField
class which contains the field itself as well as a graph or path pointing to the location of that field relative to the container you're searching in. That graph is an array of integers specifying the position of the field. If the found field is contained locally in the container you are searching, then it is an array of length == 1, e. g. int[] { 7 } meaning to say the field is the seventh field in the container. If the field is contained in a sub-container, the graph or path may look like this: int[] { 7, 3 } meaning to say this container contains another container at field position 7 and that contains the found field at position 3. Of course, we start counting at zero.
That path is helpful, because it can be used to retrieve fields, too, using the method findFieldByPath(int[])
. So, this is the way to move horizontally in your tree, from one field to another position, relative to the first. Let's say we want to find out, if some item in a character's inventory is in the same container as a torch (becasue that would cause an explostion, or whatever.)
// First, we search for all fields that have a tag starting with // NW_IT_TORCH (not saying that's the best way to identify torches) // List<FoundField<CExoStringField>> tags = inFile.getRootStruct().findField(CExoStringField.class, Util.stringToLabelArray("Tag"), "NW_IT_TORCH.*"); assertNotNull(tags); assertFalse(tags.isEmpty()); // go through the fields we found for (FoundField<CExoStringField> ff : tags) { int[] path = ff.getPath(); Container c = root; // and find the container of each field // // In our example we have a path of { 111, 18, 5 } // 111 is the position of the ItemList in the root struct // 18 is the struct element in the list that is the the item itself // 5 is the position of the tag field we queried for, inside the // item struct. // Therefore, the container is at 111 and we cut off the last 2 // elements. if (null != path && path.length > 2) { int[] containerPath = new int[path.length - 2]; System.arraycopy(path, 0, containerPath, 0, path.length - 2); c = (Container) root.findFieldByPath(containerPath).getData(); } // and in that container look for the other field List<FoundField<CResRefField>> resrefs = c.findField(CResRefField.class, Util.stringToLabelArray("TemplateResRef"), new CResRefData("nw_it_mpotion003".getBytes(Util.CHARSET_US_ASCII))); boolean found = false; if (null != resrefs && !(resrefs.isEmpty())) { // we have found the second item somewhere in the container // that also has the first item, it might be in a sub-container. for (FoundField<CResRefField> ffrr : resrefs) { int[] secondPath = ffrr.getPath(); // if the object is in the local container, we expect the // path to be 2 int long for the same reason we cut off // 2 from the original path if (2 == secondPath.length) { LOGGER.info(ffrr.getField().getValue().toString() + "is in the same container as " + ff.getField().getValue().toString()); found = true; } } } assertTrue(found);
ERF files are just archives that contain other files. Once you have read the file, you have all the resources contained in it using getResources()
. For each resource, you can determine the resource type and then cast the data of the resource to whatever is appropriate, i. e. resource.getResourceType().getContentType().typeClass()
.
URL input = Erf.class.getResource("/cep2_armorstnd.erf"); assertNotNull(input); File f = new File(input.toURI()); Erf inFile = new Erf(f); // the ERF file contains the following resources for (ErfResource resource : inFile.getResources()) { System.out.println(resource.getResRef() + "." + resource.getResourceType().extension()); } // get the ExportInfo.gff file ErfResource exInfoRes = inFile.getResources().get(0); assertNotNull(exInfoRes); if (ResourceType.GFF.equals(exInfoRes.getResourceType())) { Gff exportInfo = (Gff) exInfoRes.getData(); assertNotNull(exportInfo); // do whatever with the Gff }
BIF files are similar to ERF files, but they don't store metadata about the embedded resources. Such metadata is stored externally in KEY files. So, if you actually want to work with a specific resource (such as cls_skill_archer.2da
from xp2.bif
) rather than just blindly work on everything in a given BIF file, you need to use BIF and KEY files in conjunction.
// read the BIF file URL input = Bif.class.getResource("/xp2.bif"); assertNotNull(input); File f = new File(input.toURI()); Bif inFile = new Bif(f); // every BIF file has an associated KEY file. That KEY file is not // absolutely necessary to _work_ with the BIF file, but to know // what the data in the BIF file is. input = Key.class.getResource("/xp2.key"); assertNotNull(input); f = new File(input.toURI()); Key inFileKey = new Key(f); for (Key.KeyEntry keyEntry : inFileKey.getKeyEntries()) { // at this point, the key entry _might_ point to a different // file than the one we loaded from. We could do sophisticated // stuff like compare file names, but for the purposes of this // example, rather than reimplement the game's resource manager, // we happen to know we're only interested in file entry 0 int bifId = keyEntry.getBif(); if (0 == bifId) { VariableBifResource resource = inFile.getResources().get(keyEntry.getBifRessourceTblIdx()); System.out.println(keyEntry.getResRef() + "." + keyEntry.getResourceType().extension() + ": " + resource.getId()); } }
To work with an actual resource and its data, you would cast it to the proper class as described for the ERF file.
The 2DA files are two-dimensional arrays (tables) of data. The current implementation treats everything as a String and leaves type safety to the user (as does the game, at the end of the day.) The current implementation also mostly relies on providing an array of String arrays and provides precious few convenience methods, though more may be added in the future. It does, however, try to let you treat the table in the most natural way. You, as the user, just put a String into a cell and the implementation will quote them as appropriate or replace null Strings with "****" as necessary.
Let's say we want to look at a 2DA file packsktorm
in a BIF file and have the index into the BIF's resources list in a variable packsktormIndex
// get the packsktorm.2da resource, now VariableBifResource packsktorm = inFile.getResources().get(packsktormIndex); TwoDa p2da = (TwoDa) packsktorm.getData(); // get the table headers of the two-dimensional array System.out.println("table headers:" + Arrays.toString(p2da.getHeaders().toArray())); // get the column for a given table header int col = p2da.getColumn("Label"); // get a complete column System.out.println("\"Label\" column: " + Arrays.toString(p2da.getColumnValues(col))); // get the row for a certain value in a certain column int row = p2da.getRowByCellValue(col, "Spot"); // then fetch some other cell from the same row System.out.println("SkillIndex: " + p2da.getData()[row][0]);
The ARE file is essentially a GFF file, but it has convenience methods that allow you to modify the data usually found in ARE files, without having to worry about fields or data type. The ARE file is an abstraction that tries to act in the way that feels most natural to a Java developer.
// read the ARE file URL input = Are.class.getResource("/tavern.are"); assertNotNull(input); File f = new java.io.File(input.toURI()); Are inFile = new Are(f); System.out.println("file: " + inFile.getFileType() + " : " + inFile.getFileVersion()); // compare chances of lightning and/or rain if (inFile.getChanceLightning() > inFile.getChanceRain()) { int chanceOfLightningWithoutRain = inFile.getChanceLightning() - inFile.getChanceRain(); } // make this an interior area inFile.setInterior(true);