nwn-lib 0.4.5 → 0.4.6
Sign up to get free protection for your applications and to get access to all the features.
- data/BINARIES +1 -1
- data/CHANGELOG +39 -1
- data/CHEATSHEET +20 -6
- data/README +29 -12
- data/Rakefile +3 -3
- data/SETTINGS +3 -69
- data/bin/nwn-dsl +2 -7
- data/lib/nwn/all.rb +1 -0
- data/lib/nwn/erf.rb +19 -192
- data/lib/nwn/gff.rb +0 -1
- data/lib/nwn/gff/field.rb +53 -14
- data/lib/nwn/gff/reader.rb +2 -2
- data/lib/nwn/gff/struct.rb +27 -9
- data/lib/nwn/res.rb +275 -0
- data/lib/nwn/settings.rb +0 -4
- data/lib/nwn/tlk.rb +1 -1
- data/lib/nwn/twoda.rb +36 -17
- data/lib/nwn/yaml.rb +13 -198
- data/spec/cexolocstr_spec.rb +15 -0
- data/spec/erf_spec.rb +87 -0
- data/spec/field_spec.rb +21 -0
- data/spec/gff_spec.rb +22 -0
- data/spec/res_spec.rb +59 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/struct_spec.rb +61 -0
- data/spec/tlk_spec.rb +47 -0
- data/spec/twoda_spec.rb +146 -0
- data/spec/wellformed_gff.binary +0 -0
- metadata +13 -8
- data/DATA_STRUCTURES +0 -50
- data/TYPE_VALUE_INFERRING +0 -93
- data/data/gff-common-nwn1.yaml +0 -982
- data/lib/nwn/infer.rb +0 -125
data/lib/nwn/gff.rb
CHANGED
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
|
-
|
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 :
|
90
|
-
value.is_a?(
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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) && (
|
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
|
-
|
106
|
-
value.is_a?(Array)
|
144
|
+
|
107
145
|
when :void
|
108
|
-
|
146
|
+
value.is_a?(String)
|
147
|
+
|
109
148
|
else
|
110
149
|
false
|
111
150
|
end
|
data/lib/nwn/gff/reader.rb
CHANGED
@@ -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?}
|
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
|
data/lib/nwn/gff/struct.rb
CHANGED
@@ -3,16 +3,21 @@
|
|
3
3
|
module NWN::Gff::Struct
|
4
4
|
DEFAULT_DATA_VERSION = "V3.2"
|
5
5
|
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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
|
-
|
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
|