class Feh::Bin::LZ11

Converter for the LZ11 archive format used in Fire Emblem Heroes. Ported from DSDecmp.

Attributes

buf[R]

@return [ArrayIStream] the input buffer of the converter.

Public Class Methods

new(buffer) click to toggle source

Initializes the LZ11 converter. @param buffer [Array<Integer>] byte array containing the data to convert

# File lib/feh/bin/lz11.rb, line 16
def initialize(buffer)
  @buf = ArrayIStream.new(buffer)
end

Public Instance Methods

compress() click to toggle source

Compresses a byte buffer. This function is not required to produce exactly the same results as existing archives in Fire Emblem Heroes when given the same inputs. @return [Array<Integer>] byte array representing the compressed LZ11

archive

@return [Symbol] error code if the input is too large or empty

# File lib/feh/bin/lz11.rb, line 102
def compress
  return :input_too_short if buf.size < 2
  return :input_too_large if buf.size > 0xFFFFFF

  outstream = ArrayOStream.new
    .u8(0x11).u16(buf.size).u8(buf.size >> 16)

  outbuffer = [8 * 4 + 1] * 33
  outbuffer[0] = 0
  bufferlength = 1
  bufferedBlocks = 0
  readBytes = 0
  while readBytes < buf.size
    if bufferedBlocks == 8
      outstream.write(outbuffer[0, bufferlength])
      outbuffer[0] = 0
      bufferlength = 1
      bufferedBlocks = 0
    end

    oldLength = [readBytes, 0x1000].min
    disp, length = occurrence_length(readBytes,
      [buf.size - readBytes, 0x10110].min, readBytes - oldLength, oldLength)
    if length < 3
      outbuffer[bufferlength] = buf[readBytes]
      readBytes += 1
      bufferlength += 1
    else
      readBytes += length
      outbuffer[0] |= (1 << (7 - bufferedBlocks)) & 0xFF
      case
      when length > 0x110
        outbuffer[bufferlength] = 0x10
        outbuffer[bufferlength] |= ((length - 0x111) >> 12) & 0x0F
        bufferlength += 1
        outbuffer[bufferlength] = ((length - 0x111) >> 4) & 0xFF
        bufferlength += 1
        outbuffer[bufferlength] = ((length - 0x111) << 4) & 0xF0
      when length > 0x10
        outbuffer[bufferlength] = 0x00
        outbuffer[bufferlength] |= ((length - 0x111) >> 4) & 0x0F
        bufferlength += 1
        outbuffer[bufferlength] = ((length - 0x111) << 4) & 0xF0
      else
        outbuffer[bufferlength] = ((length - 1) << 4) & 0xF0
      end
      outbuffer[bufferlength] |= ((disp - 1) >> 8) & 0x0F
      bufferlength += 1
      outbuffer[bufferlength] = (disp - 1) & 0xFF
      bufferlength += 1
    end

    bufferedBlocks += 1
  end

  if bufferedBlocks > 0
    outstream.write(outbuffer[0, bufferlength])
  end

  outstream.buf
end
decompress() click to toggle source

Decompresses an LZ11 archive. @return [Array<Integer>] byte array representing the decompressed content

of the input archive

@return [Symbol] error code if the input is not a valid LZ11 archive

# File lib/feh/bin/lz11.rb, line 24
def decompress
  header = buf.u32
  return :invalid_data if (header & 0xFF) != 0x11
  decompressedSize = header >> 8
  decompressedSize = buf.u32 if decompressedSize == 0

  bufferLength = 0x1000
  buffer = Array.new(bufferLength)
  bufferOffset = 0

  flags = 0
  mask = 1

  outbuf = []
  until outbuf.size >= decompressedSize
    if mask == 1
      flags = buf.u8
      return :stream_too_short if flags.nil?
      mask = 0x80
    else
      mask >>= 1
    end

    if (flags & mask) > 0
      byte1 = buf.u8
      return :stream_too_short if byte1.nil?

      length = byte1 >> 4
      disp = -1
      case length
      when 0
        byte2 = buf.u8
        byte3 = buf.u8
        return :stream_too_short if byte3.nil?
        length = (((byte1 & 0x0F) << 4) | (byte2 >> 4)) + 0x11
        disp = (((byte2 & 0x0F) << 8) | byte3) + 0x1
      when 1
        byte2 = buf.u8
        byte3 = buf.u8
        byte4 = buf.u8
        return :stream_too_short if byte4.nil?
        length = (((byte1 & 0x0F) << 12) | (byte2 << 4) | (byte3 >> 4)) + 0x111
        disp = (((byte3 & 0x0F) << 8) | byte4) + 0x1
      else
        byte2 = buf.u8
        return :stream_too_short if byte2.nil?
        length = ((byte1 & 0xF0) >> 4) + 0x1
        disp = (((byte1 & 0x0F) << 8) | byte2) + 0x1
      end

      return :invalid_data if disp > outbuf.size

      bufIdx = bufferOffset + bufferLength - disp
      length.times do
        next_byte = buffer[bufIdx % bufferLength]
        bufIdx += 1
        outbuf << next_byte
        buffer[bufferOffset] = next_byte
        bufferOffset = (bufferOffset + 1) % bufferLength
      end
    else
      next_byte = buf.u8
      return :stream_too_short if next_byte.nil?
      outbuf << next_byte
      buffer[bufferOffset] = next_byte
      bufferOffset = (bufferOffset + 1) % bufferLength
    end
  end

  outbuf
end

Private Instance Methods

occurrence_length(newPtr, newLength, oldPtr, oldLength, minDisp = 1) click to toggle source
# File lib/feh/bin/lz11.rb, line 165
    def occurrence_length(newPtr, newLength, oldPtr, oldLength, minDisp = 1)
      disp = 0
      return [disp, 0] if newLength == 0
      oldRange = buf[oldPtr, newLength + oldLength - minDisp - 1]
      newArray = buf[newPtr, newLength]

      j = 0
      k = 0
      t = [-1]
      pos = 1
      cnd = 0
      while pos < newArray.size
        if newArray[pos] == newArray[cnd]
          t[pos] = t[cnd]
          pos += 1
          cnd += 1
        else
          t[pos] = cnd
          cnd = t[cnd]
          while cnd >= 0 && newArray[pos] != newArray[cnd]
            cnd = t[cnd]
          end
          pos += 1
          cnd += 1
        end
      end
      t[pos] = cnd

      maxLength = 0
      while j < oldRange.size && j - k < oldLength - minDisp
        if newArray[k] == oldRange[j]
          j += 1
          k += 1
          if k > maxLength
            maxLength = k
            disp = oldLength - (j - k)
            break if maxLength == newLength
          end
        else
          k = t[k]
          if k < 0
            j += 1
            k += 1
          end
        end
      end

#      currentLengths = Array.new(oldLength - minDisp) do |i|
#        oldArray = oldRange[i, newLength]
#        p [oldArray, newArray]
#        oldArray.zip(newArray).take_while {|x, y| x == y}.size
#      end
#      maxLength = 0
#      currentLengths.each_with_index do |currentLength, i|
#        if currentLength > maxLength
#          maxLength = currentLength
#          disp = oldLength - i
#          break if maxLength == newLength
#        end
#      end

      [disp, maxLength]
    end