nwn-lib 0.4.1 → 0.4.2
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 +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."
|