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 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."