nwn-lib 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +16 -2
- data/Rakefile +1 -1
- data/SETTINGS +6 -0
- data/bin/nwn-dsl +1 -2
- data/lib/nwn/gff.rb +1 -1
- data/lib/nwn/tlk.rb +175 -0
- data/lib/nwn/twoda.rb +62 -22
- data/scripts/debug_check_objid.rb +29 -0
- data/scripts/reformat_2da +9 -0
- metadata +5 -3
- data/scripts/fix_facings.rb +0 -22
data/CHANGELOG
CHANGED
@@ -106,13 +106,27 @@ Bernhard Stoeckner <elven@swordcoast.net> (5):
|
|
106
106
|
Stuart Coyle <stuart.coyle@gmail.com> (1):
|
107
107
|
TwoDA: use four spaces for field separation instead of tabs, indent columns
|
108
108
|
|
109
|
-
|
109
|
+
=== 0.4.0
|
110
110
|
Bernhard Stoeckner <elven@swordcoast.net>:
|
111
111
|
too many to sanely list, see README for migration information
|
112
112
|
|
113
|
-
|
113
|
+
=== 0.4.1
|
114
114
|
Bernhard Stoeckner <elven@swordcoast.net> (3):
|
115
115
|
Field#field_value=: unbreak setting a new value
|
116
116
|
Scripting: add rudimentary ask method
|
117
117
|
scripts/clean_locstrs.rb: optimise a bit
|
118
118
|
0.4.1-rel
|
119
|
+
|
120
|
+
=== 0.4.2
|
121
|
+
Bernhard Stoeckner <elven@swordcoast.net> (10):
|
122
|
+
scripts/fix_facings.rb: removed, results in corruption
|
123
|
+
bin/nwn-dsl: cleanup exception handling
|
124
|
+
TwoDA::Table: add support for non-windows newlines
|
125
|
+
TwoDA::Table: quote cells with whitespaces
|
126
|
+
TwoDA::Table: a more robust parser handling slightly malformed 2da files
|
127
|
+
scripts: add reformat_2da for validating and prettyprinting 2da files
|
128
|
+
Tlk: basic talktable reading
|
129
|
+
scripts: add a debug script
|
130
|
+
add *.itp to GuessFormats
|
131
|
+
Tlk: basic talktable editing and writing
|
132
|
+
0.4.2-rel
|
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.2"
|
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/SETTINGS
CHANGED
@@ -78,3 +78,9 @@ The default is to keep them as-is.
|
|
78
78
|
Set to a path containing all 2da files to initialize the 2da cache. This is needed for
|
79
79
|
most interactive helpers and a few type infer gizmos.
|
80
80
|
|
81
|
+
== NWN_LIB_TWODA_NEWLINE
|
82
|
+
|
83
|
+
Specify the type of newline that Table#to_2da uses to join rows.
|
84
|
+
0 for windows newlines: \r\n (default)
|
85
|
+
1 for unix newlines: \n
|
86
|
+
2 for caret return only: \r
|
data/bin/nwn-dsl
CHANGED
@@ -18,8 +18,7 @@ $base_script = ARGV.shift
|
|
18
18
|
begin
|
19
19
|
NWN::Gff::Scripting.run_script(IO.read($base_script), nil, ARGV)
|
20
20
|
rescue => e
|
21
|
-
|
22
|
-
$stderr.puts message
|
21
|
+
$stderr.puts e.message
|
23
22
|
if $backtrace
|
24
23
|
$stderr.puts ""
|
25
24
|
$stderr.puts " " + e.backtrace.join("\n")
|
data/lib/nwn/gff.rb
CHANGED
data/lib/nwn/tlk.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
module NWN
|
2
|
+
module Tlk
|
3
|
+
Languages = {
|
4
|
+
0 => :english,
|
5
|
+
1 => :french,
|
6
|
+
2 => :german,
|
7
|
+
3 => :italian,
|
8
|
+
4 => :spanish,
|
9
|
+
5 => :polish,
|
10
|
+
128 => :korean,
|
11
|
+
129 => :chinese_traditional,
|
12
|
+
130 => :chinese_simplified,
|
13
|
+
131 => :japanese,
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
ValidGender = [:male, :female].freeze
|
17
|
+
|
18
|
+
# Tlk wraps a File object that points to a .tlk file.
|
19
|
+
class Tlk
|
20
|
+
HEADER_SIZE = 20
|
21
|
+
DATA_ELEMENT_SIZE = 4 + 16 + 4 + 4 + 4 + 4 + 4
|
22
|
+
|
23
|
+
# The number of strings this Tlk holds.
|
24
|
+
# attr_reader :size
|
25
|
+
|
26
|
+
# The language_id of this Tlk.
|
27
|
+
attr_reader :language
|
28
|
+
|
29
|
+
attr_reader :cache
|
30
|
+
|
31
|
+
# Cereate
|
32
|
+
def initialize io
|
33
|
+
@io = io
|
34
|
+
|
35
|
+
# Read the header
|
36
|
+
@file_type, @file_version, language_id,
|
37
|
+
string_count, string_entries_offset =
|
38
|
+
@io.read(HEADER_SIZE).unpack("A4 A4 I I I")
|
39
|
+
|
40
|
+
raise IOError, "The given IO does not describe a valid tlk table" unless
|
41
|
+
@file_type == "TLK" && @file_version == "V3.0"
|
42
|
+
|
43
|
+
@size = string_count
|
44
|
+
@language = language_id
|
45
|
+
@entries_offset = string_entries_offset
|
46
|
+
|
47
|
+
@cache = {}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a TLK entry as a hash with the following keys:
|
51
|
+
# :text string: The text
|
52
|
+
# :sound string: A sound resref, or "" if no sound is specified.
|
53
|
+
# :sound_length float: Length of the given resref (or 0.0 if no sound is given).
|
54
|
+
#
|
55
|
+
# id is the numeric offset within the Tlk, starting at 0.
|
56
|
+
# The maximum is Tlk#size - 1.
|
57
|
+
def [](id)
|
58
|
+
return { :text => "", :sound => "", :sound_length => 0.0, :volume_variance => 0, :pitch_variance => 0} if id == 0xffffffff
|
59
|
+
|
60
|
+
return @cache[id] if @cache[id]
|
61
|
+
|
62
|
+
raise ArgumentError, "No such string ID: #{id.inspect}" if id >= self.highest_id || id < 0
|
63
|
+
seek_to = HEADER_SIZE + (id) * DATA_ELEMENT_SIZE
|
64
|
+
@io.seek(seek_to)
|
65
|
+
data = @io.read(DATA_ELEMENT_SIZE)
|
66
|
+
|
67
|
+
raise IOError, "Cannot read TLK file, missing string header data." if !data || data.size != 40
|
68
|
+
|
69
|
+
flags, sound_resref, v_variance, p_variance, offset,
|
70
|
+
size, sound_length = data.unpack("I A16 I I I I f")
|
71
|
+
flags = flags.to_i
|
72
|
+
|
73
|
+
@io.seek(@entries_offset + offset)
|
74
|
+
text = @io.read(size)
|
75
|
+
|
76
|
+
raise IOError, "Cannot read TLK file, missing string text data." if !text || text.size != size
|
77
|
+
|
78
|
+
text = flags & 0x1 > 0 ? text : ""
|
79
|
+
sound = flags & 0x2 > 0 ? sound_resref : ""
|
80
|
+
sound_length = flags & 0x4 > 0 ? sound_length.to_f : 0.0
|
81
|
+
|
82
|
+
@cache[id] = {
|
83
|
+
:text => text, :sound => sound, :sound_length => sound_length,
|
84
|
+
:volume_variance => v_variance, :pitch_variance => p_variance
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Add a new entry to this Tlk and return the strref given to it.
|
89
|
+
# To override existing entries, use tlk[][:text] = ".."
|
90
|
+
def add text, sound = "", sound_length = 0.0, v_variance = 0, p_variance = 0
|
91
|
+
next_id = self.highest_id + 1
|
92
|
+
$stderr.puts "put in cache: #{next_id}"
|
93
|
+
@cache[next_id] = {:text => text, :sound => sound, :sound_length => 0.0, :volume_variance => v_variance, :pitch_variance => p_variance}
|
94
|
+
next_id
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return the highest ID in this Tlk table.
|
98
|
+
def highest_id
|
99
|
+
highest_cached = @cache.keys.sort[-1] || 0
|
100
|
+
@size - 1 > highest_cached ? @size - 1 : highest_cached
|
101
|
+
end
|
102
|
+
|
103
|
+
# Write this Tlk to +io+.
|
104
|
+
# Take care not to write it to the same IO object you are reading from.
|
105
|
+
def write_to io
|
106
|
+
text_block = []
|
107
|
+
offsets = []
|
108
|
+
offset = 0
|
109
|
+
for i in 0..self.highest_id do
|
110
|
+
entry = self[i]
|
111
|
+
offsets[i] = offset
|
112
|
+
text_block << entry[:text]
|
113
|
+
offset += entry[:text].size
|
114
|
+
end
|
115
|
+
text_block = text_block.join("")
|
116
|
+
|
117
|
+
header = [
|
118
|
+
@file_type, @file_version,
|
119
|
+
@language,
|
120
|
+
self.highest_id + 1, HEADER_SIZE + (self.highest_id + 1) * DATA_ELEMENT_SIZE
|
121
|
+
].pack("A4 A4 I I I")
|
122
|
+
|
123
|
+
entries = []
|
124
|
+
for i in 0..self.highest_id do
|
125
|
+
entry = self[i]
|
126
|
+
text, sound, sound_length = entry[:text], entry[:sound], entry[:sound_length]
|
127
|
+
flags = 0
|
128
|
+
flags |= 0x01 if text.size > 0
|
129
|
+
flags |= 0x02 if sound.size > 0
|
130
|
+
flags |= 0x04 if sound_length > 0.0
|
131
|
+
entries << [
|
132
|
+
flags, sound, entry[:volume_variance], entry[:pitch_variance], offsets[i], text.size, sound_length
|
133
|
+
].pack("I a16 I I I I f")
|
134
|
+
end
|
135
|
+
entries = entries.join("")
|
136
|
+
|
137
|
+
io.write(header)
|
138
|
+
io.write(entries)
|
139
|
+
io.write(text_block)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# A TlkSet wraps a set of File objects, each pointing to the respective tlk file, making
|
144
|
+
# retrieval easier.
|
145
|
+
class TlkSet
|
146
|
+
# The default male Tlk.
|
147
|
+
attr_reader :dm
|
148
|
+
# The default female Tlk, (or the default male).
|
149
|
+
attr_reader :df
|
150
|
+
# The custom male Tlk, or nil.
|
151
|
+
attr_reader :cm
|
152
|
+
# The custom female Tlk, if specified (cm if no female custom tlk has been specified, nil if none).
|
153
|
+
attr_reader :cf
|
154
|
+
|
155
|
+
def initialize tlk, tlkf = nil, custom = nil, customf = nil
|
156
|
+
@dm = Tlk.new(tlk)
|
157
|
+
@df = tlkf ? Tlk.new(tlkf) : @dm
|
158
|
+
@cm = custom ? Tlk.new(custom) : nil
|
159
|
+
@cf = customf ? Tlk.new(customf) : @cm
|
160
|
+
end
|
161
|
+
|
162
|
+
def [](id, gender = :male)
|
163
|
+
raise ArgumentError, "Invalid Tlk ID: #{id.inspect}" if id > 0xffffffff
|
164
|
+
(if id < 0x01000000
|
165
|
+
gender == :female && @df ? @df : @dm
|
166
|
+
else
|
167
|
+
raise ArgumentError, "Wanted a custom ID, but no custom talk table has been specified." unless @cm
|
168
|
+
id -= 0x01000000
|
169
|
+
gender == :female && @cf ? @cf : @cm
|
170
|
+
end)[id]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
data/lib/nwn/twoda.rb
CHANGED
@@ -45,10 +45,20 @@ module NWN
|
|
45
45
|
# An array of row arrays, without headers.
|
46
46
|
attr_accessor :rows
|
47
47
|
|
48
|
+
# What to use to set up newlines.
|
49
|
+
# Alternatively, specify the environ variable NWN_LIB_2DA_NEWLINE
|
50
|
+
# with one of the following:
|
51
|
+
# 0 for windows newlines: \r\n
|
52
|
+
# 1 for unix newlines: \n
|
53
|
+
# 2 for caret return only: \r
|
54
|
+
# defaults to \r\n.
|
55
|
+
attr_accessor :newline
|
56
|
+
|
48
57
|
# Create a new, empty 2da table.
|
49
58
|
def initialize
|
50
59
|
@columns = []
|
51
60
|
@rows = []
|
61
|
+
@newline = "\r\n"
|
52
62
|
end
|
53
63
|
|
54
64
|
# Creates a new Table object from a given IO source.
|
@@ -74,52 +84,67 @@ module NWN
|
|
74
84
|
# Parses a string that represents a valid 2da definition.
|
75
85
|
# Replaces any content this table may already have.
|
76
86
|
def parse bytes
|
77
|
-
magic,
|
87
|
+
magic, *data = *bytes.split(/\r?\n/).map {|v| v.strip }
|
78
88
|
|
79
89
|
raise ArgumentError, "Not valid 2da: No valid header found" if
|
80
90
|
magic != "2DA V2.0"
|
81
91
|
|
92
|
+
# strip all empty lines; they are regarded as comments
|
93
|
+
data.reject! {|ln| ln.strip == ""}
|
82
94
|
|
83
|
-
|
84
|
-
$stderr.puts "Warning: second line of 2da not empty, continuing anyways."
|
85
|
-
data = [header].concat(data)
|
86
|
-
header = empty
|
87
|
-
end
|
95
|
+
header = data.shift
|
88
96
|
|
89
97
|
header = Shellwords.shellwords(header.strip)
|
90
98
|
data.map! {|line|
|
91
|
-
Shellwords.shellwords(line.strip)
|
99
|
+
r = Shellwords.shellwords(line.strip)
|
100
|
+
# The last cell can be without double quotes even if it contains whitespace
|
101
|
+
r[header.size..-1] = r[header.size..-1].join(" ") if r.size > header.size
|
102
|
+
r
|
92
103
|
}
|
93
104
|
|
94
|
-
|
95
|
-
line.size == 0
|
96
|
-
}
|
105
|
+
new_row_data = []
|
97
106
|
|
98
|
-
|
107
|
+
id_offset = 0
|
108
|
+
idx_offset = 0
|
99
109
|
data.each_with_index {|row, idx|
|
100
|
-
|
101
|
-
|
102
|
-
|
110
|
+
id = row.shift.to_i + id_offset
|
111
|
+
|
112
|
+
raise ArgumentError, "Invalid ID in row #{idx}" unless id >= 0
|
113
|
+
|
114
|
+
# Its an empty row - this is actually valid; we'll fill it up and dump it later.
|
115
|
+
while id > idx + idx_offset
|
116
|
+
new_row_data << Row.new([""] * header.size)
|
117
|
+
idx_offset += 1
|
118
|
+
end
|
119
|
+
|
120
|
+
# NWN automatically increments duplicate IDs - so do we.
|
121
|
+
while id + id_offset < idx
|
122
|
+
$stderr.puts "Warning: duplicate ID found at row #{idx} (id: #{id}); fixing that for you."
|
123
|
+
id_offset += 1
|
124
|
+
id += 1
|
103
125
|
end
|
104
126
|
|
105
|
-
#
|
106
|
-
|
107
|
-
|
127
|
+
# NWN fills in missing columns with an empty value - so do we.
|
128
|
+
row << "" while row.size < header.size
|
129
|
+
|
130
|
+
new_row_data << k_row = Row.new(row)
|
131
|
+
k_row.table = self
|
108
132
|
|
109
|
-
|
133
|
+
k_row.map! {|cell|
|
110
134
|
cell = case cell
|
111
135
|
when nil; nil
|
112
136
|
when "****"; ""
|
113
137
|
else cell
|
114
138
|
end
|
115
139
|
}
|
140
|
+
|
116
141
|
raise ArgumentError,
|
117
|
-
"Row #{idx}
|
118
|
-
|
142
|
+
"Row #{idx} has too many cells for the given header (has #{k_row.size}, want <= #{header.size})" if
|
143
|
+
k_row.size != header.size
|
119
144
|
}
|
120
145
|
|
121
146
|
@columns = header
|
122
|
-
@rows =
|
147
|
+
@rows = new_row_data
|
123
148
|
end
|
124
149
|
|
125
150
|
|
@@ -170,6 +195,7 @@ module NWN
|
|
170
195
|
id_cell_size = @rows.size.to_s.size + CELL_PAD_SPACES
|
171
196
|
max_cell_size_by_column = @columns.map {|col|
|
172
197
|
([col] + by_col(col)).inject(0) {|max, cell|
|
198
|
+
cell = '"%s"' % cell if cell =~ /\s/
|
173
199
|
cell.to_s.size > max ? cell.to_s.size : max
|
174
200
|
} + CELL_PAD_SPACES
|
175
201
|
}
|
@@ -189,11 +215,25 @@ module NWN
|
|
189
215
|
rv << row_idx.to_s + " " * (id_cell_size - row_idx.to_s.size)
|
190
216
|
row.each_with_index {|cell, column_idx|
|
191
217
|
cell = "****" if cell == ""
|
218
|
+
cell = '"%s"' % cell if cell =~ /\s/
|
192
219
|
rv << cell + " " * (max_cell_size_by_column[column_idx] - cell.size)
|
193
220
|
}
|
194
221
|
ret << rv.join("").rstrip
|
195
222
|
}
|
196
|
-
|
223
|
+
|
224
|
+
# Append an empty newline.
|
225
|
+
ret << ""
|
226
|
+
|
227
|
+
ret.join(case ENV['NWN_LIB_2DA_NEWLINE']
|
228
|
+
when "0"
|
229
|
+
"\r\n"
|
230
|
+
when "1"
|
231
|
+
"\n"
|
232
|
+
when "2"
|
233
|
+
"\r"
|
234
|
+
when nil
|
235
|
+
@newlines
|
236
|
+
end)
|
197
237
|
end
|
198
238
|
end
|
199
239
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# This is a debug script used to verify anchored YAML dump loads.
|
2
|
+
# There was/is a bug in some earlier ruby YAML libs that would
|
3
|
+
# incorrectly anchor objects within large dumps; this prints out
|
4
|
+
# those duplicate object ids.
|
5
|
+
|
6
|
+
$obj_ids = {}
|
7
|
+
|
8
|
+
def chk path, obj
|
9
|
+
if $obj_ids[obj.object_id] && $obj_ids[obj.object_id][1] == obj
|
10
|
+
log "Duplicate object ID:"
|
11
|
+
log " #{path}"
|
12
|
+
log " #{$obj_ids[obj.object_id]}"
|
13
|
+
else
|
14
|
+
$obj_ids[obj.object_id] = [path, obj]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
self.each_by_flat_path do |path, obj|
|
19
|
+
case obj
|
20
|
+
when Gff::Field
|
21
|
+
chk path + ".v", obj.v unless obj.v.is_a?(Numeric) || obj.v.is_a?(String)
|
22
|
+
chk path + ".l", obj.l unless obj.l.is_a?(String)
|
23
|
+
chk path, obj
|
24
|
+
else
|
25
|
+
chk path, obj
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
log "#{$obj_ids.size} object ids verified."
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nwn-lib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernhard Stoeckner
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-02-
|
12
|
+
date: 2009-02-05 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -47,6 +47,7 @@ files:
|
|
47
47
|
- lib/nwn/twoda.rb
|
48
48
|
- lib/nwn/settings.rb
|
49
49
|
- lib/nwn/gff.rb
|
50
|
+
- lib/nwn/tlk.rb
|
50
51
|
- lib/nwn/infer.rb
|
51
52
|
- lib/nwn/kivinen.rb
|
52
53
|
- lib/nwn/all.rb
|
@@ -62,9 +63,10 @@ files:
|
|
62
63
|
- tools/verify.sh
|
63
64
|
- tools/migrate_03x_to_04x.sh
|
64
65
|
- scripts/truncate_floats.rb
|
65
|
-
- scripts/
|
66
|
+
- scripts/reformat_2da
|
66
67
|
- scripts/clean_locstrs.rb
|
67
68
|
- scripts/extract_all_items.rb
|
69
|
+
- scripts/debug_check_objid.rb
|
68
70
|
- data/gff-common-nwn1.yaml
|
69
71
|
- BINARIES
|
70
72
|
- DATA_STRUCTURES
|
data/scripts/fix_facings.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
#!/usr/bin/env nwn-dsl
|
2
|
-
|
3
|
-
# The nwn toolset sometimes does weird things with facings;
|
4
|
-
# they flip signedness for no apparent reason.
|
5
|
-
|
6
|
-
# This script fixes that by forcing all facings to be unsigned.
|
7
|
-
|
8
|
-
want Gff::Struct
|
9
|
-
|
10
|
-
count = 0
|
11
|
-
|
12
|
-
self.each_by_flat_path do |label, field|
|
13
|
-
next unless field.is_a?(Gff::Field)
|
14
|
-
next unless field.field_type == :float
|
15
|
-
next unless label =~ %r{\[\d+\]/(Facing|Bearing)$}
|
16
|
-
if field.field_value < 0
|
17
|
-
field.field_value = field.field_value.abs
|
18
|
-
count += 1
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
log "#{count} bearings modified."
|