nwn-lib 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/nwn/settings.rb CHANGED
@@ -1,7 +1,3 @@
1
- if ENV['NWN_LIB_INFER_DATA_FILE'] && ENV['NWN_LIB_INFER_DATA_FILE'] != ""
2
- NWN::Gff.load_struct_defaults(ENV['NWN_LIB_INFER_DATA_FILE'])
3
- end
4
-
5
1
  if ENV['NWN_LIB_2DA_LOCATION'] && ENV['NWN_LIB_2DA_LOCATION'] != ""
6
2
  NWN::TwoDA::Cache.setup ENV['NWN_LIB_2DA_LOCATION']
7
3
  end
data/lib/nwn/tlk.rb CHANGED
@@ -59,7 +59,7 @@ module NWN
59
59
 
60
60
  return @cache[id] if @cache[id]
61
61
 
62
- raise ArgumentError, "No such string ID: #{id.inspect}" if id >= self.highest_id || id < 0
62
+ raise ArgumentError, "No such string ID: #{id.inspect}" if id > self.highest_id || id < 0
63
63
  seek_to = HEADER_SIZE + (id) * DATA_ELEMENT_SIZE
64
64
  @io.seek(seek_to)
65
65
  data = @io.read(DATA_ELEMENT_SIZE)
data/lib/nwn/twoda.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'shellwords'
2
2
 
3
3
  class Integer
4
+ # Returns the level that this amount experience resolves to.
5
+ # Depends on a set-up TwoDA::Cache, and reads from <tt>exptable</tt>.
4
6
  def xp_to_level
5
7
  NWN::TwoDA.get('exptable').rows.each {|row|
6
8
  level, exp = row.Level.to_i, row.XP.to_i
@@ -9,6 +11,8 @@ class Integer
9
11
  return nil
10
12
  end
11
13
 
14
+ # Returns the amount of experience that this level resolves to.
15
+ # Depends on a set-up TwoDA::Cache, and reads from <tt>exptable</tt>.
12
16
  def level_to_xp
13
17
  NWN::TwoDA.get('exptable').by_col("XP", self - 1).to_i
14
18
  end
@@ -97,11 +101,22 @@ module NWN
97
101
 
98
102
  # Parses a string that represents a valid 2da definition.
99
103
  # Replaces any content this table may already have.
104
+ # This will cope with all misformatting in the same way
105
+ # that NWN1 itself does. NWN2 employs slightly different
106
+ # parsing rules, and may or may not be compatible in the
107
+ # fringe cases.
108
+ #
109
+ # Will raise an ArgumentError if the given +bytes+ do
110
+ # not contain a valid 2DA header, or the file is so badly
111
+ # misshaped that it will not ever be parsed correctly by NWN1.
112
+ #
113
+ # Will complain about everything mismatching it finds
114
+ # on $stderr if $DEBUG is set (-d).
100
115
  def parse bytes
101
116
  magic, *data = *bytes.split(/\r?\n/).map {|v| v.strip }
102
117
 
103
- raise ArgumentError, "Not valid 2da: No valid header found" if
104
- magic != "2DA V2.0"
118
+ raise ArgumentError, "Not valid 2da: No valid header found (got: #{magic[0,20].inspect}..)" if
119
+ magic !~ /^2DA\s+V2.0$/
105
120
 
106
121
  # strip all empty lines; they are regarded as comments
107
122
  data.reject! {|ln| ln.strip == ""}
@@ -110,10 +125,7 @@ module NWN
110
125
 
111
126
  header = Shellwords.shellwords(header.strip)
112
127
  data.map! {|line|
113
- r = Shellwords.shellwords(line.strip)
114
- # The last cell can be without double quotes even if it contains whitespace
115
- r[header.size..-1] = r[header.size..-1].join(" ") if r.size > header.size
116
- r
128
+ Shellwords.shellwords(line.strip)
117
129
  }
118
130
 
119
131
  new_row_data = []
@@ -121,24 +133,30 @@ module NWN
121
133
  id_offset = 0
122
134
  idx_offset = 0
123
135
  data.each_with_index {|row, idx|
124
- id = row.shift.to_i + id_offset
136
+ id = row.shift
137
+
138
+ $stderr.puts "Warning: invalid ID in line #{idx}: #{id.inspect}" if $DEBUG && $id !~ /^\d+$/
125
139
 
126
- raise ArgumentError, "Invalid ID in row #{idx}" unless id >= 0
140
+ id = id.to_i + id_offset
127
141
 
128
- # Its an empty row - this is actually valid; we'll fill it up and dump it later.
142
+ # Its an empty row - NWN strictly numbers by counted lines - then so do we.
129
143
  while id > idx + idx_offset
130
- new_row_data << Row.new([""] * header.size)
144
+ $stderr.puts "Warning: missing ID at #{id - id_offset}, fixing that for you." if $DEBUG
131
145
  idx_offset += 1
132
146
  end
133
147
 
134
148
  # NWN automatically increments duplicate IDs - so do we.
135
- while id + id_offset < idx
136
- $stderr.puts "Warning: duplicate ID found at row #{idx} (id: #{id}); fixing that for you."
149
+ while id < idx + idx_offset
150
+ $stderr.puts "Warning: duplicate ID found at row #{idx} (id: #{id}); fixing that for you." if $DEBUG
137
151
  id_offset += 1
138
152
  id += 1
139
153
  end
140
154
 
141
155
  # NWN fills in missing columns with an empty value - so do we.
156
+ $stderr.puts "Warning: row #{id} (real: #{id - id_offset}) misses " +
157
+ "#{header.size - row.size} columns at the end, fixed" if
158
+ row.size < header.size if $DEBUG
159
+
142
160
  row << "" while row.size < header.size
143
161
 
144
162
  new_row_data << k_row = Row.new(row)
@@ -146,15 +164,16 @@ module NWN
146
164
 
147
165
  k_row.map! {|cell|
148
166
  cell = case cell
149
- when nil; nil
167
+ when nil; raise "Bug in parser: nil-value for cell"
150
168
  when "****"; ""
151
169
  else cell
152
170
  end
153
171
  }
154
172
 
155
- raise ArgumentError,
156
- "Row #{idx} has too many cells for the given header (has #{k_row.size}, want <= #{header.size})" if
157
- k_row.size != header.size
173
+ $stderr.puts "Warning: row #{idx} has too many cells (has #{k_row.size}, want <= #{header.size})" if
174
+ $DEBUG && k_row.size > header.size
175
+
176
+ k_row.pop while k_row.size > header.size
158
177
  }
159
178
 
160
179
  @columns = header
@@ -277,7 +296,7 @@ module NWN
277
296
  when "2"
278
297
  "\r"
279
298
  when nil
280
- @newlines
299
+ @newline
281
300
  end)
282
301
  end
283
302
  end
data/lib/nwn/yaml.rb CHANGED
@@ -1,8 +1,10 @@
1
+ # This file contains all YAML-specific loading and dumping code.
1
2
  require 'yaml'
2
3
 
3
4
  # See http://www.taguri.org/ for the exact meaning of this.
4
5
  NWN::YAML_DOMAIN = "nwn-lib.elv.es,2008-12"
5
6
 
7
+ #:stopdoc:
6
8
  class Array
7
9
  attr_accessor :to_yaml_style
8
10
  end
@@ -45,14 +47,10 @@ module NWN::Gff::Struct
45
47
  }.size == 0
46
48
 
47
49
  map.add('__' + 'data_type', @data_type) if @data_type
48
- map.add('__' + 'data_version', @data_version) if @data_version && !can_infer_data_version?
49
- map.add('__' + 'struct_id', @struct_id) if @struct_id && !can_infer_struct_id?
50
+ map.add('__' + 'data_version', @data_version) if @data_version && @data_version != DEFAULT_DATA_VERSION
51
+ map.add('__' + 'struct_id', @struct_id) if @struct_id
50
52
 
51
- reject {|k, v|
52
- # Dont emit fields that would result in their default values anyways.
53
- ENV['NWN_LIB_CLEAR_KNOWN_VALUES'] && v.can_infer_type? &&
54
- v.can_infer_str_ref? && v.can_infer_value?
55
- }.sort.each {|k,v|
53
+ sort.each {|k,v|
56
54
  map.add(k,v)
57
55
  }
58
56
  end
@@ -63,57 +61,12 @@ end
63
61
  module NWN::Gff::Field
64
62
 
65
63
  def to_yaml(opts = {})
66
-
67
- if !ENV['NWN_LIB_DONT_COMPACT_LIST_STRUCTS'] && field_type == :list && can_compact_as_list?
68
- YAML::quick_emit(nil, opts) do |out|
69
- out.seq("!", to_yaml_style) do |seq|
70
- field_value.each {|item|
71
- struct_id_of_item = NWN::Gff.get_struct_defaults_for(item.path, '__struct_id')
72
-
73
- calf = get_compact_as_list_field
74
- case calf
75
- when Array
76
- style = NWN::Gff.get_struct_defaults_for(item.path, '__inline')
77
-
78
- non_inferrable = (item.keys.reject {|x|
79
- item[x].can_infer_type? && item[x].can_infer_value?
80
- } - calf).sort
81
- raise NWN::Gff::GffError, "cannot compact list struct at #{item.path}, would " +
82
- "LOSE fields: #{non_inferrable.inspect}" if non_inferrable.size > 0
83
-
84
- ar = calf.map {|ik| item[ik] || NWN::Gff.get_struct_default_value(item.path, ik) }
85
- missing = ar.map {|x| x.nil? ? calf[ar.index(x)] : nil }.compact
86
-
87
- raise NWN::Gff::GffError, "cannot compact list-struct at #{item.path}, does not " +
88
- "have all compactable field values set or are inferrable (missing fields: #{missing.inspect})." if
89
- missing.size > 0
90
-
91
- ar.to_yaml_style = :inline if style
92
-
93
- ar.unshift(item.struct_id) if struct_id_of_item == 'inline'
94
- seq.add(ar)
95
- else
96
- isv = NWN::Gff.get_struct_defaults_for(self.path, '__inline')
97
- seq.style = :inline if isv.nil? || isv === true
98
-
99
- seq.add(item.struct_id) if struct_id_of_item == 'inline'
100
- seq.add(item[calf])
101
- end
102
- }
103
- end
104
- end
105
-
106
- elsif !ENV['NWN_LIB_DONT_COMPACT_FIELDS'] && can_compact_print?
107
- field_value_as_compact.to_yaml(opts)
108
-
109
- else
110
- YAML::quick_emit(nil, opts) do |out|
111
- out.map(taguri, to_yaml_style) do |map|
112
- map.style = :inline unless NWN::Gff::YAMLNonInlineableFields.index(self['type'])
113
- map.add('type', self['type']) unless can_infer_type?
114
- map.add('str_ref', self['str_ref']) unless can_infer_str_ref?
115
- map.add('value', self['value']) unless can_infer_value?
116
- end
64
+ YAML::quick_emit(nil, opts) do |out|
65
+ out.map(taguri, to_yaml_style) do |map|
66
+ map.style = :inline unless NWN::Gff::YAMLNonInlineableFields.index(self['type'])
67
+ map.add('type', self['type'])
68
+ map.add('str_ref', self['str_ref']) if has_str_ref?
69
+ map.add('value', self['value'])
117
70
  end
118
71
  end
119
72
  end
@@ -130,159 +83,21 @@ YAML.add_domain_type(NWN::YAML_DOMAIN,'struct') {|t,hash|
130
83
  struct.data_version = hash.delete('__data_version')
131
84
  struct.data_version ||= NWN::Gff::Struct::DEFAULT_DATA_VERSION
132
85
 
133
- if struct.struct_id.nil? && s_id = NWN::Gff.get_struct_defaults_for(struct.path, '__struct_id')
134
- struct.struct_id = s_id.to_i
135
- elsif struct.struct_id.nil?
136
- raise NWN::Gff::GffError, "Cannot infer implicit struct_id for struct at #{struct.path}."
137
- end
86
+ raise NWN::Gff::GffError, "no struct_id set for struct at #{struct.path}." if struct.struct_id.nil?
138
87
 
139
88
  hash.each {|label,element|
140
89
  label.freeze
141
- element = case element
142
- when Hash # already uncompacted or a compacted exolocstr
143
- default_type = NWN::Gff.get_struct_default_type(struct.path, label)
144
- raise NWN::Gff::GffError,
145
- "Cannot parse compacted hash with no contents and no infer data at #{struct.path}/#{label}." if
146
- element.size == 0 && default_type == nil
147
-
148
- # It has only got numbers as key, we *assume* its a cexoloc.
149
- # Thats okay, because type inferration will catch it later and bite us. Hopefully.
150
- if element.keys.select {|x| x.to_s !~ /^(str_ref|\d+)$/}.size == 0
151
- element = {
152
- 'type' => :cexolocstr,
153
- 'str_ref' => element.delete('str_ref') || NWN::Gff::Field::DEFAULT_STR_REF,
154
- 'value' => element,
155
- }
156
- end
157
-
158
- element
159
-
160
- when Array # compacted struct-list
161
- element = {
162
- 'type' => :list,
163
- 'value' => element,
164
- }
165
- path = struct.data_type + "/" + label
166
- unpack_struct_element = NWN::Gff.get_struct_defaults_for(path, '__compact')
167
- if unpack_struct_element
168
- # If it doesn't have unpack data, we need to assume its a compacted list itself.
169
- # Sorry.
170
- # Hope this wont bite anyone later on.
171
-
172
- #raise NWN::Gff::GffError,
173
- # "Cannot unpack compacted struct list at #{path}, no infer data available." unless
174
- # unpack_struct_element
175
-
176
- unpack_struct_element_struct_id = NWN::Gff.get_struct_defaults_for(path, "__struct_id")
177
-
178
- unpack_struct_elements = [unpack_struct_element].flatten.freeze
179
-
180
- unpack_struct_element_types = unpack_struct_elements.map {|unpack_struct_element|
181
- raise NWN::Gff::GffError, "While unpacking #{path}: " +
182
- "#{unpack_struct_element} is not a field-naime, dummy." unless
183
- unpack_struct_element.is_a?(String)
184
-
185
- unpack_struct_element_type =
186
- NWN::Gff.get_struct_default_type(path, unpack_struct_element)
187
-
188
- unpack_struct_element_type
189
- }.freeze
190
-
191
- last_struct_id = -1
192
- element['value'].map! {|kv|
193
- st = {}
194
- st.extend(NWN::Gff::Struct)
195
- st.struct_id = case unpack_struct_element_struct_id
196
- when 'iterative'
197
- last_struct_id += 1
198
- when 'inline'
199
- kv.shift
200
- when Fixnum
201
- unpack_struct_element_struct_id
202
- else
203
- kv['__struct_id'] || raise(NWN::Gff::GffError,
204
- "dont know how to handle struct_id #{unpack_struct_element_struct_id} at #{label} = #{kv.inspect}")
205
- end
206
- st.data_type = path
207
-
208
- unpack_struct_elements.each_with_index {|use, index|
209
- case kv
210
- when Hash
211
- if !kv[use]
212
- uset, usev = unpack_struct_element_types[index],
213
- NWN::Gff.get_struct_default_value(path, use)
214
- else
215
- uset = kv[use]['type']
216
- usev = kv[use]['value']
217
- end
218
- when Array
219
- uset = unpack_struct_element_types[index]
220
- usev = kv[index]
221
- if usev.is_a?(Hash)
222
- uset, usev = usev['type'], usev['value']
223
- end
224
- when String, Numeric, Symbol
225
- uset = unpack_struct_element_types[index]
226
- usev = kv
227
- else
228
- raise "Dont know how to unpack list-struct element: #{kv.inspect}"
229
- end
230
-
231
- raise NWN::Gff::GffError, "Cannot infer type of #{path}/#{use} (got: #{uset.inspect})" unless
232
- uset && NWN::Gff::Types.index(uset)
233
- raise NWN::Gff::GffError, "Cannot get or infer value of #{path}/#{use} (got: #{usev.inspect})" unless
234
- usev && (usev.is_a?(String) || usev.is_a?(Numeric) || usev.is_a?(Symbol))
235
-
236
- # Figure out the default value (which we'll need to re-add?) if
237
- # it was filtered while outputting.
238
- #kv[index] = NWN::Gff.get_struct_default_value(path, use) if !kv[index]
239
- #raise NWN::Gff::GffError, "field present in compacted struct-list, and cant " +
240
- # "infer default value at #{use}/#{index} (#{kv.inspect})" if !kv[index]
241
-
242
- el = st[use] = {
243
- 'label' => use,
244
- 'type' => uset,
245
- 'value' => usev
246
- }
247
- el.extend(NWN::Gff::Field)
248
- el.extend_meta_classes
249
- el.parent = st
250
- }
251
- st
252
- }
253
- end
254
- element
255
-
256
- when Numeric, String # compacted scalar
257
- {'value' => element}
258
-
259
- else
260
- fail "Don't know how to un-compact /#{label}: #{element.inspect}, klass #{element.class.to_s}"
261
- end
262
90
 
263
91
  element.extend(NWN::Gff::Field)
264
92
  element.field_label = label
265
93
  element.parent = struct
266
94
  element.str_ref ||= NWN::Gff::Field::DEFAULT_STR_REF if element.respond_to?('str_ref=')
267
95
 
268
- infer_field_type = NWN::Gff.get_struct_default_type(struct.path, element.field_label)
269
-
270
- if element.field_type && infer_field_type && infer_field_type != element.field_type
271
- raise NWN::Gff::GffError, "/#{label} has field_type #{element.field_type.inspect}, but infer data says #{infer_field_type.inspect}."
272
-
273
- elsif element.field_type.nil? && infer_field_type.nil?
274
- raise NWN::Gff::GffError, "Cannot infer implicit type for /#{label} while parsing struct-id #{struct.struct_id}."
275
-
276
- elsif element.field_type.nil? && infer_field_type
277
- element.field_type = infer_field_type
278
- end
279
-
280
-
281
96
  element.extend_meta_classes
97
+ element.validate
282
98
 
283
99
  struct[label] = element.taint
284
100
  }
285
101
 
286
- NWN::Gff.__yaml_postparse nil, struct if struct.data_type
287
102
  struct
288
103
  }
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe "Gff::Cexolocstr" do
4
+
5
+ it "should compact nil and empty strings" do
6
+ str = Gff::Field.new('Test', :cexolocstr, {})
7
+ str.v[0] = "hi"
8
+ str.v[1] = nil
9
+ str.v[4] = ""
10
+ str.v[8] = "test"
11
+ str.v.compact!
12
+ str.v.should == {0=>"hi", 8=>"test"}
13
+ end
14
+
15
+ end
data/spec/erf_spec.rb ADDED
@@ -0,0 +1,87 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ WELLFORMED_ERF = ([
4
+ "HAK", "V1.0",
5
+ locstr_count = 1, locstr_size = 14,
6
+ entry_count = 3,
7
+ offset_to_locstr = 160,
8
+ offset_to_keys = offset_to_locstr + locstr_size,
9
+ offset_to_res = offset_to_locstr + locstr_size + entry_count * 24,
10
+
11
+ 100, 126, # year, dayofyear
12
+ 0xdeadbeef, "" #description strref, 116 bytes 0-padding
13
+ ].pack("a4 a4 VV VV VV VV V a116") + [
14
+ 0, 6, "abcdef" # one locstr
15
+ ].pack("V V a*") + [
16
+ "resref", 0, 10, 0, # keylist: resref.txt, id = 0
17
+ "help", 1, 1, 0, # keylist: help.bmp, id = 1
18
+ "yegods", 2, 4, 0, # keylist: yegods.wav, id = 2
19
+ ].pack("a16 V v v" * entry_count) + [
20
+ offset_to_res + entry_count * 8, 6, # offset, size
21
+ offset_to_res + entry_count * 8 + 6, 4, # offset, size
22
+ offset_to_res + entry_count * 8 + 6 + 4, 6, # offset, size
23
+ ].pack("II" * entry_count) + [
24
+ "resref", "help", "yegods"
25
+ ].pack("a* a* a*")).freeze
26
+
27
+
28
+
29
+ describe "Erf::Erf" do
30
+
31
+ def wellformed_verify binary, expect_locstr = true
32
+ t = nil
33
+ t = Erf::Erf.new(StringIO.new binary)
34
+
35
+ t.file_type.should == "HAK"
36
+ t.file_version.should == "V1.0"
37
+ if expect_locstr
38
+ t.localized_strings.should == {0 => "abcdef"}
39
+ else
40
+ t.localized_strings.should == {}
41
+ end
42
+ t.content.size.should == 3
43
+ t.year.should == 100
44
+ t.day_of_year.should == 126
45
+ t.description_str_ref.should == 0xdeadbeef
46
+
47
+ t.content[0].filename.should == "resref.txt"
48
+ t.content[1].filename.should == "help.bmp"
49
+ t.content[2].filename.should == "yegods.wav"
50
+ t.content[0].size.should == 6
51
+ t.content[1].size.should == 4
52
+ t.content[2].size.should == 6
53
+ t.content[0].get.should == "resref"
54
+ t.content[1].get.should == "help"
55
+ t.content[2].get.should == "yegods"
56
+ end
57
+
58
+ it "reads wellformed ERF containers" do
59
+ wellformed_verify WELLFORMED_ERF
60
+ end
61
+
62
+ it "reproduces correct ERF binary data" do
63
+ t = Erf::Erf.new(StringIO.new WELLFORMED_ERF)
64
+ io = StringIO.new
65
+ t.write_to(io)
66
+ io.seek(0)
67
+ proc {
68
+ wellformed_verify io.read
69
+ }.should_not raise_error IOError
70
+ end
71
+
72
+ it "does not read ERF with locstr_size = 0 and locstr_count > 0" do
73
+ b = WELLFORMED_ERF.dup
74
+ b[12,4] = [0].pack("V")
75
+ proc {
76
+ wellformed_verify b
77
+ }.should raise_error IOError
78
+ end
79
+
80
+ it "reads ERF with locstr_size > 0 and locstr_count = 0" do
81
+ b = WELLFORMED_ERF.dup
82
+ b[8,4] = [0].pack("V")
83
+ proc {
84
+ wellformed_verify b, false
85
+ }.should_not raise_error IOError
86
+ end
87
+ end