.wad file format of id Tech 1: Ruby parsing library

Application

id Tech 1

File extension

wad

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of .wad file format of id Tech 1 using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Runtime library

All parsing code for Ruby generated by Kaitai Struct depends on the Ruby runtime library. You have to install it before you can parse data.

The Ruby runtime library can be installed from RubyGems:

gem install kaitai-struct

Code

Parse a local file and get structure in memory:

data = DoomWad.from_file("path/to/local/file.wad")

Or parse structure from a string of bytes:

bytes = "\x00\x01\x02..."
data = DoomWad.new(Kaitai::Struct::Stream.new(bytes))

After that, one can get various attributes from the structure by invoking getter methods like:

data.num_index_entries # => Number of entries in the lump index

Ruby source code to parse .wad file format of id Tech 1

doom_wad.rb

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

require 'kaitai/struct/struct'

unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.9')
  raise "Incompatible Kaitai Struct Ruby API: 0.9 or later is required, but you have #{Kaitai::Struct::VERSION}"
end

class DoomWad < Kaitai::Struct::Struct
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @magic = (@_io.read_bytes(4)).force_encoding("ASCII")
    @num_index_entries = @_io.read_s4le
    @index_offset = @_io.read_s4le
    self
  end
  class Sectors < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @entries = []
      i = 0
      while not @_io.eof?
        @entries << Sector.new(@_io, self, @_root)
        i += 1
      end
      self
    end
    attr_reader :entries
  end
  class Vertex < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @x = @_io.read_s2le
      @y = @_io.read_s2le
      self
    end
    attr_reader :x
    attr_reader :y
  end

  ##
  # Used for TEXTURE1 and TEXTURE2 lumps, which designate how to
  # combine wall patches to make wall textures. This essentially
  # provides a very simple form of image compression, allowing
  # certain elements ("patches") to be reused / recombined on
  # different textures for more variety in the game.
  # @see https://doom.fandom.com/wiki/TEXTURE1_and_TEXTURE2 Source
  class Texture12 < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @num_textures = @_io.read_s4le
      @textures = []
      (num_textures).times { |i|
        @textures << TextureIndex.new(@_io, self, @_root)
      }
      self
    end
    class TextureIndex < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @offset = @_io.read_s4le
        self
      end
      def body
        return @body unless @body.nil?
        _pos = @_io.pos
        @_io.seek(offset)
        @body = TextureBody.new(@_io, self, @_root)
        @_io.seek(_pos)
        @body
      end
      attr_reader :offset
    end
    class TextureBody < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @name = (Kaitai::Struct::Stream::bytes_strip_right(@_io.read_bytes(8), 0)).force_encoding("ASCII")
        @masked = @_io.read_u4le
        @width = @_io.read_u2le
        @height = @_io.read_u2le
        @column_directory = @_io.read_u4le
        @num_patches = @_io.read_u2le
        @patches = []
        (num_patches).times { |i|
          @patches << Patch.new(@_io, self, @_root)
        }
        self
      end

      ##
      # Name of a texture, only `A-Z`, `0-9`, `[]_-` are valid
      attr_reader :name
      attr_reader :masked
      attr_reader :width
      attr_reader :height

      ##
      # Obsolete, ignored by all DOOM versions
      attr_reader :column_directory

      ##
      # Number of patches that are used in a texture
      attr_reader :num_patches
      attr_reader :patches
    end
    class Patch < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @origin_x = @_io.read_s2le
        @origin_y = @_io.read_s2le
        @patch_id = @_io.read_u2le
        @step_dir = @_io.read_u2le
        @colormap = @_io.read_u2le
        self
      end

      ##
      # X offset to draw a patch at (pixels from left boundary of a texture)
      attr_reader :origin_x

      ##
      # Y offset to draw a patch at (pixels from upper boundary of a texture)
      attr_reader :origin_y

      ##
      # Identifier of a patch (as listed in PNAMES lump) to draw
      attr_reader :patch_id
      attr_reader :step_dir
      attr_reader :colormap
    end

    ##
    # Number of wall textures
    attr_reader :num_textures
    attr_reader :textures
  end
  class Linedef < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @vertex_start_idx = @_io.read_u2le
      @vertex_end_idx = @_io.read_u2le
      @flags = @_io.read_u2le
      @line_type = @_io.read_u2le
      @sector_tag = @_io.read_u2le
      @sidedef_right_idx = @_io.read_u2le
      @sidedef_left_idx = @_io.read_u2le
      self
    end
    attr_reader :vertex_start_idx
    attr_reader :vertex_end_idx
    attr_reader :flags
    attr_reader :line_type
    attr_reader :sector_tag
    attr_reader :sidedef_right_idx
    attr_reader :sidedef_left_idx
  end

  ##
  # @see https://doom.fandom.com/wiki/PNAMES Source
  class Pnames < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @num_patches = @_io.read_u4le
      @names = []
      (num_patches).times { |i|
        @names << (Kaitai::Struct::Stream::bytes_strip_right(@_io.read_bytes(8), 0)).force_encoding("ASCII")
      }
      self
    end

    ##
    # Number of patches registered in this global game directory
    attr_reader :num_patches
    attr_reader :names
  end
  class Thing < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @x = @_io.read_s2le
      @y = @_io.read_s2le
      @angle = @_io.read_u2le
      @type = @_io.read_u2le
      @flags = @_io.read_u2le
      self
    end
    attr_reader :x
    attr_reader :y
    attr_reader :angle
    attr_reader :type
    attr_reader :flags
  end
  class Sector < Kaitai::Struct::Struct

    SPECIAL_SECTOR = {
      0 => :special_sector_normal,
      1 => :special_sector_d_light_flicker,
      2 => :special_sector_d_light_strobe_fast,
      3 => :special_sector_d_light_strobe_slow,
      4 => :special_sector_d_light_strobe_hurt,
      5 => :special_sector_d_damage_hellslime,
      7 => :special_sector_d_damage_nukage,
      8 => :special_sector_d_light_glow,
      9 => :special_sector_secret,
      10 => :special_sector_d_sector_door_close_in_30,
      11 => :special_sector_d_damage_end,
      12 => :special_sector_d_light_strobe_slow_sync,
      13 => :special_sector_d_light_strobe_fast_sync,
      14 => :special_sector_d_sector_door_raise_in_5_mins,
      15 => :special_sector_d_friction_low,
      16 => :special_sector_d_damage_super_hellslime,
      17 => :special_sector_d_light_fire_flicker,
      18 => :special_sector_d_damage_lava_wimpy,
      19 => :special_sector_d_damage_lava_hefty,
      20 => :special_sector_d_scroll_east_lava_damage,
      21 => :special_sector_light_phased,
      22 => :special_sector_light_sequence_start,
      23 => :special_sector_light_sequence_special1,
      24 => :special_sector_light_sequence_special2,
    }
    I__SPECIAL_SECTOR = SPECIAL_SECTOR.invert
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @floor_z = @_io.read_s2le
      @ceil_z = @_io.read_s2le
      @floor_flat = (@_io.read_bytes(8)).force_encoding("ASCII")
      @ceil_flat = (@_io.read_bytes(8)).force_encoding("ASCII")
      @light = @_io.read_s2le
      @special_type = Kaitai::Struct::Stream::resolve_enum(SPECIAL_SECTOR, @_io.read_u2le)
      @tag = @_io.read_u2le
      self
    end
    attr_reader :floor_z
    attr_reader :ceil_z
    attr_reader :floor_flat
    attr_reader :ceil_flat

    ##
    # Light level of the sector [0..255]. Original engine uses
    # COLORMAP to render lighting, so only 32 actual levels are
    # available (i.e. 0..7, 8..15, etc).
    attr_reader :light
    attr_reader :special_type

    ##
    # Tag number. When the linedef with the same tag number is
    # activated, some effect will be triggered in this sector.
    attr_reader :tag
  end
  class Vertexes < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @entries = []
      i = 0
      while not @_io.eof?
        @entries << Vertex.new(@_io, self, @_root)
        i += 1
      end
      self
    end
    attr_reader :entries
  end
  class Sidedef < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @offset_x = @_io.read_s2le
      @offset_y = @_io.read_s2le
      @upper_texture_name = (@_io.read_bytes(8)).force_encoding("ASCII")
      @lower_texture_name = (@_io.read_bytes(8)).force_encoding("ASCII")
      @normal_texture_name = (@_io.read_bytes(8)).force_encoding("ASCII")
      @sector_id = @_io.read_s2le
      self
    end
    attr_reader :offset_x
    attr_reader :offset_y
    attr_reader :upper_texture_name
    attr_reader :lower_texture_name
    attr_reader :normal_texture_name
    attr_reader :sector_id
  end
  class Things < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @entries = []
      i = 0
      while not @_io.eof?
        @entries << Thing.new(@_io, self, @_root)
        i += 1
      end
      self
    end
    attr_reader :entries
  end
  class Linedefs < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @entries = []
      i = 0
      while not @_io.eof?
        @entries << Linedef.new(@_io, self, @_root)
        i += 1
      end
      self
    end
    attr_reader :entries
  end
  class IndexEntry < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @offset = @_io.read_s4le
      @size = @_io.read_s4le
      @name = (Kaitai::Struct::Stream::bytes_strip_right(@_io.read_bytes(8), 0)).force_encoding("ASCII")
      self
    end
    def contents
      return @contents unless @contents.nil?
      io = _root._io
      _pos = io.pos
      io.seek(offset)
      case name
      when "SECTORS"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Sectors.new(_io__raw_contents, self, @_root)
      when "TEXTURE1"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Texture12.new(_io__raw_contents, self, @_root)
      when "VERTEXES"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Vertexes.new(_io__raw_contents, self, @_root)
      when "BLOCKMAP"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Blockmap.new(_io__raw_contents, self, @_root)
      when "PNAMES"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Pnames.new(_io__raw_contents, self, @_root)
      when "TEXTURE2"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Texture12.new(_io__raw_contents, self, @_root)
      when "THINGS"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Things.new(_io__raw_contents, self, @_root)
      when "LINEDEFS"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Linedefs.new(_io__raw_contents, self, @_root)
      when "SIDEDEFS"
        @_raw_contents = io.read_bytes(size)
        _io__raw_contents = Kaitai::Struct::Stream.new(@_raw_contents)
        @contents = Sidedefs.new(_io__raw_contents, self, @_root)
      else
        @contents = io.read_bytes(size)
      end
      io.seek(_pos)
      @contents
    end
    attr_reader :offset
    attr_reader :size
    attr_reader :name
    attr_reader :_raw_contents
  end
  class Sidedefs < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @entries = []
      i = 0
      while not @_io.eof?
        @entries << Sidedef.new(@_io, self, @_root)
        i += 1
      end
      self
    end
    attr_reader :entries
  end
  class Blockmap < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @origin_x = @_io.read_s2le
      @origin_y = @_io.read_s2le
      @num_cols = @_io.read_s2le
      @num_rows = @_io.read_s2le
      @linedefs_in_block = []
      ((num_cols * num_rows)).times { |i|
        @linedefs_in_block << Blocklist.new(@_io, self, @_root)
      }
      self
    end
    class Blocklist < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @offset = @_io.read_u2le
        self
      end

      ##
      # List of linedefs found in this block
      def linedefs
        return @linedefs unless @linedefs.nil?
        _pos = @_io.pos
        @_io.seek((offset * 2))
        @linedefs = []
        i = 0
        begin
          _ = @_io.read_s2le
          @linedefs << _
          i += 1
        end until _ == -1
        @_io.seek(_pos)
        @linedefs
      end

      ##
      # Offset to the list of linedefs
      attr_reader :offset
    end

    ##
    # Grid origin, X coord
    attr_reader :origin_x

    ##
    # Grid origin, Y coord
    attr_reader :origin_y

    ##
    # Number of columns
    attr_reader :num_cols

    ##
    # Number of rows
    attr_reader :num_rows

    ##
    # Lists of linedefs for every block
    attr_reader :linedefs_in_block
  end
  def index
    return @index unless @index.nil?
    _pos = @_io.pos
    @_io.seek(index_offset)
    @index = []
    (num_index_entries).times { |i|
      @index << IndexEntry.new(@_io, self, @_root)
    }
    @_io.seek(_pos)
    @index
  end
  attr_reader :magic

  ##
  # Number of entries in the lump index
  attr_reader :num_index_entries

  ##
  # Offset to the start of the index
  attr_reader :index_offset
end