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 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
- ==== 0.4.0
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
- ==== 0.4.1
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.1"
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
- message = e.message.split(':', 4)[3].strip
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
@@ -64,7 +64,7 @@ module NWN
64
64
  FileFormatGuesses = {
65
65
  /^ut[cdeimpstw]$/ => :gff,
66
66
  /^(git|are|gic)$/ => :gff,
67
- /^(mod|ifo|fac|ssf|dlg)$/ => :gff,
67
+ /^(mod|ifo|fac|ssf|dlg|itp)$/ => :gff,
68
68
  /^(bic)$/ => :gff,
69
69
  /^ya?ml$/ => :yaml,
70
70
  /^marshal$/ => :marshal,
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, empty, header, *data = *bytes.split(/\r?\n/).map {|v| v.strip }
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
- if empty != ""
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
- data.reject! {|line|
95
- line.size == 0
96
- }
105
+ new_row_data = []
97
106
 
98
- offset = 0
107
+ id_offset = 0
108
+ idx_offset = 0
99
109
  data.each_with_index {|row, idx|
100
- if (idx + offset) != row[0].to_i
101
- $stderr.puts "Warning: row #{idx} has a non-matching ID #{row[0]} (while parsing #{row[0,3].join(' ')})."
102
- offset += (row[0].to_i - idx)
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
- # [1..-1]: Strip off the ID
106
- data[row[0].to_i] = row = Row.new(row[1..-1])
107
- row.table = self
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
- row.map! {|cell|
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} does not have the appropriate amount of cells (has: #{row.size}, want: #{header.size}) (while parsing #{row[0,3].join(' ')})." if
118
- row.size != header.size
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 = data
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
- ret.join("\r\n")
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."
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env nwn-dsl
2
+ # vim: ft=ruby
3
+
4
+ ARGV.each {|f|
5
+ log "Working on #{f} .."
6
+ t = TwoDA::Table.parse(IO.read(f))
7
+ File.open(f, "w") {|n| n.puts t.to_2da }
8
+ log "done."
9
+ }
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.1
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-04 00:00:00 +01:00
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/fix_facings.rb
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
@@ -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."