Class: TCD::BitBuffer

Inherits:
Object
  • Object
show all
Defined in:
lib/tcd/bit_buffer.rb

Overview

Pure Ruby bit-level I/O for reading arbitrary bit-width integers from a binary stream. TCD files use bit-packing where fields don’t align to byte boundaries.

Instance Method Summary collapse

Constructor Details

#initialize(io) ⇒ BitBuffer

Returns a new instance of BitBuffer.



7
8
9
10
11
# File 'lib/tcd/bit_buffer.rb', line 7

def initialize(io)
    @io = io
    @buffer = 0          # Accumulated bits (MSB first)
    @bits_available = 0  # Number of valid bits in buffer
end

Instance Method Details

#alignObject

Discard any partial byte and align to next byte boundary



56
57
58
59
# File 'lib/tcd/bit_buffer.rb', line 56

def align
    @buffer = 0
    @bits_available = 0
end

#posObject

Current position in underlying IO



87
88
89
# File 'lib/tcd/bit_buffer.rb', line 87

def pos
    @io.pos
end

#read_cstringObject

Read a null-terminated string Strings in TCD are NOT byte-aligned - they start at the current bit position and are read 8 bits at a time until a null byte is found.



64
65
66
67
68
69
70
71
72
# File 'lib/tcd/bit_buffer.rb', line 64

def read_cstring
    chars = []
    loop do
        byte = read_uint(8)
        break if byte == 0
        chars << byte
    end
    chars.pack("C*").force_encoding("ISO-8859-1").encode("UTF-8")
end

#read_fixed_string(size) ⇒ Object

Read fixed-size string, stripping null padding



75
76
77
78
79
80
81
82
83
84
# File 'lib/tcd/bit_buffer.rb', line 75

def read_fixed_string(size)
    align
    bytes = @io.read(size)
    return "" if bytes.nil? || bytes.empty?
    # Find first null and truncate, handle encoding
    bytes.force_encoding("ISO-8859-1")
    null_pos = bytes.index("\x00")
    str = null_pos ? bytes[0, null_pos] : bytes
    str.encode("UTF-8", invalid: :replace, undef: :replace)
end

#read_int(n) ⇒ Object

Read n bits as signed integer (two’s complement)



36
37
38
39
40
# File 'lib/tcd/bit_buffer.rb', line 36

def read_int(n)
    value = read_uint(n)
    msb = 1 << (n - 1)
    value >= msb ? value - (1 << n) : value
end

#read_offset_scaled(n, offset, scale) ⇒ Object

Read n bits with offset and scale: (raw + offset) / scale Used for constituent speeds where offset shifts the range



50
51
52
53
# File 'lib/tcd/bit_buffer.rb', line 50

def read_offset_scaled(n, offset, scale)
    raw = read_uint(n)
    (raw.to_f + offset) / scale
end

#read_scaled(n, scale, signed: false) ⇒ Object

Read n bits and apply scale factor: value / scale



43
44
45
46
# File 'lib/tcd/bit_buffer.rb', line 43

def read_scaled(n, scale, signed: false)
    raw = signed ? read_int(n) : read_uint(n)
    raw.to_f / scale
end

#read_uint(n) ⇒ Object

Read n bits as unsigned integer (1-32 bits supported)

Raises:

  • (ArgumentError)


14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/tcd/bit_buffer.rb', line 14

def read_uint(n)
    raise ArgumentError, "bit count must be 1-32, got #{n}" if n < 1 || n > 32

    # Fill buffer until we have enough bits
    while @bits_available < n
        byte = @io.readbyte
        @buffer = (@buffer << 8) | byte
        @bits_available += 8
    end

    # Extract top n bits
    shift = @bits_available - n
    value = (@buffer >> shift) & ((1 << n) - 1)

    # Remove extracted bits from buffer
    @bits_available = shift
    @buffer &= (1 << shift) - 1

    value
end

#seek(offset) ⇒ Object

Seek to absolute position (clears bit buffer)



92
93
94
95
# File 'lib/tcd/bit_buffer.rb', line 92

def seek(offset)
    @io.seek(offset)
    align
end