nwn-lib 0.4.6 → 0.4.7
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/CHANGELOG +35 -0
- data/Rakefile +1 -1
- data/bin/nwn-erf +22 -14
- data/bin/nwn-gff +3 -3
- data/bin/nwn-irb +1 -2
- data/lib/nwn/all.rb +0 -1
- data/lib/nwn/erf.rb +62 -14
- data/lib/nwn/gff.rb +1 -1
- data/lib/nwn/gff/reader.rb +43 -16
- data/lib/nwn/gff/writer.rb +18 -9
- data/lib/nwn/res.rb +6 -4
- data/lib/nwn/scripting.rb +2 -2
- data/lib/nwn/settings.rb +12 -0
- data/lib/nwn/twoda.rb +7 -10
- data/scripts/extract_all_items.rb +1 -1
- data/scripts/reformat_2da +1 -1
- data/spec/bin_dsl_spec.rb +36 -0
- data/spec/bin_erf_spec.rb +118 -0
- data/spec/bin_gff_spec.rb +90 -0
- data/spec/erf_spec.rb +63 -46
- data/spec/gff_spec.rb +46 -3
- data/spec/spec_helper.rb +174 -0
- data/spec/struct_spec.rb +21 -0
- data/spec/tlk_spec.rb +0 -17
- data/spec/twoda_spec.rb +0 -68
- data/spec/{wellformed_gff.binary → wellformed_gff.bic} +0 -0
- metadata +6 -4
- data/lib/nwn/helpers.rb +0 -153
data/CHANGELOG
CHANGED
@@ -203,3 +203,38 @@ Bernhard Stoeckner <elven@swordcoast.net> (26):
|
|
203
203
|
to the newly-added specs.
|
204
204
|
|
205
205
|
Please browse the commit log for a full list of changes.
|
206
|
+
|
207
|
+
Bernhard Stoeckner <elven@swordcoast.net> (25):
|
208
|
+
bin/*: do not require rubygems explicitly
|
209
|
+
remove obsolete Gff::Helpers module
|
210
|
+
Erf: add support for ERF V1.1
|
211
|
+
env var NWN_LIB_DEBUG sends debug messages to stderr when set
|
212
|
+
add support for Microsoft Windows
|
213
|
+
R::ContentObject: do not fail on missing io, seek properly
|
214
|
+
R::ContentObject#filename: "unknown-N" instead of nil for unknown res_types
|
215
|
+
R::ContentObject: properly raise ENOENT on invalid io and file
|
216
|
+
G::Reader, Writer: work on io objects instead of strings
|
217
|
+
G::Writer: fix struct verify exception on reading substructs
|
218
|
+
specs: move all test stubs into _helper
|
219
|
+
Erf: improve broken locstr header handling
|
220
|
+
Erf: raise errors for invalid filenames on add, add_file
|
221
|
+
Erf: fix misaligned offsets in reslist
|
222
|
+
nwn-erf: fix parsing erf files where day_of_year = 0
|
223
|
+
nwn-erf: output non-errors to stdout instead of stderr
|
224
|
+
nwn-erf: fix packing up files
|
225
|
+
nwn-erf: add specs
|
226
|
+
Erf: reproduction spec checks for binary equality with sample
|
227
|
+
Gff: add specs for each_by_flat_path, to_gff
|
228
|
+
nwn-gff: fix backup strategy handler missing FileUtils
|
229
|
+
nwn-gff: add specs
|
230
|
+
rename spec/wellformed_gff.binary to .bic
|
231
|
+
nwn-dsl: add specs
|
232
|
+
0.4.7-rel
|
233
|
+
|
234
|
+
A real bugfix-release, this takes care of a whole swad of fixes
|
235
|
+
in erf and gff handling, and also improves Windows compatibility.
|
236
|
+
|
237
|
+
Particularily, erf writing was kind of broken in the previous release,
|
238
|
+
putting wrong offsets for files created with nwn-erf.
|
239
|
+
|
240
|
+
Please browse the commit log for a full list of changes.
|
data/Rakefile
CHANGED
@@ -9,7 +9,7 @@ include FileUtils
|
|
9
9
|
# Configuration
|
10
10
|
##############################################################################
|
11
11
|
NAME = "nwn-lib"
|
12
|
-
VERS = "0.4.
|
12
|
+
VERS = "0.4.7"
|
13
13
|
CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
|
14
14
|
RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
|
15
15
|
'nwn-lib: a ruby library for accessing NWN resource files', \
|
data/bin/nwn-erf
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
|
-
require 'rubygems'
|
3
2
|
require 'optparse'
|
4
3
|
require 'nwn/all'
|
5
4
|
require 'tempfile'
|
@@ -9,6 +8,7 @@ $verbose = false
|
|
9
8
|
$type = 'ERF'
|
10
9
|
$allow_duplicates = false
|
11
10
|
$descriptions = {}
|
11
|
+
$version = "V1.0"
|
12
12
|
|
13
13
|
# tar-compat mode: first argument is options if no dash is specified.
|
14
14
|
ARGV[0] = "-" + ARGV[0] if ARGV.size > 0 && ARGV[0][0] != ?-
|
@@ -63,6 +63,13 @@ begin OptionParser.new do |o|
|
|
63
63
|
$type = 'MOD'
|
64
64
|
end
|
65
65
|
|
66
|
+
o.on "-0", "Create (only -c) V1.0 ERF, 16 byte resrefs. (NWN1, default)" do
|
67
|
+
$version = "V1.0"
|
68
|
+
end
|
69
|
+
o.on "-1", "Create (only -c) V1.0 ERF, 32 byte resrefs. (NWN2)." do
|
70
|
+
$version = "V1.1"
|
71
|
+
end
|
72
|
+
|
66
73
|
o.separator " "
|
67
74
|
o.separator "Hacks:"
|
68
75
|
|
@@ -91,15 +98,15 @@ def input filename = nil
|
|
91
98
|
if $file.nil?
|
92
99
|
yield $stdin
|
93
100
|
else
|
94
|
-
File.open(filename || $file, "
|
101
|
+
File.open(filename || $file, "rb") {|f| yield f}
|
95
102
|
end
|
96
103
|
end
|
97
104
|
|
98
|
-
def output
|
105
|
+
def output filename = nil
|
99
106
|
if $file.nil?
|
100
107
|
yield $stdout
|
101
108
|
else
|
102
|
-
File.open(filename || $file,
|
109
|
+
File.open(filename || $file, "wb") {|f| yield f}
|
103
110
|
end
|
104
111
|
end
|
105
112
|
|
@@ -107,14 +114,14 @@ case $action
|
|
107
114
|
when :t
|
108
115
|
input {|f|
|
109
116
|
erf = NWN::Erf::Erf.new(f)
|
110
|
-
|
117
|
+
puts "# %14s %16s %-10s %-4s %s" % %w{offset size date type filename} if $verbose
|
111
118
|
erf.content.each {|c|
|
112
119
|
if !$verbose
|
113
|
-
|
120
|
+
puts "%s" % [c.filename]
|
114
121
|
else
|
115
|
-
|
122
|
+
puts "%16d %16d %10s %4d %s" % [
|
116
123
|
c.offset, c.size,
|
117
|
-
Date.ordinal(1900 + erf.year, erf.day_of_year).strftime("%Y-%m-%d"),
|
124
|
+
Date.ordinal(1900 + erf.year, 1 + erf.day_of_year).strftime("%Y-%m-%d"),
|
118
125
|
c.res_type, c.filename
|
119
126
|
]
|
120
127
|
end
|
@@ -125,8 +132,8 @@ case $action
|
|
125
132
|
input {|f|
|
126
133
|
erf = NWN::Erf::Erf.new(f)
|
127
134
|
erf.content.each {|c|
|
128
|
-
|
129
|
-
|
135
|
+
puts "%s" % [c.filename] if $verbose
|
136
|
+
output(c.filename) {|ff|
|
130
137
|
ff.write(c.get)
|
131
138
|
}
|
132
139
|
}
|
@@ -135,6 +142,7 @@ case $action
|
|
135
142
|
when :c
|
136
143
|
erf = NWN::Erf::Erf.new
|
137
144
|
erf.file_type = $type if $type
|
145
|
+
erf.file_version = $version
|
138
146
|
|
139
147
|
if $descriptions
|
140
148
|
erf.localized_strings.merge! $descriptions
|
@@ -142,10 +150,10 @@ case $action
|
|
142
150
|
end
|
143
151
|
|
144
152
|
ARGV.each {|a|
|
145
|
-
|
153
|
+
puts File.basename(a) if $verbose
|
146
154
|
raise ArgumentError, "#{File.basename(a)} already present in erf." if
|
147
155
|
!$allow_duplicates && erf.has?(a)
|
148
|
-
erf.
|
156
|
+
erf.add_file a
|
149
157
|
}
|
150
158
|
output {|f| erf.write_to(f) }
|
151
159
|
|
@@ -163,7 +171,7 @@ case $action
|
|
163
171
|
when :a
|
164
172
|
raise ArgumentError, "#{File.basename(arg)} already present in erf." if
|
165
173
|
!$allow_duplicates && erf.has?(arg)
|
166
|
-
erf.
|
174
|
+
erf.add_file arg
|
167
175
|
|
168
176
|
when :r
|
169
177
|
erf.content.reject! {|con|
|
@@ -173,7 +181,7 @@ case $action
|
|
173
181
|
}
|
174
182
|
|
175
183
|
tempfile = Tempfile.new("nwn-erf", File.dirname($file || "."))
|
176
|
-
output(
|
184
|
+
output(tempfile.path) do |f|
|
177
185
|
erf.write_to(f)
|
178
186
|
end
|
179
187
|
|
data/bin/nwn-gff
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
|
-
require 'rubygems'
|
3
2
|
require 'optparse'
|
4
3
|
require 'nwn/all'
|
4
|
+
require 'fileutils'
|
5
5
|
|
6
6
|
Thread.abort_on_exception = true
|
7
7
|
|
@@ -116,7 +116,7 @@ elsif :in == $options[:outformat]
|
|
116
116
|
end
|
117
117
|
|
118
118
|
vputs "Reading: #{$options[:infile]}"
|
119
|
-
data_in = $options[:infile] == '-' ? $stdin.read :
|
119
|
+
data_in = $options[:infile] == '-' ? $stdin.read : File.open($options[:infile], "rb")
|
120
120
|
data_in = NWN::Gff.read(data_in, $options[:informat])
|
121
121
|
|
122
122
|
# verify that we read a GFF struct
|
@@ -193,6 +193,6 @@ if $options[:backup] && FileTest.exists?($options[:outfile])
|
|
193
193
|
FileUtils.cp($options[:outfile], bdest) if bdest
|
194
194
|
end
|
195
195
|
|
196
|
-
write_to = $options[:outfile] == '-' ? $stdout : File.open($options[:outfile], '
|
196
|
+
write_to = $options[:outfile] == '-' ? $stdout : File.open($options[:outfile], 'wb')
|
197
197
|
|
198
198
|
NWN::Gff.write(write_to, $options[:outformat], data_in)
|
data/bin/nwn-irb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
|
-
require 'rubygems'
|
3
2
|
require 'optparse'
|
4
3
|
require 'nwn/all'
|
5
4
|
require 'irb'
|
@@ -19,7 +18,7 @@ def save destination = nil
|
|
19
18
|
|
20
19
|
File.expand_path(destination ||= $file)
|
21
20
|
$stderr.puts "Saving to `#{destination}' .."
|
22
|
-
File.open(destination, "
|
21
|
+
File.open(destination, "wb") {|d|
|
23
22
|
NWN::Gff.write(d, NWN::Gff.guess_file_format(destination), $gff)
|
24
23
|
}
|
25
24
|
$stderr.puts "saved."
|
data/lib/nwn/all.rb
CHANGED
data/lib/nwn/erf.rb
CHANGED
@@ -28,8 +28,34 @@ module NWN
|
|
28
28
|
read_from io if io
|
29
29
|
end
|
30
30
|
|
31
|
+
def add_file filename, io = nil
|
32
|
+
fnlen = filename_length @file_version
|
33
|
+
raise ArgumentError, "Invalid filename: #{filename.inspect}" if
|
34
|
+
filename.size == 0 || filename.size > (fnlen + 4)
|
35
|
+
super(filename, io)
|
36
|
+
end
|
37
|
+
|
38
|
+
def add o
|
39
|
+
fnlen = filename_length @file_version
|
40
|
+
raise ArgumentError, "Invalid filename: #{o.filename.inspect}" if
|
41
|
+
o.resref.size == 0 || o.resref.size > fnlen
|
42
|
+
super(o)
|
43
|
+
end
|
44
|
+
|
31
45
|
private
|
32
46
|
|
47
|
+
def filename_length version
|
48
|
+
case version
|
49
|
+
when "V1.0"
|
50
|
+
16
|
51
|
+
when "V1.1"
|
52
|
+
32
|
53
|
+
else
|
54
|
+
raise IOError, "Invalid ERF version: #{version}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
33
59
|
def read_from io
|
34
60
|
header = @io.read(160)
|
35
61
|
raise IOError, "Cannot read header: Not a erf file?" unless
|
@@ -46,34 +72,44 @@ module NWN
|
|
46
72
|
raise IOError, "Cannot read erf stream: invalid type #{@file_type.inspect}" unless
|
47
73
|
NWN::Erf::ValidTypes.index(@file_type)
|
48
74
|
|
49
|
-
|
50
|
-
@filename_length = 16
|
51
|
-
else
|
52
|
-
raise IOError, "Invalid erf version: #{@file_version}"
|
53
|
-
end
|
75
|
+
fnlen = filename_length @file_version
|
54
76
|
|
55
77
|
@io.seek(offset_to_locstr)
|
56
78
|
locstr = @io.read(locstr_size)
|
57
79
|
|
58
80
|
for lstr in 0...locstr_count do
|
59
|
-
|
60
|
-
|
81
|
+
if locstr.nil? || locstr.size == 0
|
82
|
+
NWN.log_debug "locstr table: not enough entries (expected: #{locstr_count}, got: #{lstr})"
|
83
|
+
break
|
84
|
+
end
|
85
|
+
|
86
|
+
if locstr.size < 8
|
87
|
+
NWN.log_debug "locstr table: not enough entries (expected: #{locstr_count}, got: #{lstr})" +
|
88
|
+
" partial data: #{locstr.inspect}"
|
89
|
+
break
|
90
|
+
end
|
61
91
|
|
62
92
|
lid, strsz = locstr.unpack("V V")
|
93
|
+
if strsz > locstr.size - 8
|
94
|
+
NWN.log_debug "locstr table: given strsz is bigger than available data, truncating"
|
95
|
+
strsz = locstr.size - 8
|
96
|
+
end
|
63
97
|
str = locstr.unpack("x8 a#{strsz}")[0]
|
64
|
-
|
65
|
-
|
66
|
-
|
98
|
+
|
99
|
+
# This just means that the given locstr size was encoded wrongly -
|
100
|
+
# the old erf.exe is known to do that.
|
101
|
+
NWN.log_debug "Expected locstr size does not match actual " +
|
102
|
+
"string size (want: #{strsz}, got #{str.size} of #{str.inspect})" if strsz != str.size
|
67
103
|
|
68
104
|
@localized_strings[lid] = str
|
69
105
|
locstr = locstr[8 + str.size .. -1]
|
70
106
|
end
|
71
107
|
|
72
|
-
keylist_entry_size =
|
108
|
+
keylist_entry_size = fnlen + 4 + 2 + 2
|
73
109
|
@io.seek(offset_to_keys)
|
74
110
|
keylist = @io.read(keylist_entry_size * entry_count)
|
75
111
|
raise IOError, "keylist too short" if keylist.size != keylist_entry_size * entry_count
|
76
|
-
keylist = keylist.unpack("
|
112
|
+
keylist = keylist.unpack("A#{fnlen} V v v" * entry_count)
|
77
113
|
keylist.each_slice(4) {|resref, res_id, res_type, unused|
|
78
114
|
@content << NWN::Resources::ContentObject.new(resref, res_type, @io)
|
79
115
|
}
|
@@ -95,9 +131,21 @@ module NWN
|
|
95
131
|
|
96
132
|
# Writes this Erf to a io stream.
|
97
133
|
def write_to io
|
134
|
+
fnlen = filename_length @file_version
|
135
|
+
|
98
136
|
locstr = @localized_strings.map {|x| [x[0], x[1].size, x[1]].pack("V V a*") }.join("")
|
99
|
-
keylist = @content.map {|c|
|
100
|
-
|
137
|
+
keylist = @content.map {|c|
|
138
|
+
NWN.log_debug "truncating filename #{c.resref}, longer than #{fnlen}" if c.resref.size > fnlen
|
139
|
+
[c.resref, @content.index(c), c.res_type, 0].pack("a#{fnlen} V v v")
|
140
|
+
}.join("")
|
141
|
+
|
142
|
+
pre_offset = 160 + locstr.size + keylist.size + 8 * @content.size
|
143
|
+
|
144
|
+
reslist = @content.map {|c|
|
145
|
+
offset = pre_offset +
|
146
|
+
@content[0, @content.index(c)].inject(0) {|sum,x| sum + x.size }
|
147
|
+
[offset, c.size].pack("V V")
|
148
|
+
}.join("")
|
101
149
|
|
102
150
|
offset_to_locstr = 160
|
103
151
|
offset_to_keylist = offset_to_locstr + locstr.size
|
data/lib/nwn/gff.rb
CHANGED
data/lib/nwn/gff/reader.rb
CHANGED
@@ -4,22 +4,26 @@ class NWN::Gff::Reader
|
|
4
4
|
|
5
5
|
attr_reader :root_struct
|
6
6
|
|
7
|
-
|
8
|
-
# This is not needed usually; use Reader.read instead.
|
9
|
-
def initialize bytes
|
10
|
-
@bytes = bytes
|
7
|
+
private_class_method :new
|
11
8
|
|
12
|
-
|
9
|
+
# Create a new Reader with the given +io+ and immediately parse it.
|
10
|
+
def self.read io
|
11
|
+
t = new(io)
|
12
|
+
t.root_struct
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def initialize io #:nodoc:
|
16
|
+
@io = io
|
17
|
+
read_all
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
22
|
def read_all
|
23
|
+
header = @io.read(160)
|
24
|
+
raise IOError, "Cannot read header" unless header &&
|
25
|
+
header.size == 160
|
26
|
+
|
23
27
|
type, version,
|
24
28
|
struct_offset, struct_count,
|
25
29
|
field_offset, field_count,
|
@@ -27,9 +31,9 @@ class NWN::Gff::Reader
|
|
27
31
|
field_data_offset, field_data_count,
|
28
32
|
field_indices_offset, field_indices_count,
|
29
33
|
list_indices_offset, list_indices_count =
|
30
|
-
|
34
|
+
header.unpack("a4a4 VV VV VV VV VV VV")
|
31
35
|
|
32
|
-
raise GffError, "Unknown version #{
|
36
|
+
raise GffError, "Unknown version #{version}; not a gff?" unless
|
33
37
|
version == "V3.2"
|
34
38
|
|
35
39
|
raise GffError, "struct offset at wrong place, not a gff?" unless
|
@@ -39,12 +43,35 @@ class NWN::Gff::Reader
|
|
39
43
|
field_len = field_count * 16
|
40
44
|
label_len = label_count * 16
|
41
45
|
|
42
|
-
@
|
43
|
-
@
|
44
|
-
|
45
|
-
@
|
46
|
-
|
47
|
-
@
|
46
|
+
@io.seek(struct_offset)
|
47
|
+
@structs = @io.read(struct_len)
|
48
|
+
raise IOError, "cannot read structs" unless @structs && @structs.size == struct_len
|
49
|
+
@structs = @structs.unpack("V*")
|
50
|
+
|
51
|
+
@io.seek(field_offset)
|
52
|
+
@fields = @io.read(field_len)
|
53
|
+
raise IOError, "cannot read fields" unless @fields && @fields.size == field_len
|
54
|
+
@fields = @fields.unpack("V*")
|
55
|
+
|
56
|
+
@io.seek(label_offset)
|
57
|
+
@labels = @io.read(label_len)
|
58
|
+
raise IOError, "cannot read labels" unless @labels && @labels.size == label_len
|
59
|
+
@labels = @labels.unpack("A16" * label_count)
|
60
|
+
|
61
|
+
@io.seek(field_data_offset)
|
62
|
+
@field_data = @io.read(field_data_count)
|
63
|
+
raise IOError, "cannot read field_data" unless @field_data && @field_data.size == field_data_count
|
64
|
+
|
65
|
+
@io.seek(field_indices_offset)
|
66
|
+
@field_indices = @io.read(field_indices_count)
|
67
|
+
raise IOError, "cannot read field_indices" unless @field_indices && @field_indices.size == field_indices_count
|
68
|
+
@field_indices = @field_indices.unpack("V*")
|
69
|
+
|
70
|
+
@io.seek(list_indices_offset)
|
71
|
+
@list_indices = @io.read(list_indices_count)
|
72
|
+
raise IOError, "cannot read list_indices" unless @list_indices && @list_indices.size == list_indices_count
|
73
|
+
@list_indices = @list_indices.unpack("V*")
|
74
|
+
|
48
75
|
@root_struct = read_struct 0, type.strip, version
|
49
76
|
end
|
50
77
|
|
data/lib/nwn/gff/writer.rb
CHANGED
@@ -1,10 +1,25 @@
|
|
1
1
|
class NWN::Gff::Writer
|
2
|
-
include NWN
|
3
2
|
include NWN::Gff
|
4
3
|
|
4
|
+
private_class_method :new
|
5
|
+
|
5
6
|
attr_reader :bytes
|
6
7
|
|
7
|
-
|
8
|
+
# Takes a NWN::Gff::Gff object and dumps it to +io+,
|
9
|
+
# including the header.
|
10
|
+
# If +io+ is nil, return the raw bytes, otherwise
|
11
|
+
# the number of bytes written.
|
12
|
+
def self.dump(gff, io = nil, data_type = nil)
|
13
|
+
ret = new(gff, data_type).bytes
|
14
|
+
if io
|
15
|
+
io.write(ret)
|
16
|
+
ret.size
|
17
|
+
else
|
18
|
+
ret
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(gff, data_type = nil) #:nodoc:
|
8
23
|
@gff = gff
|
9
24
|
@data_type = data_type
|
10
25
|
|
@@ -18,12 +33,6 @@ class NWN::Gff::Writer
|
|
18
33
|
write_all
|
19
34
|
end
|
20
35
|
|
21
|
-
# Takes a NWN::Gff::Gff object and dumps it as raw bytes,
|
22
|
-
# including the header.
|
23
|
-
def self.dump(gff, data_type = nil)
|
24
|
-
self.new(gff, data_type).bytes
|
25
|
-
end
|
26
|
-
|
27
36
|
private
|
28
37
|
|
29
38
|
def get_label_id_for_label str
|
@@ -127,7 +136,7 @@ private
|
|
127
136
|
|
128
137
|
when :struct
|
129
138
|
raise GffError, "type = struct, but value not a hash" unless
|
130
|
-
v.field_value.is_a?(
|
139
|
+
v.field_value.is_a?(Struct)
|
131
140
|
|
132
141
|
fields_of_this_struct << add_data_field(v.field_type, k, write_struct(v.field_value))
|
133
142
|
|