class ElderScrollsPlugin

Constants

KNOWN_FIELDS
KNOWN_GRUP_RECORDS_WITHOUT_FIELDS
KNOWN_GRUP_RECORDS_WITH_FIELDS
VERSION

Attributes

chunks_tree[R]

Hash< Chunk or nil, Array<Chunk> >: The chunks tree, with nil being the root node

masters[R]

Array<String>: Ordered list of masters

unknown_chunks[R]

Array<Riffola::Chunk>: Unknown chunks encountered

Public Class Methods

current_esp() click to toggle source

Get the current esp being read (useful for BinData decoding types that depend on the esp)

Result
  • ElderScrollsPlugin: The current esp

# File lib/elder_scrolls_plugin.rb, line 19
def self.current_esp
  @esp
end
current_esp=(esp) click to toggle source

Set the current esp being read (useful for BinData decoding types that depend on the esp)

Parameters
# File lib/elder_scrolls_plugin.rb, line 11
def self.current_esp=(esp)
  @esp = esp
end
new(file_name, decode_only_tes4: false, ignore_unknown_chunks: false, decode_fields: true, warnings: true, debug: false) click to toggle source

Constructor

Parameters
  • file_name (String): ESP file name

  • decode_only_tes4 (Boolean): Do we decode only the TES4 header? [default: false]

  • ignore_unknown_chunks (Boolean): Do we ignore unknown chunks? [default: false]

  • decode_fields (Boolean): Do we decode fields content? [default: true]

  • warnings (Boolean): Do we activate warnings? [default: true]

  • debug (Boolean): Do we activate debugging logs? [default: false]

# File lib/elder_scrolls_plugin.rb, line 672
def initialize(file_name, decode_only_tes4: false, ignore_unknown_chunks: false, decode_fields: true, warnings: true, debug: false)
  @file_name = file_name
  @decode_only_tes4 = decode_only_tes4
  @ignore_unknown_chunks = ignore_unknown_chunks
  @decode_fields = decode_fields
  @warnings = warnings
  @debug = debug
  # Get the list of masters
  @masters = []
  # Internal mapping of first 2 digits of a FormID to the corresponding master name
  @master_ids = {}
  # List of form ids being defined
  @form_ids = []
  # Unknown chunks encountered during decoding
  @unknown_chunks = []
  # Configure the current parser
  ElderScrollsPlugin.current_esp = self
  # Tree of chunks (nil for root)
  # Hash< Chunk or nil, Array<Chunk> >
  @chunks_tree = {}
  chunks = Riffola.read(@file_name, chunks_format: {
    '*' => { header_size: 8 },
    'TES4' => { header_size: 16 },
    'GRUP' => { data_size_correction: -24, header_size: 16 }
  }, debug: @debug, warnings: @warnings) do |chunk|
    # Decode the TES4 to get the masters
    read_chunk(chunk) if chunk.name == 'TES4'
    !decode_only_tes4 || chunk.name != 'TES4'
  end
  # We just finished parsing TES4, update the masters index
  @master_ids.merge!(sprintf('%.2x', @master_ids.size) => File.basename(@file_name))
  @chunks_tree[nil] = chunks
  unless decode_only_tes4
    chunks.each do |chunk|
      # Don't read TES4 twice, especially because we already have our master IDs parsed
      read_chunk(chunk) unless chunk.name == 'TES4'
    end
  end
end

Public Instance Methods

absolute_form_id(form_id) click to toggle source

Convert a Form ID into its absolute form. An absolute form ID is not dependent on the order of the masters and includes the master name.

Parameters
  • form_id (String): The original form ID

Result
  • String: The absolute Form ID

# File lib/elder_scrolls_plugin.rb, line 778
def absolute_form_id(form_id)
  "#{@master_ids.key?(form_id[0..1]) ? @master_ids[form_id[0..1]] : "!!!#{form_id[0..1]}"}/#{form_id[2..7]}"
end
dump(chunk = nil, output_prefix = '') click to toggle source

Output a node of the chunks tree

Parameters
  • chunk (Riffola::Chunk or nil): The node to be dumped, or nil for root [default = nil]

  • output_prefix (String): Output prefix [default = '']

# File lib/elder_scrolls_plugin.rb, line 717
def dump(chunk = nil, output_prefix = '')
  esp_info = chunk.nil? ? nil : chunk.instance_variable_get(:@esp_info)
  sub_chunks = @chunks_tree[chunk]
  puts "#{output_prefix}+- #{chunk.nil? ? 'ROOT' : "#{chunk.name}#{esp_info[:description].nil? ? '' : " - #{esp_info[:description]}"}"}#{sub_chunks.empty? ? '' : " (#{sub_chunks.size} sub-chunks)"}"
  sub_chunks.each.with_index do |sub_chunk, idx_sub_chunk|
    dump(sub_chunk, "#{output_prefix}#{idx_sub_chunk == sub_chunks.size - 1 ? ' ' : '|'}  ")
  end
end
dump_absolute_form_ids() click to toggle source

Dump absolute Form IDs

# File lib/elder_scrolls_plugin.rb, line 734
def dump_absolute_form_ids
  @form_ids.sort.each do |form_id|
    puts "* [#{form_id}] - #{absolute_form_id(form_id)}"
  end
end
dump_masters() click to toggle source

Dump masters

# File lib/elder_scrolls_plugin.rb, line 727
def dump_masters
  @masters.each.with_index do |master, idx|
    puts "* [#{sprintf('%.2x', idx)}] - #{master}"
  end
end
to_json(chunk = nil) click to toggle source

Return the esp content as JSON

Parameters
  • chunk (Riffola::Chunk or nil): The node to be dumped, or nil for root [default = nil]

Result
  • Hash: JSON object

# File lib/elder_scrolls_plugin.rb, line 746
def to_json(chunk = nil)
  esp_info = chunk.nil? ? { type: :root, description: 'root' } : chunk.instance_variable_get(:@esp_info)
  json = {
    name: chunk.nil? ? 'ROOT' : chunk.name
  }
  json[:type] = esp_info[:type] unless esp_info[:type].nil?
  json[:description] = esp_info[:description] unless esp_info[:description].nil?
  json[:decoded_data] = esp_info[:decoded_data] unless esp_info[:decoded_data].nil?
  json[:decoded_header] = esp_info[:decoded_header] unless esp_info[:decoded_header].nil?
  unless chunk.nil?
    if esp_info[:decoded_header].nil?
      header = chunk.header
      json[:header] = (header.ascii_only? ? header : Base64.encode64(header)) unless header.empty?
    end
    if esp_info[:type] == :field && esp_info[:decoded_data].nil?
      data = chunk.data
      json[:data] = (data.ascii_only? ? data : Base64.encode64(data))
    end
  end
  json[:sub_chunks] = @chunks_tree[chunk].
    map { |sub_chunk| to_json(sub_chunk) }.
    sort_by { |chunk_json| [chunk_json[:name], chunk_json[:description], chunk_json[:data]] } if @chunks_tree[chunk].size > 0
  json
end

Private Instance Methods

read_chunk(chunk) click to toggle source

Read a given chunk info

Parameters
  • chunk (Riffola::Chunk): Chunk to be read

# File lib/elder_scrolls_plugin.rb, line 788
def read_chunk(chunk)
  puts "[ESP DEBUG] - Read chunk #{chunk.name}..." if @debug
  description = nil
  decoded_data = nil
  subchunks = []
  header = chunk.header
  case chunk.name
  when 'TES4'
    # Always read fields of TES4 as they define the masters, which are needed for others
    puts "[ESP DEBUG] - Read children chunks of #{chunk}" if @debug
    subchunks = chunk.sub_chunks(sub_chunks_format: {
      '*' => { header_size: 0, size_length: 2 },
      'ONAM' => {
        data_size_correction: proc do |file|
          # Size of ONAM field is sometimes badly computed. Correct it.
          file.seek(4, IO::SEEK_CUR)
          stored_size = file.read(2).unpack('S').first
          file.read(chunk.size).index('INTV') - stored_size
        end
      }
    })
    chunk_type = :record
  when 'MAST'
    description = chunk.data[0..-2].downcase
    @masters << description
    @master_ids[sprintf('%.2x', @master_ids.size)] = description
    chunk_type = :field
  when 'GRUP'
    puts "[ESP DEBUG] - Read children chunks of #{chunk}" if @debug
    subchunks = chunk.sub_chunks(sub_chunks_format: Hash[(['GRUP'] + KNOWN_GRUP_RECORDS_WITHOUT_FIELDS + KNOWN_GRUP_RECORDS_WITH_FIELDS).map do |known_sub_record_name|
      [
        known_sub_record_name,
        {
          header_size: 16,
          data_size_correction: known_sub_record_name == 'GRUP' ? -24 : 0
        }
      ]
    end])
    chunk_type = :group
  when *KNOWN_GRUP_RECORDS_WITHOUT_FIELDS
    # GRUP record having no fields
    form_id_str = sprintf('%.8x', header[4..7].unpack('L').first)
    @form_ids << form_id_str
    description = "FormID: #{form_id_str}"
    puts "[WARNING] - #{chunk} seems to have fields: #{chunk.data.inspect}" if @warnings && chunk.data[0..3] =~ /^\w{4}$/
    chunk_type = :record
  when *KNOWN_GRUP_RECORDS_WITH_FIELDS
    # GRUP record having fields
    form_id_str = sprintf('%.8x', header[4..7].unpack('L').first)
    @form_ids << form_id_str
    description = "FormID: #{form_id_str}"
    if @decode_fields
      puts "[ESP DEBUG] - Read children chunks of #{chunk}" if @debug
      subchunks = chunk.sub_chunks(sub_chunks_format: { '*' => { header_size: 0, size_length: 2 } })
    end
    chunk_type = :record
  when *KNOWN_FIELDS
    # Field
    record_module_name =
      if Data.const_defined?(chunk.parent_chunk.name.to_sym)
        chunk.parent_chunk.name.to_sym
      elsif Data.const_defined?(:All)
        :All
      else
        nil
      end
    unless record_module_name.nil?
      record_module = Data.const_get(record_module_name)
      data_class_name =
        if record_module.const_defined?("#{record_module_name}_#{chunk.name}".to_sym)
          "#{record_module_name}_#{chunk.name}".to_sym
        elsif record_module.const_defined?("#{record_module_name}_All".to_sym)
          "#{record_module_name}_All".to_sym
        else
          nil
        end
      unless data_class_name.nil?
        data_info = record_module.const_get(data_class_name)
        decoded_data = {}
        data_info.read(chunk.data).each_pair do |property, value|
          decoded_data[property] = value
        end
      end
    end
    chunk_type = :field
  else
    warning_desc = "Unknown chunk: #{chunk}. Data: #{chunk.data.inspect}"
    if @ignore_unknown_chunks
      puts "[WARNING] - #{warning_desc}" if @warnings
      @unknown_chunks << chunk
      chunk_type = :unknown
    else
      raise warning_desc
    end
  end
  # Decorate the chunk with our info
  esp_info = {
    description: description,
    type: chunk_type
  }
  esp_info[:decoded_data] = decoded_data unless decoded_data.nil?
  unless header.empty?
    header_class_name =
      if Headers.const_defined?(chunk.name.to_sym)
        chunk.name.to_sym
      elsif Headers.const_defined?(:All)
        :All
      else
        nil
      end
    unless header_class_name.nil?
      header_info = Headers.const_get(header_class_name)
      esp_info[:decoded_header] = {}
      header_info.read(header).each_pair do |property, value|
        esp_info[:decoded_header][property] = value
      end
    end
  end
  chunk.instance_variable_set(:@esp_info, esp_info)
  @chunks_tree[chunk] = subchunks
  subchunks.each.with_index do |subchunk, idx_subchunk|
    read_chunk(subchunk)
  end
end