Data 解析 Doom 的 WAD 文件
Swift 3 : 从 NSData 到 Data 的转变Swift 3 带来了许多大大小小的变化。其中一个是为常见的 Foundation 引用类型(例如将 NSData 封装成 不仅如此,在学习完基础知识之后,我们还会写一个简单的示例应用。这个应用会读取和解析一个 Doom 毁灭战士的 WAD 文件 2。
基本区别对于 func writeToURL(_ url: NSURL,atomically atomically: Bool) -> Bool func writeToURL(_ url: NSURL,options writeOptionsMask: NSDataWritingOptions) throws // ... (implementations for file: String instead of NSURL) init?(contentsOfURL url: NSURL) init(contentsOfURL url: NSURL,options readOptionsMask: NSDataReadingOptions) throws // ... (implementations for file: String instead of NSURL) 基本的使用方法并没有什么改动。新的 init(contentsOf: URL,options: ReadingOptions) func write(to: URL,options: WritingOptions) 留意到 比较一下
这给 func distance(from: Int,to: Int) func dropFirst(Int) func dropLast(Int) func filter((UInt8) -> Bool) func flatMap<ElementOfResult>((UInt8) -> ElementOfResult?) func forEach((UInt8) -> Void) func index(Int,offsetBy: Int,limitedBy: Int) func map<T>((UInt8) -> T) func max() func min() func partition() func prefix(Int) func reversed() func sort() func sorted() func split(separator: UInt8,maxSplits: Int,omittingEmptySubsequences: Bool) func reduce<Result>(Result,(partialResult: Result,UInt8) -> Result) 如你所见,许多函数式方法,例如 mapping 和 filtering 现在都可以操作 var data = Data(bytes: [0x00,0x01,0x02,0x03]) print(data[2]) // 2 data[2] = 0x09 print (data == Data(bytes: [0x00,0x09,0x03])) // true
init(bytes: Array<UInt8>) init<SourceType>(buffer: UnsafeMutableBufferPointer<SourceType>) init(repeating: UInt8,count: Int) 获取字节如果你使用 // NSData func getBytes(_ buffer: UnsafeMutablePointer<Void>,length length: Int)
该文件包含了一个四字节字符串 ABCD 标签,用来表示正确的文件类型(做校验)。接着的四字节定义了实际数据(例如头部的结束和项目的开始),头部最后的四字节定义了该文件存储项目的数量。 用 let data = ... var length: UInt32 = 0 var start: UInt32 = 0 data.getBytes(&start,range: NSRange(location: 4,length: 4)) data.getBytes(&length,range: NSRange(location: 8,length: 4)) 如此将返回正确结果3。如果数据不包含 C 字符串,方法会更简单。你可以直接用正确的字段定义一个
let data = ... struct Header { let start: UInt32 let length: UInt32 } var header = Header(start: 0,length: 0) data.getBytes(&header,range: NSRange(location: 0,length: 8)) Data 中 getBytes 的替代方案不过 // 从数据里获得字节 func withUnsafeBytes<ResultType,ContentType>((UnsafePointer<ContentType>) -> ResultType) 通过这个方法,我们可以从闭包中直接读取数据的字节内容。来看一个简单的例子: let data = Data(bytes: [0x01,0x03]) data.withUnsafeBytes { (pointer: UnsafePointer<UInt8>) -> Void in print(pointer) print(pointer.pointee) } // 打印 // : 0x00007f8dcb77cc50 // : 1 好了,现在有一个指向数据的 unsafe UInt8 指针,那要怎样利用起来呢?首先,我们需要一个不同的数据类型,然后一定要确定该数据的类型。我们知道这段数据包含一个 Int32 类型,那该如何正确地解码呢? 既然已经有了一个 unsafe pointer(UInt8 类型),那么就能够轻松地转换成目标类型 unsafe pointer。 let data = Data(bytes: [0x00,0x00,0x00]) let result = data.withUnsafeBytes { (pointer: UnsafePointer<Int32>) -> Int32 in return pointer.pointee } print(result) //: 256 如你所见,我们创建了一个字节的 let result: Int32 = data.withUnsafeBytes { $0.pointee } 数据的生命周期使用
泛型解决方案现在,我们已经可以读取原始字节数据,并把它们转换成正确的类型了。接下来创建一个通用的方法来更轻松地执行操作,而不用额外地关心语法。 另外,我们暂时还无法针对数据的子序列执行操作,而只能对整个 extension Data { func scanValue<T>(start: Int,length: Int) -> T { return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee } } } let data = Data(bytes: [0x01,0x02]) let a: Int16 = data.scanValue(start: 0,length: 1) print(a) // : 1 与之前的代码相比,存在两个显著的不同点:
数据转换另一方面,从现有的变量内容里得到 var variable = 256 let data = Data(buffer: UnsafeBufferPointer(start: &variable,count: 1)) print(data) // : <00010000 00000000> 解析 Doom WAD 文件我小时候非常热爱 Doom(毁灭战士)这个游戏。也玩到了很高的等级,并修改 WAD 文件加入了新的精灵,纹理等。因此当我想给解析二进制文件找一个合适(和简单)的例子时,就想起了 WAD 文件的设计。因为它十分直观且容易实现。于是我写了一个简单的小程序,用于读取 WAD 文件,然后列出所有存储地板的纹理名称 4。 我把源代码放在了 GitHub。
但是对于这个简单的示例,只需要了解部分的文件格式就够了。
开头的 4 字节用来确定文件格式。 解析头文件读取 WAD 文件的方法非常简单: let data = try Data(contentsOf: wadFileURL,options: .alwaysMapped) 我们获取到数据之后,首先需要解析头文件。这里多次使用了之前创建的 public func validateWadFile() throws { // 一些 Wad 文件定义 let wadMaxSize = 12,wadLumpsStart = 4,wadDirectoryStart = 8,wadDefSize = 4 // WAD 文件永远以 12 字节的头文件开始。 guard data.count >= wadMaxSize else { throw WadReaderError.invalidWadFile(reason: "File is too small") } // 它包含了三个值: // ASCII 字符 "IWAD" 或 "PWAD" 定义了 WAD 是 IWAD 还是 PWAD。 let validStart = "IWAD".data(using: String.Encoding.ascii)! guard data.subdata(in: 0..<wadDefSize) == validStart else { throw WadReaderError.invalidWadFile(reason: "Not an IWAD") } // 一个声明了 WAD 中区块数目的整数。 let lumpsInteger: Int32 = data.scanValue(start: wadLumpsStart,length: wadDefSize) // 一个整数,含有指向目录地址的指针。 let directoryInteger: Int32 = data.scanValue(start: wadDirectoryStart,length: wadDefSize) guard lumpsInteger > 0 && directoryInteger > Int32(wadMaxSize) else { throw WadReaderError.invalidWadFile(reason: "Empty Wad File") } } 你可以在 GitHub 找到其他的类型(例如 解析目录目录与区块的名字、包含的数据相关联。它包括了一系列的项目,每个项目的长度为 16 字节。目录的长度取决于 WAD 头文件里给出的数字。 每个 16 字节的项目按照以下的格式:
名字的字符定义得比较复杂。文档是这么说的:
留意最后一句话。在 C 语言里,字符串由空字符(
看看上面的表格, 短名字会在字符串最后补空字符(位置 3)。长名字则没有空字符,而是以 FLOOR4_5 的最后一个字符 5 作为结束。 在我们尝试支持区块的名字字符格式之前,首先处理一下简单的部分。那就是读取开头和大小。 在开始之前,我们应该定义一个数据结构,用于保存从目录里读取的内容: public struct Lump { public let filepos: Int32 public let size: Int32 public let name: String } 然后,从完整的数据实例里取出数据片段,这是这些数据构成我们的目录。 // 定义一个目录项的默认大小。 let wadDirectoryEntrySize = 16 // 从完整数据里提取目录片段。 let directory = data.subdata(in: Int(directoryLocation)..<(Int(directoryLocation) + Int(numberOfLumps) * wadDirectoryEntrySize)) 接着,我们以每段 16 字节的长度在 for currentIndex in stride(from: 0,to: directory.count,by: wadDirectoryEntrySize) { let currentDirectoryEntry = directory.subdata(in: currentIndex..<currentIndex+wadDirectoryEntrySize) // 一个整数表明区块数据的起始在文件中的位置。 let lumpStart: Int32 = currentDirectoryEntry.scanValue(start: 0,length: 4) // 一个表示了区块字节大小的整数。 let lumpSize: Int32 = currentDirectoryEntry.scanValue(start: 4,length: 4) ... } 简单的部分到此结束,下面我们要开始进入秋名山飙车了。 解析 C 字符串要知道对于每个区块的名字,每当遇到空的结束字符或者达到 8 字节的时候,我们都要停止向 Swift 字符串的写入。首要任务是利用相关数据创建一个数据片段。 let nameData = currentDirectoryEntry.subdata(in: 8..<16) Swift 给 C 字符串提供了很好的互操作性。这意味着需要创建一个字符串的时候,我们只需要把数据交给 let lumpName = String(data: nameData,encoding: String.Encoding.ascii) 这个方法可以执行,但是结果并不正确。因为它忽略了空结束符,所以即使是短名字,也会跟长名字一样转换成 8 字节的字符串。例如,名字为 IMP 的区块会变成 IMP00000。但是由于 如果我们想要支持空字符, Swift 提供了一个 // 根据所给的 C 数组创建字符串 // 根据所给的编码方式编码 init?(cString: UnsafePointer<CChar>,encoding enc: String.Encoding) 留意这里的参数不需要传入 let lumpName2 = nameData.withUnsafeBytes({ (pointer: UnsafePointer<UInt8>) -> String? in return String(cString: UnsafePointer<CChar>(pointer),encoding: String.Encoding.ascii) }) 以上方法依然不能得到我们想要的结果。在 Doom 的名字长度小于 8 字符的情况下,这段代码都能完美运行。但是只要某个名字长度达到 8 字节而没有一个空结束符时,这会继续读取(变成一个 16 字节片段),直到找到下一个有效的空结束符。 这就带来一些不确定长度的长字符串。 这个逻辑是 Doom 自定义的,因此我们需要自己来实现相应的代码。 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)") } 这段代码把数据以 不过如果我们能以 Doom 引擎类似的方法来解决的话,效果会更好。Doom 仅移动了 那么我们要怎样在 Swift 里实现这个逻辑呢?事实上,可以再次借助 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)") }
接着,开始我们的主要工作。从 0 到 8 循环,每次循环都检测指针指向的字符( 完成之后 ,就计算一下我们原始 最后用得到的数据创建新的 lumps.append(Lump(filepos: lumpStart,size: lumpSize,name: lumpName4)) 如果你观察源代码,会发现 应用最终的截图:
我知道这看起来并不酷炫。之后可能会计划在博客里写写如何展示那些纹理。 桥接 NSData我发现新的
// 创建一个 Data 结构体 let aDataStruct = Data() // 获得底层的引用类型 NSData let aDataReference = aDataStruct as NSData 无论何时,如果你觉得 1: 有些类型(例如 2: Doom1,Doom2,Hexen,Heretic,还有 Ultimate Doom。虽然我只在 Doom1 Shareware 验证过。</sup 3: 留意,我们并没有验证最开头的 4 个字节,确保这的确是 ABCD 文件。但是要添加这个验证也很简单。</sup 4: 其实我也想展示 texture 但是不够时间去实现。</sup 5: Swift 3 不再在闭包和函数体里支持有用的
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |