nwn-lib 0.1

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/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
+