I am trying to use a VB.NET program to retrieve the Mod_Name field from a Mod file.
I believe my initial issue is that I am unable to determine how to locate the start position of the IFO record within the file.
I have been using the NWN Omnibus to understand how these file are structured, but am too stupid to work out how to derive the start position of the IFI portion of the file.
Any help in understanding how I could achieve this will be very welcome and much appreciated.
Think I know what the problem is. You’re reading in old chars (which was 1 byte back in the day) like new chars (which are 2 bytes nowadays).
@pscythe
I may well be missing your something, but I am not sure that explains the problems. Accessing the Mod Description works perfectly. Also, the information as defined in the NWN Omnibus, specifies 1 byte rather than 2.
Have you been able to verify this?
Remember that NwN was built before the days of the various utf encodings. Internally NwN is still in ANSI/ASCII i.e. byte sized characters. I had a problem with editing 2da files recently, the fix for which was to tell Notepad++ to use ANSI encoding as the default as in one update or another they had switched to one the utf encodings.
Trent has said in the streams that they are looking into this but it is my guess that if they do change it, that it will be quite some time before it happens. It will involve quite a lot of work.
TR
@Tarot_Redhand That is exactly my point.
But somehow @Surazal manages to retrieve the ERF file description using BinaryReader.readChars which reads in 2-byte Unicode chars instead of the 1-byte ANSI char that NWN was using. Does VB’s Option Compare Text changes that to 1-byte chars?? Anyway I am wring a mod reader from scratch strictly using readBytes so there is no ambiguity.
I haven’t tried running your code but that was the first thing I spotted when studying it.
The other problem was that the code was trying to read the ERF resources list without going through the Keys list first to locate module.ifo. You had to search the keys list for resref module.ifo then that gives you a index into the resources list which would give you a offset into the actual IFO.
That’s good info - I will take another look at the Omnibus and see what I can work out. Many thanks.
According to my copy of a VB programmers reference we are dealing with streams which I never use. Reading about the ReadChars method it appears that it reads a single character and advances the reader’s position according to the streams encoding and the character. So it would seem that stream classes somehow know the encoding of the text being read in.
TR
The default encoding is actually UTF-8 which can be 1 to 6 bytes depending on the character itself. So this explains why it works for @Surazal as the module descriptions are in ASCII which is 1-byte chars.
anyway this is what I do to read 4-chars as NWN sees chars using the BinaryReader.
System.Text.Encoding.ASCII.GetString(reader.ReadBytes(4))
Okay, it is done.
F:\ModReader> ModReader.exe "To Heir is Human Custom.mod"
Module Description=
"To Heir is Human"
by BioWare
Set in the heart of Cormanthor, near Myth Drannor, a band of rangers, searching
for a kidnapped child, has been defeated by a drow army. The few remaining surv
ivors have given up hope of rescuing the heir to a barony. Who dares face the mi
ght of the drow to save an innocent child?
Recommended Levels: 5 - 15
Number of Players: 1 - 4
Mod_Name=To Heir is Human
The code is a bit messy but it was mostly a hack
Option Compare Text
Option Explicit On
'Option Strict On
Imports System.IO
Public Class ErfReader
Private Const BufferSize As Integer = 4096 * 2
Public reader As BinaryReader
Private fStream As FileStream
' For localised descriptions of the module
Private locStringCount As Int32 ' LanguageCount
Private locStringSize As Int32 ' LocalizedStringSize
Private locStringOffset As Int32
Private keysCount As Int32
Private keysOffset As Int32
Private resourcesOffset As Int32
' ERF keys have a fixed size
Private Const keySize = 16 + 4 + 2 + 2
Sub debug(str As String)
Console.WriteLine("Got: |" + str + "|")
End Sub
Function checkType(TypeStr As String)
Return (TypeStr = "MOD " Or TypeStr = "ERF " Or TypeStr = "SAV ")
End Function
Function checkVersion(VersionStr As String)
Return (VersionStr = "V1.0")
End Function
Public Function open(fname As String)
fStream = New FileStream(fname, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.RandomAccess)
reader = New BinaryReader(fStream)
Dim type As String = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(4))
Dim version As String = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(4))
' Not sure if these are signed?
locStringCount = reader.ReadInt32
locStringSize = reader.ReadInt32
keysCount = reader.ReadInt32
locStringOffset = reader.ReadInt32
keysOffset = reader.ReadInt32
resourcesOffset = reader.ReadInt32
' What is this??
'Dim descOffset As Int32 = reader.ReadInt32
Return checkType(type) And checkVersion(version)
End Function
Public Sub close()
reader.Close()
fStream.Close()
End Sub
Public Function GetDescription()
reader.BaseStream.Position = locStringOffset
Dim attempts As Int32 = 0
Do While attempts < locStringCount
Dim languageId As Int32 = reader.ReadInt32
Dim size As Int32 = reader.ReadInt32
Dim description As String = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(size))
If (languageId = 0) Then ' English
Return description
End If
attempts = attempts + 1
Loop
Return "***No English Description found for Module.***"
End Function
' Returns the ResId from KeyList for the given resref and type
Public Function FindFile(name0 As String, type As Integer)
Dim name As String = name0.ToLower
reader.BaseStream.Position = keysOffset
Dim seen As Int32 = 0
Do While seen < keysCount
Dim resref As String = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(16))
Dim resid As Int32 = reader.ReadInt32
Dim resType As Int16 = reader.ReadInt16
reader.ReadBytes(2) ' discard unused
If (resref = name And type = resType) Then
'Console.WriteLine(name + " of Type=" + type.ToString + " Found at " + seen.ToString + " with resId=" + resid.ToString)
Return resid
End If
'Console.WriteLine(resref + resType.ToString)
seen = seen + 1
Loop
Return -1
End Function
Private Const resourceElemSize = 4 + 4
' Returns the FileOffset for the given ResId
Public Function GetResourcePosition(ResId As Int32)
Dim resource = New ResourcePosition
reader.BaseStream.Position = resourcesOffset + ResId * resourceElemSize
resource.offset = reader.ReadInt32
resource.size = reader.ReadInt32
Return resource
End Function
End Class
Public Class ResourcePosition
Public offset As Int32 ' Offset from file
Public size As Int32
End Class
Public Class IfoReader
Private reader As BinaryReader
Private position As ResourcePosition
Private structOffset As UInt32
Private structCount As UInt32
Private fieldOffset As UInt32
Private fieldCount As UInt32
Private labelOffset As UInt32
Private labelCount As UInt32
Private fieldDataOffset As UInt32
Private fieldDataCount As UInt32
Private fieldIndicesOffset As UInt32
Private fieldIndicesCount As UInt32
Private listIndicesOffst As UInt32
Private listIndicesCount As UInt32
Public Sub New(r As BinaryReader, resPos As ResourcePosition)
reader = r
position = resPos
End Sub
' Res is pointing to the module.IFO within the ERF
Public Function ReadHeader()
reader.BaseStream.Position = position.offset
Dim type As String = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(4))
Dim version As String = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(4))
structOffset = reader.ReadUInt32
structCount = reader.ReadUInt32
fieldOffset = reader.ReadUInt32 ' field array
fieldCount = reader.ReadUInt32
labelOffset = reader.ReadUInt32 ' label array
labelCount = reader.ReadUInt32
fieldDataOffset = reader.ReadUInt32
fieldDataCount = reader.ReadUInt32
fieldIndicesOffset = reader.ReadUInt32
fieldIndicesCount = reader.ReadUInt32
listIndicesOffst = reader.ReadUInt32
listIndicesCount = reader.ReadUInt32
Return checkIfoHeader(type, version)
End Function
Public Function GetModName() As Object
If Not ReadHeader() Then
Console.WriteLine("Not IFO file")
End If
'Goto the very first struct
reader.BaseStream.Position = position.offset + structOffset
Dim structType As Int32 = reader.ReadInt32 ' structType is actually signed
Dim structDataOffset As UInt32 = reader.ReadUInt32 ' index into field indices array
Dim structFieldCount As UInt32 = reader.ReadUInt32
If Not structType = &HFFFFFFFF Then
Console.WriteLine("**** Failed to find top struct !!!")
End If
'Console.WriteLine("struct offset=" + structOffset.ToString)
'Console.WriteLine("struct type=" + structType.ToString)
'Console.WriteLine("struct count=" + structCount.ToString)
'Console.WriteLine("struct field count=" + structFieldCount.ToString)
'Console.WriteLine("struct data offset=" + structDataOffset.ToString)
' Looking into the field indices array
reader.BaseStream.Position = position.offset + fieldIndicesOffset
' retrieve all the indices first
Dim seen As Int32 = 0
Dim indices As List(Of UInt32) = New List(Of UInt32)
Do While seen < structFieldCount
Dim index As UInt32 = reader.ReadUInt32
indices.Add(index)
'Console.WriteLine("field index=" + index.ToString)
seen = seen + 1
Loop
' now inspect the fields array
Dim currPosition As UInt32 = position.offset + fieldOffset
For Each index In indices
reader.BaseStream.Position = currPosition
Dim fieldType As Int32 = reader.ReadInt32 ' signed
Dim fieldLabelIndex As UInt32 = reader.ReadUInt32 ' index into label array
Dim fieldDataOffset As UInt32 = reader.ReadUInt32 ' byte offset into Field Data Block
currPosition = reader.BaseStream.Position
Dim label = GetLabel(fieldLabelIndex)
'Console.WriteLine("Label = " + label)
If (label = "Mod_Name") Then
Return GetFieldDataCExoLocString(fieldDataOffset)
End If
Next
Return "*** Mod_Name field not found ***"
End Function
Private Function GetFieldDataCExoLocString(offset As UInteger) As String
reader.BaseStream.Position = position.offset + fieldDataOffset + offset
Dim totalSize As UInt32 = reader.ReadUInt32 ' total size not including totalSize itself
Dim strref As UInt32 = reader.ReadUInt32 ' into dialog.tlk
Dim count As UInt32 = reader.ReadUInt32 ' Number of substrings here
'Console.WriteLine(count.ToString + " substrings")
Dim seen As UInt32 = 0
Do While seen < count
Dim stringId = reader.ReadUInt32 ' unsigned?
Dim stringLen = reader.ReadUInt32 ' unsigned ?
Dim contents = System.Text.Encoding.ASCII.GetString(reader.ReadBytes(stringLen))
'Console.WriteLine("stringId=" + stringId.ToString + " |" + contents + "|")
If (stringId = 0) Then
Return contents
End If
seen = seen + 1
Loop
Return "*** Mod_Name value not found ***"
End Function
Private Const labelSize = 16
Private Function GetLabel(offset As UInt32)
reader.BaseStream.Position = position.offset + labelOffset + offset * labelSize
Return System.Text.Encoding.ASCII.GetString(reader.ReadBytes(labelSize))
End Function
Function checkIfoHeader(type As String, version As String)
Return (type = "IFO " And version = "V3.2")
End Function
End Class
Public Module ModReader
Public Sub main()
Dim arguments As String() = Environment.GetCommandLineArgs()
Dim reader As ErfReader = New ErfReader
If (Not reader.open(arguments(1))) Then
Console.WriteLine("wrong right file")
Return
End If
Console.WriteLine("Module Description=")
Console.WriteLine(reader.GetDescription)
Console.WriteLine()
Dim resId = reader.FindFile("module", 2014) ' 2014 for IFOs
Dim resPos = reader.GetResourcePosition(resId)
Dim ifo = New IfoReader(reader.reader, resPos)
Console.WriteLine("Mod_Name=" + ifo.GetModName)
End Sub
End Module
The Bioware docs are fairly reliable as far as this task is concern. The only mistake was in the GFF docs where struct-type is actually a signed 32bit int as it is used rather than the unisgned one it says in places.
@pscythe
Thanks for this and all the effort you have made to help me - very much appreciated. Will take me some time to work through the code so I understand it. I had managed to compute the IFO location before you submitted your code, but was stuck on processing the IFO information - so getting your solution is very welcome.
Once I understand your code, I will integrate into NIT and then start changing how the .sav name is mapped to the NIT mod. Will also have to code some version migration code to tidy up what is no longer needed.
I will release the automated .sav to Mod name change before starting to think about a Basic user interface option.
1 Like
Let me if you got any questions. The GFF is a whole lot of indirection. LOL
I have been using a modified version of @pscythe’s code, which seems to work for almost all cases. However, there are some exceptions to reliably getting the Mod_Name and Mod_Description fields.
In particular, The_Lord_of_Terror_2_0_5.mod and some of the descriptions for the NWM modules.
Does anyone have a routine to reliably get the Mod_Name and Mod_Description information for module files? If not, is anyone prepared to write one or help me see where I am going wrong?
If interested, you can download the Visual Basic source.
Many thanks for any help and suggestions.