class ElderScrollsPlugin
Constants
- KNOWN_FIELDS
- KNOWN_GRUP_RECORDS_WITHOUT_FIELDS
- KNOWN_GRUP_RECORDS_WITH_FIELDS
- VERSION
Attributes
Hash< Chunk or nil, Array<Chunk> >: The chunks tree, with nil being the root node
Array<String>: Ordered list of masters
Array<Riffola::Chunk>: Unknown chunks encountered
Public Class Methods
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
Set the current esp being read (useful for BinData decoding types that depend on the esp)
- Parameters
-
esp (
ElderScrollsPlugin
): The current esp
# File lib/elder_scrolls_plugin.rb, line 11 def self.current_esp=(esp) @esp = esp end
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
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
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
# 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
# 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
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 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