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/gff.rb CHANGED
@@ -117,4 +117,3 @@ require 'nwn/gff/cexolocstr'
117
117
  require 'nwn/gff/reader'
118
118
  require 'nwn/gff/writer'
119
119
  require 'nwn/gff/api'
120
- require 'nwn/infer'
data/lib/nwn/gff/field.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # A Field wraps a GFF label->value pair, providing:
2
2
  # * +.field_type+ describing the field type (e.g. :int)
3
3
  # * +.field_value+ holding the value of this Field
4
-
4
+ #
5
5
  # and, if loaded by Gff::Reader or through YAML:
6
6
  # * +.field_label+ holding the label
7
7
  # * +.parent+ holding the struct this Field is child of.
@@ -21,6 +21,7 @@ module NWN::Gff::Field
21
21
  s = {}.extend(self)
22
22
  s['label'], s['type'], s['value'] = label, type, value
23
23
  s.extend_meta_classes
24
+ s.validate
24
25
  s
25
26
  end
26
27
 
@@ -62,7 +63,12 @@ module NWN::Gff::Field
62
63
  def path
63
64
  raise NWN::Gff::GffError, "field not bound to a parent" unless @parent
64
65
  parent_path = @parent.path
65
- parent_path + "/" + field_label
66
+ if @parent.element && @parent.element.field_type == :list
67
+ idx = @parent.element.field_value.index(@parent)
68
+ parent_path + "[#{idx}]/" + field_label
69
+ else
70
+ parent_path + "/" + field_label
71
+ end
66
72
  end
67
73
 
68
74
  # This extends this field object and its' value with the
@@ -83,29 +89,62 @@ module NWN::Gff::Field
83
89
  field_value.is_a?(field_value_klass)
84
90
  end
85
91
 
92
+ # Validate if this field value is within the bounds of the set type.
93
+ def valid?
94
+ NWN::Gff::Field.valid_for? self.v, self.t
95
+ end
96
+
97
+ # Validate this field, and raise an Excpetion if not valid.
98
+ def validate
99
+ valid? or raise NWN::Gff::GffError,
100
+ "#{self.path rescue $!.to_s + '/' + self.l}: " +
101
+ "value '#{self.v.inspect}' not valid for type '#{self.t.inspect}'"
102
+ end
103
+
86
104
  # Validate if +value+ is within bounds of +type+.
87
105
  def self.valid_for? value, type
88
106
  case type
89
- when :char, :byte
90
- value.is_a?(Fixnum)
91
- when :short, :word
92
- value.is_a?(Fixnum)
93
- when :int, :dword
94
- value.is_a?(Fixnum)
95
- when :int64, :dword64
96
- value.is_a?(Fixnum)
107
+ when :byte, :char
108
+ value.is_a?(Integer) && value >= 0 && value <= 255
109
+
110
+ when :short
111
+ value.is_a?(Integer) && value >= -0x8000 && value <= 0x7fff
112
+ when :word
113
+ value.is_a?(Integer) && value >= 0 && value <= 0xffff
114
+
115
+ when :int
116
+ value.is_a?(Integer) && value >= -0x80000000 && value <= 0x7fffffff
117
+ when :dword
118
+ value.is_a?(Integer) && value >= 0 && value <= 0xffffffff
119
+
120
+ when :int64
121
+ value.is_a?(Integer) && value >= -0x800000000000 && value <= 0x7fffffffffff
122
+ when :dword64
123
+ value.is_a?(Integer) && value >= 0 && value <= 0xffffffffffff
124
+
97
125
  when :float, :double
98
126
  value.is_a?(Float)
127
+
99
128
  when :resref
100
- value.is_a?(String) && (1..16).member?(value.size)
129
+ value.is_a?(String) && (0..16).member?(value.size)
130
+
101
131
  when :cexostr
102
132
  value.is_a?(String)
133
+
103
134
  when :cexolocstr
135
+ value.is_a?(Hash) &&
136
+ value.keys.reject {|x| x.is_a?(Fixnum) && x >= 0 }.size == 0 &&
137
+ value.values.reject {|x| x.is_a?(String) }.size == 0
138
+
139
+ when :struct
140
+ value.is_a?(Hash)
141
+
142
+ when :list
104
143
  value.is_a?(Array)
105
- when :struct, :list
106
- value.is_a?(Array)
144
+
107
145
  when :void
108
- true
146
+ value.is_a?(String)
147
+
109
148
  else
110
149
  false
111
150
  end
@@ -166,8 +166,7 @@ class NWN::Gff::Reader
166
166
  }
167
167
  len = total_size + 4
168
168
  # Filter out empty strings.
169
- exostr.reject! {|k,v| v.nil? || v.empty?} if
170
- ENV['NWN_LIB_FILTER_EMPTY_EXOLOCSTR']
169
+ exostr.reject! {|k,v| v.nil? || v.empty?}
171
170
  exostr.taint
172
171
 
173
172
  when :void
@@ -214,6 +213,7 @@ class NWN::Gff::Reader
214
213
 
215
214
  # We extend all fields and field_values with matching classes.
216
215
  field.extend_meta_classes
216
+ field.validate
217
217
 
218
218
  [label, field]
219
219
  end
@@ -3,16 +3,21 @@
3
3
  module NWN::Gff::Struct
4
4
  DEFAULT_DATA_VERSION = "V3.2"
5
5
 
6
- # The file-type this struct represents.
7
- # This is usually the file extension for root structs,
8
- # and nil for sub-structs.
6
+ # Each Gff::Struct has a data_type, which describes the type of data the struct contains.
7
+ # For top-level structs, this equals the data type written to the GFF file ("UTI",
8
+ # for example); for sub structures, this is usually the top-level data type + the
9
+ # field label ("UTI/PropertiesList", for example).
10
+ #
11
+ # This is set for completeness' sake, but is not required to save the struct.
12
+ # Scripts could use this, for example, to reliably re-attach a Item within
13
+ # /ItemList/ somewhere else, or export it as .uti.
9
14
  attr_accessor :data_type
10
15
 
11
16
  # The file version. Usually "V3.2" for root structs,
12
17
  # and nil for sub-structs.
13
18
  attr_accessor :data_version
14
19
 
15
- # GFF struct type
20
+ # GFF struct type. The default is 0xffffffff.
16
21
  attr_accessor :struct_id
17
22
 
18
23
  # The field this struct is value of.
@@ -43,11 +48,15 @@ module NWN::Gff::Struct
43
48
  # Create a new struct.
44
49
  # Usually, you can leave out data_type and data_version for non-root structs,
45
50
  # because that will be guess-inherited based on the existing associations.
51
+ #
52
+ # You can pass a block to this method, which will receive the newly-created
53
+ # Struct as the only argument.
46
54
  def self.new struct_id = 0xffffffff, data_type = nil, data_version = nil
47
55
  s = {}.extend(self)
48
56
  s.struct_id = struct_id
49
57
  s.data_type = data_type
50
58
  s.data_version = data_version
59
+ yield(s) if block_given?
51
60
  s
52
61
  end
53
62
 
@@ -58,17 +67,26 @@ module NWN::Gff::Struct
58
67
  # some_struct.add_field 'ID', :byte, 5
59
68
  # is equivalent to:
60
69
  # some_struct.add_byte 'ID', 5
70
+ #
71
+ # You can pass a block to this method, which will receive the newly-created
72
+ # Field as an argument.
73
+ #
74
+ # This allows for code like this:
75
+ # Gff::Struct.new(0) do |s|
76
+ # s.add_byte "Byte", 5
77
+ # s.add_list "Some_List", [] do |l|
78
+ # l.v << Gff::Struct.new ...
79
+ # ..
80
+ # end
81
+ # end
61
82
  def add_field label, type, value, &block
62
83
  self[label] = NWN::Gff::Field.new(label, type, value)
63
84
  self[label].parent = self
64
- if block_given?
65
- yield(self[label])
66
- end
85
+ yield(self[label]) if block_given?
67
86
  self[label]
68
87
  end
69
88
 
70
- #:nodoc:
71
- def method_missing meth, *av, &block
89
+ def method_missing meth, *av, &block # :nodoc:
72
90
  if meth.to_s =~ /^add_(.+)$/
73
91
  if NWN::Gff::Types.index($1.to_sym)
74
92
  av.size == 2 or super
data/lib/nwn/res.rb ADDED
@@ -0,0 +1,275 @@
1
+ module NWN
2
+ module Resources
3
+
4
+ # This is a generic index to a resource.
5
+ class ContentObject
6
+ attr_accessor :resref
7
+ attr_accessor :res_type
8
+ attr_accessor :io
9
+ attr_accessor :offset
10
+ attr_accessor :size_override
11
+
12
+ # Create a new index to +filename+, optionally specifying +io+.
13
+ def self.new_from filename, io = nil
14
+ filename = File.expand_path(filename)
15
+ base = File.basename(filename).split(".")[0..-2].join(".").downcase
16
+ ext = File.extname(filename)[1..-1].downcase
17
+ res_type = NWN::Resources::Extensions[ext] or raise ArgumentError,
18
+ "Not a valid extension: #{ext.inspect} (while packing #{filename})"
19
+
20
+ ContentObject.new(base, res_type, io || filename, 0, io.size || File.stat(filename).size)
21
+ end
22
+
23
+ def initialize resref, res_type, io = nil, offset = nil, size = nil
24
+ @resref, @res_type = resref.downcase, res_type
25
+ @io, @offset = io, offset
26
+ @size_override = size
27
+
28
+ raise ArgumentError, "Invalid object passed: responds_to :read, want @offset, but does not respond_to :seek" if
29
+ @io.respond_to?(:read) && @offset && @offset != 0 && !@io.respond_to?(:seek)
30
+ end
31
+
32
+ # Get the size in bytes of this object.
33
+ def size
34
+ @size_override || (@io.is_a?(IO) ? @io.stat.size : File.stat(@io).size)
35
+ end
36
+
37
+ # Get the contents of this object. This is a costly operation, loading
38
+ # the whole buffer. If you want fine-grained access, use ContentObject#io
39
+ # and do it yourself, observing ContentObject#offset and ContentObject#size.
40
+ def get
41
+ if @io.respond_to?(:read)
42
+ @io.seek(@offset) if @offset
43
+ d = @io.read(self.size)
44
+ raise IOError,
45
+ "not enough data available while reading #{self.filename}" if
46
+ d.size != self.size
47
+ d
48
+ else
49
+ IO.read(@io)
50
+ end
51
+ end
52
+
53
+ # Get the canonical filename of this object.
54
+ def filename
55
+ @resref + "." + self.extension
56
+ end
57
+
58
+ # Get the extension of this object.
59
+ def extension
60
+ NWN::Resources::Extensions.index(@res_type)
61
+ end
62
+ end
63
+
64
+ # Wraps n ContentObjects; a baseclass for erf/key encapsulation.
65
+ class Container
66
+ attr_reader :content
67
+
68
+ def initialize
69
+ @content = []
70
+ end
71
+
72
+ def has?(filename)
73
+ base = File.basename(filename)
74
+ @content.each {|f|
75
+ return true if f.filename.downcase == base.downcase
76
+ }
77
+ return false
78
+ end
79
+
80
+ # Add a content object giving a +filename+ and a optional
81
+ # +io+.
82
+ def add_file filename, io = nil
83
+ @content << ContentObject.new_from(filename, io)
84
+ end
85
+
86
+ # Add a content object giving the ContentObject
87
+ def add o
88
+ @content << o
89
+ end
90
+
91
+ # Returns a list of filenames
92
+ def filenames
93
+ @content.map {|x| x.filename }
94
+ end
95
+
96
+ # Get the ContentObject pointing to the given filename.
97
+ # Raises ENOENT if not mapped.
98
+ def get_content_object filename
99
+ ret = @content.select {|x| filename.downcase == x.filename }
100
+ raise Errno::ENOENT,
101
+ "No ContentObject with the given filename found." if
102
+ ret.size == 0
103
+ ret[0]
104
+ end
105
+
106
+ # Get the contents of the given filename.
107
+ # Raises ENOENT if not mapped.
108
+ def get filename
109
+ get_content_object(filename.downcase).get
110
+ end
111
+ end
112
+
113
+ # A Container that directly wraps a directory (e.g. override/).
114
+ # Does not update on changes - caches the directory entries on initialize.
115
+ class DirectoryContainer < Container
116
+ def initialize path
117
+ super()
118
+ @path = path
119
+ Dir[path + "/*.*"].each {|x|
120
+ add_file x
121
+ }
122
+ end
123
+ end
124
+
125
+ # The resource manager, providing ordered access to Container objects.
126
+ class Manager
127
+ def initialize
128
+ @path = []
129
+ @_content_cache = nil
130
+ end
131
+
132
+ def add_container c
133
+ @path << c
134
+ end
135
+
136
+ # Get the ContentObject pointing to the given filename.
137
+ # Raises ENOENT if not mapped.
138
+ def get_content_object filename
139
+ @path.reverse.each {|con|
140
+ con.has?(filename) or next
141
+ return con.get_content_object(filename)
142
+ }
143
+ raise Errno::ENOENT, "No ContentObject with the given filename found."
144
+ end
145
+
146
+ # Get the contents of the given filename.
147
+ # Raises ENOENT if not mapped.
148
+ def get filename
149
+ get_content_object(filename).get
150
+ end
151
+
152
+ # Get a list of filenames contained inside.
153
+ def content
154
+ @_content_cache ||= @path.inject([]) {|a, x|
155
+ a |= x.filenames
156
+ }
157
+ end
158
+ end
159
+
160
+ Extensions = {
161
+ 'res' => 0,
162
+ 'bmp' => 1,
163
+ 'mve' => 2,
164
+ 'tga' => 3,
165
+ 'wav' => 4,
166
+ 'wfx' => 5,
167
+ 'plt' => 6,
168
+ 'ini' => 7,
169
+ 'mp3' => 8,
170
+ 'mpg' => 9,
171
+ 'txt' => 10,
172
+ 'plh' => 2000,
173
+ 'tex' => 2001,
174
+ 'mdl' => 2002,
175
+ 'thg' => 2003,
176
+ 'fnt' => 2005,
177
+ 'lua' => 2007,
178
+ 'slt' => 2008,
179
+ 'nss' => 2009,
180
+ 'ncs' => 2010,
181
+ 'mod' => 2011,
182
+ 'are' => 2012,
183
+ 'set' => 2013,
184
+ 'ifo' => 2014,
185
+ 'bic' => 2015,
186
+ 'wok' => 2016,
187
+ '2da' => 2017,
188
+ 'tlk' => 2018,
189
+ 'txi' => 2022,
190
+ 'git' => 2023,
191
+ 'bti' => 2024,
192
+ 'uti' => 2025,
193
+ 'btc' => 2026,
194
+ 'utc' => 2027,
195
+ 'dlg' => 2029,
196
+ 'itp' => 2030,
197
+ 'btt' => 2031,
198
+ 'utt' => 2032,
199
+ 'dds' => 2033,
200
+ 'bts' => 2034,
201
+ 'uts' => 2035,
202
+ 'ltr' => 2036,
203
+ 'gff' => 2037,
204
+ 'fac' => 2038,
205
+ 'bte' => 2039,
206
+ 'ute' => 2040,
207
+ 'btd' => 2041,
208
+ 'utd' => 2042,
209
+ 'btp' => 2043,
210
+ 'utp' => 2044,
211
+ 'dft' => 2045,
212
+ 'gic' => 2046,
213
+ 'gui' => 2047,
214
+ 'css' => 2048,
215
+ 'ccs' => 2049,
216
+ 'btm' => 2050,
217
+ 'utm' => 2051,
218
+ 'dwk' => 2052,
219
+ 'pwk' => 2053,
220
+ 'btg' => 2054,
221
+ 'utg' => 2055,
222
+ 'jrl' => 2056,
223
+ 'sav' => 2057,
224
+ 'utw' => 2058,
225
+ '4pc' => 2059,
226
+ 'ssf' => 2060,
227
+ 'hak' => 2061,
228
+ 'nwm' => 2062,
229
+ 'bik' => 2063,
230
+ 'ndb' => 2064,
231
+ 'ptm' => 2065,
232
+ 'ptt' => 2066,
233
+ 'bak' => 2067,
234
+ 'osc' => 3000,
235
+ 'usc' => 3001,
236
+ 'trn' => 3002,
237
+ 'utr' => 3003,
238
+ 'uen' => 3004,
239
+ 'ult' => 3005,
240
+ 'sef' => 3006,
241
+ 'pfx' => 3007,
242
+ 'cam' => 3008,
243
+ 'lfx' => 3009,
244
+ 'bfx' => 3010,
245
+ 'upe' => 3011,
246
+ 'ros' => 3012,
247
+ 'rst' => 3013,
248
+ 'ifx' => 3014,
249
+ 'pfb' => 3015,
250
+ 'zip' => 3016,
251
+ 'wmp' => 3017,
252
+ 'bbx' => 3018,
253
+ 'tfx' => 3019,
254
+ 'wlk' => 3020,
255
+ 'xml' => 3021,
256
+ 'scc' => 3022,
257
+ 'ptx' => 3033,
258
+ 'ltx' => 3034,
259
+ 'trx' => 3035,
260
+ 'mdb' => 4000,
261
+ 'mda' => 4001,
262
+ 'spt' => 4002,
263
+ 'gr2' => 4003,
264
+ 'fxa' => 4004,
265
+ 'fxe' => 4005,
266
+ 'jpg' => 4007,
267
+ 'pwc' => 4008,
268
+ 'ids' => 9996,
269
+ 'erf' => 9997,
270
+ 'bif' => 9998,
271
+ 'key' => 9999,
272
+ }.freeze
273
+
274
+ end
275
+ end