Module: Esp::Mw::Preflight
- Defined in:
- lib/esp/mw/preflight.rb
Overview
Resolves text_source into inline text and regenerates Script-record subrecords (SCHD header + SCVR vars + SCDT bytecode placeholder) into the form tes3conv expects. Mutates records in place.
Validation rules:
-
Source must be pure ASCII (MWScript’s compiler is silent on UTF-8).
-
Variable identifiers must be <= 20 chars (MWScript truncates ~20-23).
-
Warn (don’t fail) when a script’s ‘Begin <name>` doesn’t match its record id; vanilla often disagrees so this is intentionally soft.
Defined Under Namespace
Classes: ValidationError
Constant Summary collapse
- VAR_DECL_RE =
/^\s*(short|long|float)\s+([A-Za-z_]\w*)\b/i- BEGIN_RE =
/^\s*begin\s+([A-Za-z_]\w*)\b/i- NAME_LIMIT =
20
Class Method Summary collapse
- .apply_script_data!(record, text, shorts, longs, floats) ⇒ Object
-
.collect_plugin_ids(records) ⇒ Object
Records this plugin defines (anything with a top-level ‘id`).
- .describe_cell(record) ⇒ Object
- .parse_variables(text, label) ⇒ Object
-
.process!(records, source_dir:) ⇒ Object
Walks records and processes Script + Cell entries.
-
.process_cell!(record, plugin_ids) ⇒ Object
Defaults missing ‘mast_index` on cell references.
- .process_script!(record, source_dir) ⇒ Object
- .resolve_text(record, source_dir) ⇒ Object
- .validate_ascii!(text, label) ⇒ Object
Class Method Details
.apply_script_data!(record, text, shorts, longs, floats) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/esp/mw/preflight.rb', line 90 def self.apply_script_data!(record, text, shorts, longs, floats) vars_blob, vars_length = Esp::Mw::ScriptBlob.variables(shorts + longs + floats) record['header'] = { 'num_shorts' => shorts.size, 'num_longs' => longs.size, 'num_floats' => floats.size, 'bytecode_length' => 0, 'variables_length' => vars_length } record['variables'] = vars_blob record['bytecode'] = Esp::Mw::ScriptBlob.empty_bytecode record['text'] = text record.delete('text_source') end |
.collect_plugin_ids(records) ⇒ Object
Records this plugin defines (anything with a top-level ‘id`). Lowercased so the lookup is case-insensitive, matching how the engine and linter treat TES3 ids.
62 63 64 |
# File 'lib/esp/mw/preflight.rb', line 62 def self.collect_plugin_ids(records) records.filter_map { |r| r['id']&.to_s&.downcase }.to_set end |
.describe_cell(record) ⇒ Object
66 67 68 69 70 71 72 73 |
# File 'lib/esp/mw/preflight.rb', line 66 def self.describe_cell(record) name = record['name'].to_s grid = record.dig('data', 'grid') return "cell #{name.inspect}" unless name.empty? return "exterior cell #{grid.inspect}" if grid 'cell <unknown>' end |
.parse_variables(text, label) ⇒ Object
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/esp/mw/preflight.rb', line 131 def self.parse_variables(text, label) shorts = [] longs = [] floats = [] begin_name = nil text.each_line do |line| stripped = line.lstrip next if stripped.start_with?(';') if (m = BEGIN_RE.match(stripped)) begin_name = m[1] next end next unless (m = VAR_DECL_RE.match(line)) kind = m[1].downcase name = m[2] if name.length > NAME_LIMIT raise ValidationError, Esp.t('errors.preflight.var_too_long', label: label, name: name.inspect, length: name.length, limit: NAME_LIMIT) end bucket = { 'short' => shorts, 'long' => longs, 'float' => floats }[kind] bucket << name end [shorts, longs, floats, begin_name] end |
.process!(records, source_dir:) ⇒ Object
Walks records and processes Script + Cell entries. Returns a list of log lines (info + warnings) for the caller to print.
23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/esp/mw/preflight.rb', line 23 def self.process!(records, source_dir:) logs = [] plugin_ids = collect_plugin_ids(records) records.each do |record| case record['type'] when 'Script' then logs.concat(process_script!(record, source_dir)) when 'Cell' then logs.concat(process_cell!(record, plugin_ids)) end end logs end |
.process_cell!(record, plugin_ids) ⇒ Object
Defaults missing ‘mast_index` on cell references. tes3conv requires the field on every reference; the convention is `0` for plugin-local records and N>0 for master records (1 = first listed master, etc.). We auto-fill 0 when the referenced id is defined in this plugin —the common case for new content — and emit a clear error otherwise so the user doesn’t see tes3conv’s cryptic “missing field” message.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/esp/mw/preflight.rb', line 41 def self.process_cell!(record, plugin_ids) cell_label = describe_cell(record) logs = [] Array(record['references']).each_with_index do |ref, i| next if ref.key?('mast_index') ref_id = ref['id'].to_s if plugin_ids.include?(ref_id.downcase) ref['mast_index'] = 0 logs << "#{cell_label}: ref[#{i}] #{ref_id.inspect} — defaulted mast_index to 0 (plugin-local)" else raise ValidationError, Esp.t('errors.preflight.master_ref_needs_mast_index', cell: cell_label, id: ref_id) end end logs end |
.process_script!(record, source_dir) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/esp/mw/preflight.rb', line 75 def self.process_script!(record, source_dir) sid = record['id'] || '<unknown>' text, label = resolve_text(record, source_dir) return ["#{sid}: empty text, skipping"] if text.empty? validate_ascii!(text, label) shorts, longs, floats, begin_name = parse_variables(text, label) logs = [] if begin_name && begin_name.downcase != sid.downcase logs << "#{sid}: WARNING — Begin name #{begin_name.inspect} does not match record id" end apply_script_data!(record, text, shorts, longs, floats) logs end |
.resolve_text(record, source_dir) ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/esp/mw/preflight.rb', line 105 def self.resolve_text(record, source_dir) ts = record['text_source'] return [record['text'].to_s, "<inline text for #{record['id'] || '?'}>"] unless ts path = File.(ts, source_dir) unless File.file?(path) raise ValidationError, Esp.t('errors.preflight.text_source_missing', path: path) end [File.read(path), path] end |
.validate_ascii!(text, label) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/esp/mw/preflight.rb', line 118 def self.validate_ascii!(text, label) text.each_char.with_index do |c, i| next if c.ord < 128 prefix = text[0, i] line = prefix.count("\n") + 1 col = i - (prefix.rindex("\n") || -1) hex = format('%04X', c.ord) raise ValidationError, Esp.t('errors.preflight.non_ascii', label: label, char: c.inspect, hex: hex, line: line, col: col) end end |