Module: Esp::Mw::ScriptBlob

Defined in:
lib/esp/mw/script_blob.rb

Overview

Synthesizes the ZSTD-framed blobs that tes3conv expects for a Script record’s SCVR (variable list) and SCDT (bytecode) subrecords.

Vanilla blobs use ZSTD frames whose blocks may be raw (small/empty scripts) or compressed-with-LZ77 matches (large scripts). tes3conv accepts either, so we always emit raw blocks — simpler, smaller code, and only marginally larger output. Decoding always goes through libzstd via zstd-ruby so we can extract names from any vanilla blob.

Raw frame layout:

4 bytes  ZSTD magic                (28 b5 2f fd)
1 byte   Frame Header Descriptor   (0x00)
1 byte   Window Descriptor         (0x58)
3 bytes  Block header              ((size << 3) | type<<1 | last_block)
N bytes  Raw payload

Payload layout (both SCVR and SCDT placeholder):

4 bytes  LE uint32 — byte length of names section (0 if none)
N bytes  Null-terminated variable names, concatenated

Empty vars and the bytecode placeholder both use a 4-byte zero payload (length prefix = 0). A truly zero-byte payload is rejected by tes3conv for SCDT and isn’t what vanilla emits anywhere.

Constant Summary collapse

ZSTD_MAGIC =
"\x28\xb5\x2f\xfd".b.freeze
FHD =
0x00
WINDOW_DESCRIPTOR =
0x58

Class Method Summary collapse

Class Method Details

.decode_var_names(b64) ⇒ Object

Decompress any vanilla SCVR blob and split out the variable names.



54
55
56
57
58
59
60
61
62
# File 'lib/esp/mw/script_blob.rb', line 54

def decode_var_names(b64)
  payload = decompress(b64)
  return [] if payload.bytesize <= 4

  names_section = payload.byteslice(4..)
  names = names_section.split("\x00", -1)
  names.pop while names.last == '' || names.last.nil?
  names.map { |n| n.dup.force_encoding(Encoding::ASCII) }
end

.decompress(b64) ⇒ Object

Decompress a blob and return its raw payload (length prefix + names). Useful for tests that compare semantic content across raw and compressed framings.



67
68
69
# File 'lib/esp/mw/script_blob.rb', line 67

def decompress(b64)
  Zstd.decompress(Base64.decode64(b64))
end

.empty_bytecodeObject

SCDT placeholder. OpenMW recompiles bytecode from ‘text` on load, so we never emit real bytecode.



49
50
51
# File 'lib/esp/mw/script_blob.rb', line 49

def empty_bytecode
  wrap(("\x00" * 4).b)
end

.variables(names) ⇒ Object

Returns [base64_blob, variables_length] where variables_length is the byte count of the names section (matching the SCHD header field).



37
38
39
40
41
42
43
44
45
# File 'lib/esp/mw/script_blob.rb', line 37

def variables(names)
  if names.empty?
    [wrap(("\x00" * 4).b), 0]
  else
    names_bytes = names.map { |n| "#{n.b}\x00".b }.join
    payload = [names_bytes.bytesize].pack('V') + names_bytes
    [wrap(payload), names_bytes.bytesize]
  end
end