Remember, for each lump's name, we need to stop reading bytes into our Swift string once we reach a null terminator or once we reach 8 bytes. The very first thing to do is create a data slice with the relevant data:

let nameData = currentDirectoryEntry.subdata(in: 8..<16)

Swift offers great support for C String interoperability. This means that to create a string we just need to hand the data to a String initializer:

let lumpName = String(data: nameData, encoding: String.Encoding.ascii)

This works, though the result is not correct. This method ignores the null terminator, so that all names, even the short ones, are converted to 8byte strings. As an example, the lump for the IMP character name becomes IMP00000. This happens because Doom fills the remaining 5 bytes with null characters and String(data:encoding:) does not interpret them but creates a string of the full 8 bytes of the nameData .

If we want to support null characters, Swift offers something else, a cString initializer which is defined for reading valid cStrings with null terminators:

// Produces a string containing the bytes in a given C array, // interpreted according to a given encoding. init?(cString: UnsafePointer<CChar>, encoding enc: String.Encoding)

Note that it doesn't require a data instance as its parameter but an unsafePointer to CChars instead. We already know how to do that, so lets write the code:

let lumpName2 = nameData.withUnsafeBytes({ (pointer: UnsafePointer<UInt8>) -> String? in return String(cString: UnsafePointer<CChar>(pointer), encoding: String.Encoding.ascii) })

This, again, doesn't work. In all cases where Doom's names are less than 8 characters, this code works flawlessly, but once we reach a 8 byte name without a null terminator, it will continue reading (into the next 16byte segment) until it finds the next valid null terminator. This results in long strings with random memory at the end.

Since this logic is custom to Doom, we also need to implement custom code. As Data supports Swift's collection & sequence operations, we can just solve this in terms of reduce:

let lumpName3Bytes = try nameData.reduce([UInt8](), { (a: [UInt8], b: UInt8) throws -> [UInt8] in guard b > 0 else { return a } guard a.count <= 8 else { return a } return a + [b] }) guard let lumpName3 = String(bytes: lumpName3Bytes, encoding: String.Encoding.ascii) else { throw WadReaderError.invalidLup(reason: "Could not decode lump name for bytes \(lumpName3Bytes)") }

This code just reduces over the UInt8 bytes of our data and checks whether we have an early null terminator. This code works, though it is not necessarily fast as the data has to be moved through several abstractions.

It would be better if we could solve this similarly to how the Doom engine does it. Doom just moves the pointer of the char* and checks for each char whether it is a null terminator in order to break early. As Doom is written in low level C code, it can just iterate over the raw pointer addresses.

How would we implement this logic in Swift? We can actually do something quite similar in Swift by, again, utilizing withUnsafeBytes . Lets see:

let finalLumpName = nameData.withUnsafeBytes({ (pointer: UnsafePointer<CChar>) -> String? in var localPointer = pointer for _ in 0..<8 { guard localPointer.pointee != CChar(0) else { break } localPointer = localPointer.successor() } let position = pointer.distance(to: localPointer) return String(data: nameData.subdata(in: 0..<position), encoding: String.Encoding.ascii) }) guard let lumpName4 = finalLumpName else { throw WadReaderError.invalidLup(reason: "Could not decode lump name for bytes \(lumpName3Bytes)") }

Similar to our earlier uses of withUnsafeBytes we're receiving a pointer to the raw memory. pointer is a let constant, but we need to modify the variable, which is why we create a local mutable version in the first line .

Afterwards, we're performing the main work. We loop from 0 to 8 and for each loop iteration we test whether the char that the pointer is pointing to (the pointee ) is equal to the null terminator ( CChar(0) ). If it is equal to the null terminator, this means that we found the null terminator early, and we break. If it is not equal to the null terminator, we overwrite localPointer with its successor, i.e. the next position in memory after the current pointer. That way, we're iterating byte by byte over the contents of our memory.

Once we're done, we calculate the distance between our original pointer and our localPointer . If we just advanced three times before finding a null terminator, the distance between the two pointers would be 3. This distance, finally, allows us to create a new String instance with the subdata of actual C String.

This allows us to create a new Lump struct with the required data:

lumps.append(Lump(filepos: lumpStart, size: lumpSize, name: lumpName4))

When you look into the source, you will see ominous references to F_START and F_END . Doom marks the beginning and end of special lump regions with empty lumps with magic names. F_START / F_END enclose all the floor texture lumps. We will ignore this additional step in this tutorial.

A screenshot from the final application:









Not really impressive, I know. One of the next installments on this blog might concentrate on how to display those textures.