nwn-lib 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,15 @@
1
+ Copyright (C) 2008 Bernhard Stoeckner <elven@swordcoast.net>
2
+
3
+ This program is free software; you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation; either version 2 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License along
14
+ with this program; if not, write to the Free Software Foundation, Inc.,
15
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
data/README ADDED
@@ -0,0 +1,56 @@
1
+ == A library for Neverwinter Nights 1/2 resource files
2
+
3
+ This package provides a library for reading, changing, and writing common file formats that are found with NWN[http://nwn.bioware.com/], a Bioware game.
4
+
5
+ They should work with NWN2 just as well, since the file format specifications did not change.
6
+
7
+ There are various things included in this distribution:
8
+ * some binaries under bin/
9
+ * the actual library under lib/nwn
10
+
11
+ == Supplied binaries
12
+ * nwn-gff-print
13
+
14
+ == Quickstart
15
+
16
+ It is easiest to just use the gem, which is available through rubyforge (<tt>sudo gem install nwn-lib</tt>).
17
+
18
+ As an alternative, fetch the latest development files with git[http://git.swordcoast.net/?p=nwn/nwn-lib.git;a=summary].
19
+
20
+ require 'rubygems'
21
+ require 'nwn/gff'
22
+
23
+ # Read a GFF file
24
+ o = NWN::Gff::Reader.read(IO.read('/tmp/test-creature.gff'))
25
+
26
+ # Do some modifications to it
27
+ o['/Tag'] = 'testtag'
28
+
29
+ # And write it somewhere else
30
+ bytes = NWN::Gff::Writer.dump(o)
31
+
32
+ File.open('/tmp/test-creature-2.gff', 'w') {|f| f.write(bytes) }
33
+
34
+ Modification API is fairly limited, for now, but will improve soon.
35
+
36
+ == GFF data structures: Quick Intro
37
+
38
+ Ruby GFF data structures are nearly a 1:1 wrapper around the known format.
39
+
40
+ Each GFF object has a root structure with a +struct_id+ of 0.
41
+
42
+ === Label
43
+ A Label is a string of up to 16 bytes, and occurs as keys in Structs.
44
+
45
+ === Struct
46
+ A struct is a hash. It is a unordered collection of key => value pairs.
47
+ Keys are Labels, values are either:
48
+ * Structs
49
+ * Lists
50
+ * Elements
51
+
52
+ === List
53
+ A list is an *ordered* array of Structs.
54
+
55
+ === Element
56
+ A Element is a distinct value of a given type. For all possible types, see the +Types+ hash in the NWN::Gff module.
@@ -0,0 +1,104 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+ require "rake/gempackagetask"
4
+ require "rake/rdoctask"
5
+ require "fileutils"
6
+ include FileUtils
7
+
8
+ ##############################################################################
9
+ # Configuration
10
+ ##############################################################################
11
+ NAME = "nwn-lib"
12
+ VERS = "0.1"
13
+ CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
14
+ RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
15
+ 'nwn-lib: a ruby library for accessing NWN resource files', \
16
+ '--main', 'README']
17
+
18
+ Rake::RDocTask.new do |rdoc|
19
+ rdoc.rdoc_dir = "rdoc"
20
+ rdoc.options += RDOC_OPTS
21
+ rdoc.rdoc_files.add ["README", "COPYING", "CHANGELOG", "doc/*.rdoc", "lib/**/*.rb"]
22
+ end
23
+
24
+ desc "Packages up nwn-lib"
25
+ task :package => [:clean]
26
+
27
+ spec = Gem::Specification.new do |s|
28
+ s.name = NAME
29
+ s.rubyforge_project = 'nwn-lib'
30
+ s.version = VERS
31
+ s.platform = Gem::Platform::RUBY
32
+ s.has_rdoc = true
33
+ s.extra_rdoc_files = ["README", "COPYING"] + Dir["doc/*.rdoc"]
34
+ s.rdoc_options += RDOC_OPTS + ["--exclude", "^(examples|extras)\/"]
35
+ s.summary = "a ruby library for accessing Neverwinter Nights resource files"
36
+ s.description = s.summary
37
+ s.author = "Bernhard Stoeckner"
38
+ s.email = "elven@swordcoast.net"
39
+ s.homepage = "http://nwn-lib.elv.es"
40
+ s.executables = ["nwn-gff-print"]
41
+ s.required_ruby_version = ">= 1.8.4"
42
+ s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,spec,lib}/**/*")
43
+ s.require_path = "lib"
44
+ s.bindir = "bin"
45
+ end
46
+
47
+ Rake::GemPackageTask.new(spec) do |p|
48
+ p.need_tar = true
49
+ p.gem_spec = spec
50
+ end
51
+
52
+ desc "Install nwn-lib gem"
53
+ task :install do
54
+ sh %{rake package}
55
+ sh %{sudo gem install pkg/#{NAME}-#{VERS}}
56
+ end
57
+
58
+ desc "Install nwn-lib gem without docs"
59
+ task :install_no_docs do
60
+ sh %{rake package}
61
+ sh %{sudo gem install pkg/#{NAME}-#{VERS} --no-rdoc --no-ri}
62
+ end
63
+
64
+ desc "Uninstall nwn-lib gem"
65
+ task :uninstall => [:clean] do
66
+ sh %{sudo gem uninstall #{NAME}}
67
+ end
68
+
69
+ desc "Upload nwn-lib gem to rubyforge"
70
+ task :release => [:package] do
71
+ sh %{rubyforge login}
72
+ sh %{rubyforge add_release nwn-lib #{NAME} #{VERS} pkg/#{NAME}-#{VERS}.tgz}
73
+ sh %{rubyforge add_file nwn-lib #{NAME} #{VERS} pkg/#{NAME}-#{VERS}.gem}
74
+ end
75
+
76
+ require "spec/rake/spectask"
77
+
78
+ desc "Run specs with coverage"
79
+ Spec::Rake::SpecTask.new("spec") do |t|
80
+ t.spec_files = FileList["spec/*_spec.rb"]
81
+ t.spec_opts = File.read("spec/spec.opts").split("\n")
82
+ t.rcov_opts = File.read("spec/rcov.opts").split("\n")
83
+ t.rcov = true
84
+ end
85
+
86
+ desc "Run specs without coverage"
87
+ task :default => [:spec_no_cov]
88
+ Spec::Rake::SpecTask.new("spec_no_cov") do |t|
89
+ t.spec_files = FileList["spec/*_spec.rb"]
90
+ t.spec_opts = File.read("spec/spec.opts").split("\n")
91
+ end
92
+
93
+ desc "Run rcov only"
94
+ Spec::Rake::SpecTask.new("rcov") do |t|
95
+ t.rcov_opts = File.read("spec/rcov.opts").split("\n")
96
+ t.spec_opts = File.read("spec/spec.opts").split("\n")
97
+ t.spec_files = FileList["spec/*_spec.rb"]
98
+ t.rcov = true
99
+ end
100
+
101
+ desc "check documentation coverage"
102
+ task :dcov do
103
+ sh "find lib -name '*.rb' | xargs dcov"
104
+ end
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/ruby
2
+ require 'rubygems'
3
+ require 'optparse'
4
+ require 'nwn/gff'
5
+ require 'yaml'
6
+
7
+ format = nil
8
+
9
+ OptionParser.new do |o|
10
+ o.banner = "Usage: nwn-gff-print [options] file/- [path]"
11
+ o.on "-y", "--yaml", "Dump as yaml" do
12
+ format = :yaml
13
+ end
14
+ o.on "-k", "--kivinen", "Dump as kivinens dump format (like the perl tools)" do
15
+ format = :kivinen
16
+ end
17
+ end.parse!
18
+
19
+ file = ARGV.shift or begin
20
+ $stderr.puts "Required argument: filename to process, or - for stdin (try -h)."
21
+ exit 1
22
+ end
23
+
24
+ path = ARGV.shift
25
+
26
+ if file == "-"
27
+ bytes = $stdin.read
28
+ else
29
+ bytes = IO.read(file)
30
+ end
31
+
32
+ g = NWN::Gff::Reader.read(bytes)
33
+
34
+ if path
35
+ begin
36
+ g = g[path]
37
+ rescue Exception => e
38
+ $stderr.puts "Error: " + e.to_s
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ def k_format_struct s, prefix = "/", &block
44
+ if s.is_a?(NWN::Gff::Gff)
45
+ s = NWN::Gff::Element.new("", :struct, s.root_struct)
46
+ end
47
+ if s.is_a?(Array)
48
+ v = NWN::Gff::Element.new("(unlabeled list)", :list, s)
49
+ end
50
+ if s.is_a?(NWN::Gff::Struct)
51
+ s = NWN::Gff::Element.new("(unlabeled struct)", :struct, s)
52
+ end
53
+ case s.type
54
+ when :struct
55
+ s.value.each {|k,v|
56
+ k_format_struct v, prefix + s.label + (s.label == "" ? "" : "/") do |l,v|
57
+ yield(l, v)
58
+ end
59
+ }
60
+ when :cexolocstr
61
+ s.value.each {|vv|
62
+ yield(prefix + s.label + "/" + vv.language.to_s, vv.text)
63
+ }
64
+ yield(prefix + s.label + ". ___string_ref", s._str_ref)
65
+ when :list
66
+ s.value.each_with_index {|vv, idx|
67
+ vv.each {|kkk, vvv|
68
+ k_format_struct vvv, prefix + s.label + "[#{idx}]/" do |l,v|
69
+ yield(l,v)
70
+ end
71
+ }
72
+ }
73
+ else
74
+ yield(prefix + s.label, s.value)
75
+ end
76
+ end
77
+
78
+ case format
79
+ when :yaml
80
+ y g
81
+ when :kivinen
82
+ k_format_struct g do |label, value|
83
+ puts "%s:\t%s" % [label, value]
84
+ end
85
+ else
86
+ puts "Unknown format; try -h"
87
+ end
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/ruby
2
+ require 'rubygems'
3
+ require 'optparse'
4
+ require 'nwn/gff'
5
+ require 'yaml'
6
+
7
+ format = nil
8
+
9
+ OptionParser.new do |o|
10
+ o.banner = "Usage: nwn-gff-print [options] file/- [path]"
11
+ o.on "-y", "--yaml", "Dump as yaml" do
12
+ format = :yaml
13
+ end
14
+ o.on "-k", "--kivinen", "Dump as kivinens dump format (like the perl tools)" do
15
+ format = :kivinen
16
+ end
17
+ end.parse!
18
+
19
+ file = ARGV.shift or begin
20
+ $stderr.puts "Required argument: filename to process, or - for stdin (try -h)."
21
+ exit 1
22
+ end
23
+
24
+ path = ARGV.shift
25
+
26
+ if file == "-"
27
+ bytes = $stdin.read
28
+ else
29
+ bytes = IO.read(file)
30
+ end
31
+
32
+ g = NWN::Gff::Reader.read(bytes)
33
+
34
+ if path
35
+ begin
36
+ g = g[path]
37
+ rescue Exception => e
38
+ $stderr.puts "Error: " + e.to_s
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ def k_format_struct s, prefix = "/", &block
44
+ if s.is_a?(NWN::Gff::Gff)
45
+ s = NWN::Gff::Element.new("", :struct, s.root_struct)
46
+ end
47
+ if s.is_a?(Array)
48
+ v = NWN::Gff::Element.new("(unlabeled list)", :list, s)
49
+ end
50
+ if s.is_a?(NWN::Gff::Struct)
51
+ s = NWN::Gff::Element.new("(unlabeled struct)", :struct, s)
52
+ end
53
+ case s.type
54
+ when :struct
55
+ s.value.each {|k,v|
56
+ k_format_struct v, prefix + s.label + (s.label == "" ? "" : "/") do |l,v|
57
+ yield(l, v)
58
+ end
59
+ }
60
+ when :cexolocstr
61
+ s.value.each {|vv|
62
+ yield(prefix + s.label + "/" + vv.language.to_s, vv.text)
63
+ }
64
+ yield(prefix + s.label + ". ___string_ref", s._str_ref)
65
+ when :list
66
+ s.value.each_with_index {|vv, idx|
67
+ vv.each {|kkk, vvv|
68
+ k_format_struct vvv, prefix + s.label + "[#{idx}]/" do |l,v|
69
+ yield(l,v)
70
+ end
71
+ }
72
+ }
73
+ else
74
+ yield(prefix + s.label, s.value)
75
+ end
76
+ end
77
+
78
+ case format
79
+ when :yaml
80
+ y g
81
+ when :kivinen
82
+ k_format_struct g do |label, value|
83
+ puts "%s:\t%s" % [label, value]
84
+ end
85
+ else
86
+ puts "Unknown format; try -h"
87
+ end
@@ -0,0 +1,733 @@
1
+ module NWN
2
+ module Gff
3
+ # This error gets thrown if reading or writing fails.
4
+ class GffError < Exception; end
5
+
6
+ # This error gets thrown if a supplied value does not
7
+ # fit into the given data type, or you are trying to assign
8
+ # a type to something that does not hold type.
9
+ #
10
+ # Example: You're trying to pass a value greater than 2**32
11
+ # into a int.
12
+ class GffTypeError < Exception; end
13
+
14
+ # Gets raised if you are trying to access a path that does
15
+ # not exist.
16
+ class GffPathInvalidError < Exception; end
17
+
18
+ # This hash lists all possible NWN::Gff::Element types.
19
+ Types = {
20
+ 0 => :byte,
21
+ 1 => :char,
22
+ 2 => :word,
23
+ 3 => :short,
24
+ 4 => :dword,
25
+ 5 => :int,
26
+ 6 => :dword64,
27
+ 7 => :int64,
28
+ 8 => :float,
29
+ 9 => :double,
30
+ 10 => :cexostr,
31
+ 11 => :resref,
32
+ 12 => :cexolocstr,
33
+ 13 => :void,
34
+ 14 => :struct,
35
+ 15 => :list,
36
+ }.freeze
37
+
38
+ #:stopdoc:
39
+ # Used internally to figure out if a field is stored directly
40
+ # or by reference.
41
+ ComplexTypes = [6, 7, 9, 10, 11, 12, 13, 14, 15].freeze
42
+ SimpleTypes = (Types.keys - ComplexTypes)
43
+ SimpleTypes.freeze
44
+ #:startdoc:
45
+
46
+ Formats = {
47
+ :byte => "Cxxx",
48
+ :char => "Cxxx",
49
+ :word => 'Sxx',
50
+ :short => 'sxx',
51
+ :dword => 'I',
52
+ :int => 'i',
53
+ :dword64 => 'II',
54
+ :int64 => 'q',
55
+ :float => 'f',
56
+ :double => 'd',
57
+ }.freeze
58
+ end
59
+ end
60
+
61
+ # A GFF object encapsulates a whole GFF identity, with a type,
62
+ # version, and a root structure.
63
+ # This object also provides easy accessors for labels and values.
64
+ class NWN::Gff::Gff
65
+ include NWN::Gff
66
+
67
+ attr_accessor :type
68
+ attr_accessor :version
69
+
70
+ # Create a new GFF object from the given +struct+.
71
+ # This is normally not needed unless you are creating
72
+ # GFF objects entirely from hand.
73
+ #
74
+ # See NWN::Gff::Reader.
75
+ def initialize struct, type, version = "V3.2"
76
+ @hash = struct
77
+ @type = type
78
+ @version = version
79
+ end
80
+
81
+ # Return the root struct of this GFF.
82
+ def root_struct
83
+ @hash
84
+ end
85
+
86
+ # A simple accessor that can be used to
87
+ # retrieve or set properties in the struct, delimited by slashes.
88
+ #
89
+ # Will raise a GffPathInvalidError if the given path cannot be found,
90
+ # and GffTypeError if some type fails to validate.
91
+ #
92
+ # Examples (with +gff+ assumed to be a item):
93
+ # gff['/Tag']
94
+ # will retrieve the Tag of the given object
95
+ # gff['/Tag'] = 'Test'
96
+ # Set the Tag to 'Test'
97
+ # gff['/PropertiesList']
98
+ # will retrieve an array of Gff::Elements
99
+ # gff['/PropertiesList[1]']
100
+ # will yield element 2 in the list
101
+ # gff['/'] = NWN::Gff::Element.new('Property', :byte, 14)
102
+ # will add a new property at the root struct with the name of 'Property', or
103
+ # overwrite an existing one with the same label.
104
+ # gff['/PropertiesList[0]'] = 'Test'
105
+ # This will raise an error (obviously)
106
+ def get_or_set k, new_value = nil, new_type = nil, new_label = nil, new_str_ref = nil
107
+ puts "get_or_set(#{k} = #{new_value})"
108
+ h = self.root_struct
109
+ path = []
110
+ value_path = [h]
111
+ current_value = nil
112
+
113
+ k.split('/').each {|v|
114
+ next if v == ""
115
+ path << v
116
+
117
+ if current_value.is_a?(Gff::Element) && current_value.type == :list # && v =~ /\[(\d+)\]$/
118
+ puts "value = #{$1}"
119
+ current_value = current_value.value[$1.to_i]
120
+ end
121
+
122
+ if h.is_a?(Gff::Element)
123
+ case h.type
124
+ when :cexolocstr
125
+ current_value = h.value.select {|vx| vx.language.to_i == v.to_i}
126
+ current_value = current_value[0] != nil ? current_value[0].text : ''
127
+
128
+ when :list
129
+ raise GffPathInvalidError, "List-selector access not implemented yet."
130
+
131
+ else
132
+ raise GffPathInvalidError,
133
+ "Tried to access sub-label of a non-complex field: /#{path.join('/')}"
134
+
135
+ end
136
+ elsif h.is_a?(Gff::Struct)
137
+
138
+ if v =~ /^(.+?)\[(\d+)\]$/
139
+ current_value = h[$1.to_s]
140
+ if current_value.is_a?(Gff::Element) && !current_value.type == :list
141
+ raise GffPathInvalidError, "Tried to access list-index of a non-list at /#{path.join('/')}"
142
+ end
143
+ current_value = current_value.value[$2.to_i]
144
+ else
145
+ current_value = h[v]
146
+ end
147
+ else
148
+ raise GffPathInvalidError, "Unknown sub-field type #{h.class.to_s} at /#{path.join('/')}"
149
+ end
150
+
151
+ value_path << current_value
152
+ h = current_value
153
+
154
+ raise GffPathInvalidError,
155
+ "Cannot find path: /#{path.join('/')}" if current_value.nil? && !new_value.is_a?(Gff::Element)
156
+ }
157
+
158
+ if path.size == 0
159
+ if new_value.is_a?(Gff::Element)
160
+ value_path << h
161
+ else
162
+ raise GffPathInvalidError, "Do not operate on the root struct unless through adding items."
163
+ end
164
+ end
165
+
166
+ old_value = current_value.nil? ? nil : current_value.dup
167
+
168
+ if new_value.is_a?(Gff::Element)
169
+ new_value.validate
170
+ value_path[-2].delete(current_value)
171
+ value_path[-2][new_value.label] = new_value
172
+ else
173
+
174
+ if !new_label.nil?
175
+ # Set a new label
176
+ value_path[-2].delete(current_value.label)
177
+ current_value.label = new_label
178
+ value_path[-2][new_label] = current_value
179
+ end
180
+
181
+ if !new_type.nil?
182
+ # Set a new datatype
183
+ raise GffTypeError, "Cannot set a type on a non-element." unless current_value.is_a?(Gff::Element)
184
+ test = current_value.dup
185
+ test.type = new_type
186
+ test.validate
187
+
188
+ current_value.type = new_type
189
+
190
+ end
191
+
192
+ if !new_str_ref.nil?
193
+ # Set a new str_ref
194
+ raise GffTypeError, "specified path is not a CExoStr" unless current_value.is_a?(Gff::CExoString)
195
+ current_value._str_ref = new_str_ref.to_i
196
+ end
197
+
198
+ if !new_value.nil?
199
+
200
+ case current_value
201
+ when Gff::Element
202
+ test = current_value.dup
203
+ test.value = new_value
204
+ test.validate
205
+ current_value.value = new_value
206
+
207
+ when String #means: cexolocstr assignment
208
+ if value_path[-2].is_a?(Gff::Element) && value_path[-2].type == :cexolocstr
209
+ value_path[-2].value.select{|xy| xy.language == path[-1].to_i }[0].text = new_value
210
+ else
211
+ raise GffPathInvalidError, "Dont know how to set #{new_value.class} on #{path.inspect}."
212
+ end
213
+ else
214
+ raise GffPathInvalidError, "Don't know what to do with #{current_value.class} -> #{new_value.class} at /#{path.join('/')}"
215
+ end
216
+
217
+ end
218
+ end
219
+
220
+ old_value
221
+ end
222
+
223
+ def [] k
224
+ get_or_set k
225
+ end
226
+
227
+ def []= k, v
228
+ get_or_set k, v
229
+ end
230
+
231
+ end
232
+
233
+ # A Element wraps a GFF label->value pair,
234
+ # provides a +.type+ and, optionally,
235
+ # a +._str_ref+ for CExoLocStrings.
236
+ #
237
+ # Fields:
238
+ # [+label+] The label of this element, for reference.
239
+ # [+type+] The type of this element. (See NWN::Gff)
240
+ # [+value+] The value of this element.
241
+ class NWN::Gff::Element
242
+ attr_accessor :label, :type, :value
243
+ attr_accessor :_str_ref
244
+
245
+ def initialize label = nil, type = nil, value = nil
246
+ @label, @type, @value = label, type, value
247
+ end
248
+
249
+ def validate path_prefix = "/"
250
+ raise NWN::Gff::GffTypeError, "#{path_prefix}#{self.label}: New value #{self.value} is not compatible with the current type #{self.type}" unless
251
+ self.class.valid_for?(self.value, self.type)
252
+ end
253
+
254
+ # 0 => :byte,
255
+ # 1 => :char,
256
+ # 2 => :word,
257
+ # 3 => :short,
258
+ # 4 => :dword,
259
+ # 5 => :int,
260
+ # 6 => :dword64,
261
+ # 7 => :int64,
262
+ # 8 => :float,
263
+ # 9 => :double,
264
+ # 10 => :cexostr,
265
+ # 11 => :resref,
266
+ # 12 => :cexolocstr,
267
+ # 13 => :void,
268
+ # 14 => :struct,
269
+ # 15 => :list,
270
+
271
+ # Validate if +value+ is within bounds of +type+.
272
+ def self.valid_for? value, type
273
+ case type
274
+ when :char, :byte
275
+ value.is_a?(Fixnum)
276
+ when :short, :word
277
+ value.is_a?(Fixnum)
278
+ when :int, :dword
279
+ value.is_a?(Fixnum)
280
+ when :int64, :dword64
281
+ value.is_a?(Fixnum)
282
+ when :float, :double
283
+ value.is_a?(Float)
284
+ when :resref
285
+ value.is_a?(String) && (1..16).member?(value.size)
286
+ when :cexostr
287
+ value.is_a?(String)
288
+ when :cexolocstr
289
+ value.is_a?(Array)
290
+ when :struct, :list
291
+ value.is_a?(Array)
292
+ when :void
293
+ true
294
+ else
295
+ false
296
+ end
297
+ end
298
+
299
+ end
300
+
301
+ # A Gff::Struct is a hash of label->Element pairs,
302
+ # with an added +.struct_id+.
303
+ class NWN::Gff::Struct < Hash
304
+ attr_accessor :struct_id
305
+ def initialize *a
306
+ @struct_id = 0
307
+ super
308
+ end
309
+ end
310
+
311
+ # A CExoLocString is a localised CExoString.
312
+ #
313
+ # Attributes:
314
+ # [+language+] The language ID
315
+ # [+text+] The text for this language.
316
+ #
317
+ # ExoLocStrings in the wild are usually arrays of NWN::Gff:CExoLocString
318
+ # (one for each language supplied).
319
+ # Note that a CExoLocString is NOT a GFF list, although both are
320
+ # represented as arrays.
321
+ class NWN::Gff::CExoLocString < Struct.new(:language, :text)
322
+ end
323
+
324
+ # A class that parses binary GFF bytes into ruby-friendly data structures.
325
+ class NWN::Gff::Reader
326
+ include NWN::Gff
327
+
328
+ attr_reader :hash
329
+ attr_reader :gff
330
+
331
+ # Create a new Reader with the given +bytes+ and immediately parse it.
332
+ # This is not needed usually; use Reader.read instead.
333
+ def initialize bytes
334
+ @bytes = bytes
335
+ read_all
336
+ end
337
+
338
+ # Reads +bytes+ as gff data and returns a NWN::Gff:Gff object.
339
+ def self.read bytes
340
+ self.new(bytes).gff
341
+ end
342
+
343
+ private
344
+
345
+ def read_all
346
+ type, version,
347
+ struct_offset, struct_count,
348
+ field_offset, field_count,
349
+ label_offset, label_count,
350
+ field_data_offset, field_data_count,
351
+ field_indices_offset, field_indices_count,
352
+ list_indices_offset, list_indices_count =
353
+ @bytes.unpack("a4a4 VV VV VV VV VV VV")
354
+
355
+ raise GffError, "Unknown version #{@version}; not a gff?" unless
356
+ version == "V3.2"
357
+
358
+ raise GffError, "struct offset at wrong place, not a gff?" unless
359
+ struct_offset == 56
360
+
361
+ struct_len = struct_count * 12
362
+ field_len = field_count * 16
363
+ label_len = label_count * 16
364
+
365
+ @structs = @bytes[struct_offset, struct_len].unpack("V*")
366
+ @fields = @bytes[field_offset, field_len].unpack("V*")
367
+ @labels = @bytes[label_offset, label_len].unpack("A16" * label_count)
368
+ @field_data = @bytes[field_data_offset, field_data_count]
369
+ @field_indices = @bytes[field_indices_offset, field_indices_count].unpack("V*")
370
+ @list_indices = @bytes[list_indices_offset, list_indices_count].unpack("V*")
371
+ # puts "FieldDataOffset = #{field_data_offset}, Count = #{field_data_count}"
372
+ @hash = read_struct 0
373
+ @gff = Gff.new(@hash, type, version)
374
+ end
375
+
376
+ # This iterates through a struct and reads all fields into a hash, which it returns.
377
+ def read_struct index
378
+ struct = Gff::Struct.new
379
+
380
+ type = @structs[index * 3]
381
+ data_or_offset = @structs[index * 3 + 1]
382
+ count = @structs[index * 3 + 2]
383
+
384
+ raise GffError, "struct index #{index} outside of struct_array" if
385
+ index * 3 + 3 > @structs.size + 1
386
+
387
+ if count == 1
388
+ lbl, vl = * read_field(data_or_offset)
389
+ struct[lbl] = vl
390
+ else
391
+ return 1 if count == 0
392
+ raise GffError, "struct index not divisable by 4" if
393
+ data_or_offset % 4 != 0
394
+ data_or_offset /= 4
395
+ for i in data_or_offset...(data_or_offset+count)
396
+ lbl, vl = * read_field(@field_indices[i])
397
+ struct[lbl] = vl
398
+ end
399
+ end
400
+
401
+ struct.struct_id = type
402
+ struct
403
+ end
404
+
405
+ # Reads the field at +index+ and returns [label_name, Gff::Element]
406
+ def read_field index
407
+ gff = {}
408
+
409
+ field = Element.new
410
+
411
+ index *= 3
412
+ type = @fields[index]
413
+ label_index = @fields[index + 1]
414
+ data_or_offset = @fields[index + 2]
415
+ # puts "Reading field #{index}"
416
+ # puts "Label_index = #{label_index}, label = #{@labels[label_index]}"
417
+ # puts "type = #{type}, data_or_offset = #{data_or_offset}"
418
+ raise GffError, "Label index #{label_index} outside of label array" if
419
+ label_index > @labels.size
420
+
421
+ raise GffError, "Field data offset #{data_or_offset} outside of field data block (#{@field_data.size})" if
422
+ ComplexTypes.index(type) && data_or_offset > @field_data.size
423
+
424
+ raise GffError, "Unknown field type #{type}." unless Types[type]
425
+ type = Types[type]
426
+
427
+ label = @labels[label_index]
428
+
429
+ value = case type
430
+ when :byte, :char
431
+ data_or_offset & 0xff
432
+
433
+ when :word
434
+ data_or_offset & 0xffff
435
+
436
+ when :short
437
+ [(data_or_offset & 0xffff)].pack("S").unpack("s")[0]
438
+
439
+ when :dword
440
+ data_or_offset
441
+
442
+ when :int
443
+ [data_or_offset].pack("I").unpack("i")[0]
444
+
445
+ when :float
446
+ [data_or_offset].pack("V").unpack("f")[0]
447
+
448
+ when :dword64
449
+ len = 8
450
+ v1, v2 = @field_data[data_or_offset, len].unpack("II")
451
+ v1 * (2**32) + v2
452
+
453
+ when :int64
454
+ len = 8
455
+ @field_data[data_or_offset, len].unpack("q")[0]
456
+
457
+ when :double
458
+ len = 8
459
+ @field_data[data_or_offset, len].unpack("d")[0]
460
+
461
+ when :cexostr
462
+ len = @field_data[data_or_offset, 4].unpack("V")[0]
463
+ @field_data[data_or_offset + 4, len]
464
+
465
+ when :resref
466
+ len = @field_data[data_or_offset, 1].unpack("C")[0]
467
+ @field_data[data_or_offset + 1, len]
468
+
469
+ when :cexolocstr
470
+ total_size, str_ref, str_count =
471
+ @field_data[data_or_offset, 12].unpack("VVV")
472
+ all = @field_data[data_or_offset + 12, total_size]
473
+ field._str_ref = str_ref
474
+
475
+ r = []
476
+ str_count.times {
477
+ id, len = all.unpack("VV")
478
+ str = all[8, len].unpack("a*")[0]
479
+ all = all[(8 + len)..-1]
480
+ r << Gff::CExoLocString.new(id, str)
481
+ }
482
+ len = total_size + 4
483
+ r
484
+
485
+ when :void
486
+ len = @field_data[data_or_offset, 4].unpack("V")
487
+ void = @field_data[data_or_offset + 4, len].unpack("H*")
488
+ raise "void: #{void.inspect}"
489
+
490
+ when :struct
491
+ read_struct data_or_offset
492
+
493
+ when :list
494
+ list = []
495
+
496
+ raise GffError, "List index not divisable by 4" unless
497
+ data_or_offset % 4 == 0
498
+
499
+ data_or_offset /= 4
500
+
501
+ raise GffError, "List index outside list indices" if
502
+ data_or_offset > @list_indices.size
503
+
504
+ count = @list_indices[data_or_offset]
505
+
506
+ raise GffError, "List index overflow the list indices array" if
507
+ data_or_offset + count > @list_indices.size
508
+
509
+ data_or_offset += 1
510
+
511
+ for i in data_or_offset...(data_or_offset + count)
512
+ list << read_struct(@list_indices[i])
513
+ end
514
+
515
+ list
516
+
517
+ end
518
+
519
+ raise GffError, "Field data overflows from the field data block area\
520
+ offset = #{data_or_offset + len}, len = #{@field_data.size}" if
521
+ len && data_or_offset + len > @field_data.size
522
+
523
+ field.label = label
524
+ field.type = type
525
+ field.value = value
526
+
527
+ [label, field] #::Gff::Element.new(type,label,value)
528
+ end
529
+ end
530
+
531
+
532
+ class NWN::Gff::Writer
533
+ include NWN::Gff
534
+
535
+ attr_reader :bytes
536
+
537
+ def initialize(gff)
538
+ @gff = gff
539
+
540
+ @structs = []
541
+ @fields = []
542
+ @labels = []
543
+ @field_indices = []
544
+ @list_indices = []
545
+ @field_data = ""
546
+
547
+ write_all
548
+ end
549
+
550
+ # Takes a NWN::Gff::Gff object and dumps it as raw bytes,
551
+ # including the header.
552
+ def self.dump(gff)
553
+ self.new(gff).bytes
554
+ end
555
+
556
+ private
557
+
558
+ def get_label_id_for_label str
559
+ @labels << str unless @labels.index(str)
560
+ @labels.index(str)
561
+ end
562
+
563
+ def add_data_field type, label, content
564
+ label_id = get_label_id_for_label label
565
+ @fields.push Types.index(type), label_id, content
566
+ (@fields.size - 1) / 3
567
+ end
568
+
569
+ def write_all
570
+ data = []
571
+ write_struct @gff.root_struct
572
+
573
+ c_offset = 0
574
+ data << [
575
+ @gff.type,
576
+ @gff.version,
577
+
578
+ # Offset of Struct array as bytes from the beginning of the file
579
+ c_offset += 56,
580
+ # Number of elements in Struct array
581
+ @structs.size / 3,
582
+
583
+ # Offset of Field array as bytes from the beginning of the file
584
+ fields_start = c_offset += @structs.size / 3 * 12,
585
+ # Number of elements in Field array
586
+ @fields.size / 3,
587
+
588
+ # Offset of Label array as bytes from the beginning of the file
589
+ c_offset += @fields.size / 3 * 12,
590
+ # Number of elements in Label array
591
+ @labels.size,
592
+
593
+ # Offset of Field Data as bytes from the beginning of the file
594
+ c_offset += @labels.size * 16,
595
+ # Number of bytes in Field Data block
596
+ @field_data.size,
597
+
598
+ # Offset of Field Indices array as bytes from the beginning of the file
599
+ c_offset += @field_data.size,
600
+ # Number of bytes in Field Indices array
601
+ @field_indices.size * 4,
602
+
603
+ # Offset of List Indices array as bytes from the beginning of the file
604
+ c_offset += @field_indices.size * 4,
605
+ # Number of bytes in List Indices array
606
+ @list_indices.size * 4
607
+
608
+ ].pack("a4a4 VV VV VV VV VV VV")
609
+
610
+ data << @structs.pack("V*")
611
+ data << @fields.pack("V*")
612
+ data << @labels.pack("a16" * @labels.size)
613
+ data << @field_data
614
+ data << @field_indices.pack("V*")
615
+ data << @list_indices.pack("V*")
616
+
617
+ @bytes = data.join("")
618
+ end
619
+
620
+ def write_struct struct
621
+ raise GffError, "struct invalid: #{struct.inspect}" unless struct.is_a?(Gff::Struct)
622
+ raise GffError, "struct_id missing from struct" unless struct.struct_id
623
+
624
+ # This holds all field label ids this struct has as a member
625
+ fields_of_this_struct = []
626
+
627
+ # This will hold the index of this struct
628
+ index = @structs.size / 3
629
+
630
+ @structs.push struct.struct_id, 0, 0
631
+
632
+ struct.sort.each {|k,v|
633
+ raise GffError, "Empty label." if !k || k == ""
634
+
635
+ case v.type
636
+ # simple data types
637
+ when :byte, :char, :word, :short, :dword, :int, :float
638
+ format = Formats[v.type]
639
+ fields_of_this_struct << add_data_field(v.type, k, [v.value].pack(format).unpack("V")[0])
640
+
641
+ # complex data types
642
+ when :dword64, :int64, :double, :void
643
+ $stderr.puts "Warning: complex datatypes dword64, int64, double and void are untested."
644
+
645
+ fields_of_this_struct << add_data_field(v.type, k, @field_data.size)
646
+ format = Formats[v.type]
647
+ @field_data << case v.type
648
+ when :dword64
649
+ [
650
+ ( v.value / (2**32) ) & 0xffffffff,
651
+ v.value % (2**32)
652
+ ].pack("II")
653
+ when :void
654
+ [ v.value.size, v.value ].pack("VH*")
655
+ else
656
+ [v.value].pack(format)
657
+ end
658
+
659
+ raise GffError, "unhandled complex datatype #{v.type}"
660
+
661
+ when :struct
662
+ raise GffError, "type = struct, but value not a hash" unless
663
+ v.value.is_a?(Gff::Struct)
664
+
665
+ fields_of_this_struct << add_data_field(v.type, k, write_struct(v.value))
666
+
667
+ when :list
668
+ raise GffError, "type = list, but value not an array" unless
669
+ v.value.is_a?(Array)
670
+
671
+ fields_of_this_struct << add_data_field(v.type, k, 4 * @list_indices.size)
672
+
673
+ count = v.value.size
674
+ tmp = @list_indices.size
675
+ @list_indices << count
676
+ count.times {
677
+ @list_indices << 0
678
+ }
679
+
680
+ v.value.each_with_index do |kk, idx|
681
+ vv = write_struct(kk)
682
+ @list_indices[ idx + tmp + 1 ] = vv
683
+ end
684
+
685
+ when :resref
686
+ fields_of_this_struct << add_data_field(v.type, k, @field_data.size)
687
+ @field_data << [v.value.size, v.value].pack("Ca*")
688
+
689
+ when :cexostr
690
+ fields_of_this_struct << add_data_field(v.type, k, @field_data.size)
691
+ @field_data << [v.value.size, v.value].pack("Va*")
692
+
693
+ when :cexolocstr
694
+ raise GffError, "type = cexolocstr, but value not an array" unless
695
+ v.value.is_a?(Array)
696
+
697
+ fields_of_this_struct << add_data_field(v.type, k, @field_data.size)
698
+
699
+ # total size (4), str_ref (4), str_count (4)
700
+ total_size = 8 + v.value.inject(0) {|t,x| t + x.text.size + 8}
701
+ @field_data << [
702
+ total_size,
703
+ v._str_ref,
704
+ v.value.size
705
+ ].pack("VVV")
706
+
707
+ v.value.each {|s|
708
+ @field_data << [s.language, s.text.size, s.text].pack("VVa*")
709
+ }
710
+
711
+
712
+ else
713
+ raise GffError, "Unknown data type: #{v.type}"
714
+ end
715
+ }
716
+
717
+ # id/type, data_or_offset, nr_of_fields
718
+ @structs[3 * (index) + 2] = fields_of_this_struct.size
719
+
720
+ if fields_of_this_struct.size < 1
721
+ elsif fields_of_this_struct.size == 1
722
+ @structs[3 * (index) + 1] = fields_of_this_struct[0]
723
+ else
724
+ # Offset into field_indices starting where are number of nr_of_fields
725
+ # dwords as indexes into @fields
726
+ @structs[3 * (index) + 1] = 4 * (@field_indices.size)
727
+ @field_indices.push *fields_of_this_struct
728
+ end
729
+
730
+ index
731
+ end
732
+
733
+ end
@@ -0,0 +1,9 @@
1
+ module NWN
2
+ module TwoDA
3
+ class Table
4
+ end
5
+
6
+ class Reader
7
+ end
8
+ end
9
+ end
File without changes
File without changes
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: nwn-lib
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.1"
7
+ date: 2008-06-28 00:00:00 +02:00
8
+ summary: a ruby library for accessing Neverwinter Nights resource files
9
+ require_paths:
10
+ - lib
11
+ email: elven@swordcoast.net
12
+ homepage: http://nwn-lib.elv.es
13
+ rubyforge_project: nwn-lib
14
+ description: a ruby library for accessing Neverwinter Nights resource files
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.4
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Bernhard Stoeckner
31
+ files:
32
+ - COPYING
33
+ - README
34
+ - Rakefile
35
+ - bin/nwn-gff-print
36
+ - bin/nwn-gff-set
37
+ - spec/spec.opts
38
+ - spec/rcov.opts
39
+ - lib/nwn
40
+ - lib/nwn/twoda.rb
41
+ - lib/nwn/gff.rb
42
+ test_files: []
43
+
44
+ rdoc_options:
45
+ - --quiet
46
+ - --line-numbers
47
+ - --inline-source
48
+ - --title
49
+ - "nwn-lib: a ruby library for accessing NWN resource files"
50
+ - --main
51
+ - README
52
+ - --exclude
53
+ - ^(examples|extras)/
54
+ extra_rdoc_files:
55
+ - README
56
+ - COPYING
57
+ executables:
58
+ - nwn-gff-print
59
+ extensions: []
60
+
61
+ requirements: []
62
+
63
+ dependencies: []
64
+