Class: NWN::TwoDA::Table

Inherits:
Object
  • Object
show all
Defined in:
lib/nwn/twoda.rb

Constant Summary collapse

CELL_PAD_SPACES =
4

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeTable

Create a new, empty 2da table.



86
87
88
89
90
91
# File 'lib/nwn/twoda.rb', line 86

def initialize
  @columns = []
  @columns_lookup = []
  @rows = []
  @newline = "\r\n"
end

Instance Attribute Details

#newlineObject

What to use to set up newlines. Alternatively, specify the environ variable NWN_LIB_2DA_NEWLINE with one of the following:

0 for windows newlines: \r\n
1 for unix newlines: \n
2 for caret return only: \r

defaults to rn.



83
84
85
# File 'lib/nwn/twoda.rb', line 83

def newline
  @newline
end

#rowsObject

An array of row arrays, without headers.



74
75
76
# File 'lib/nwn/twoda.rb', line 74

def rows
  @rows
end

Class Method Details

.parse(bytes) ⇒ Object

Parse a existing string containing a full 2da table. Returns a TwoDA::Table.



107
108
109
110
111
# File 'lib/nwn/twoda.rb', line 107

def self.parse bytes
  obj = self.new
  obj.parse bytes
  obj
end

.read_from(io) ⇒ Object

Creates a new Table object from a given IO source.

file

A IO object pointing to a 2da file.



96
97
98
# File 'lib/nwn/twoda.rb', line 96

def self.read_from io
  self.parse io.read()
end

Instance Method Details

#[]=(row, column = nil, value = nil) ⇒ Object

Set a cell or row value.

row

The row to operate on (starts at 0)

column

Optional column name or index.

value

New value, either a full row, or a single value.

Examples:

TwoDA.get('portraits')[1, "BaseResRef"] = "hi"
TwoDA.get('portraits')[1] = %w{1 2 3 4 5 6}


212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/nwn/twoda.rb', line 212

def []= row, column = nil, value = nil
  if value.nil?
    value = column
    raise ArgumentError, "Expected array for setting a whole row" unless value.is_a?(Array)
  end

  if value.is_a?(Array)
    raise ArgumentError, "Given array size does not match table columns (got: #{value.size}, want: #{self.columns.size})" unless value.size == self.columns.size
    new_row = Row.new
    new_row.concat(value.map {|x| x.to_s})

    @rows[row] = new_row

  else
    col = column_name_to_id column
    @rows[row][col] = value

  end
end

#by_col(column, row = nil) ⇒ Object

Retrieve data by column.

column

The column to retrieve (name or id).

row

The row to retrieve (starts at 0), or nil for all rows.

Raises:

  • (ArgumentError)


237
238
239
240
241
# File 'lib/nwn/twoda.rb', line 237

def by_col column, row = nil
  column = column_name_to_id column
  raise ArgumentError, "column must not be nil." if column.nil?
  row.nil? ? @rows.map {|v| v[column] } : (@rows[row.to_i].nil? ? nil : @rows[row.to_i][column])
end

#by_row(row, column = nil) ⇒ Object Also known as: []

Retrieve data by row.

row

The row to retrieve (starts at 0)

column

The column to retrieve (name or id), or nil for all columns.



196
197
198
199
# File 'lib/nwn/twoda.rb', line 196

def by_row row, column = nil
  column = column_name_to_id column
  column.nil? ? @rows[row.to_i] : (@rows[row.to_i].nil? ? nil : @rows[row.to_i][column])
end

#column_name_to_id(column) ⇒ Object

Translate a column name to its array offset; will validate and raise an ArgumentError if the given argument is invalid or the column cannot be resolved.



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/nwn/twoda.rb', line 247

def column_name_to_id column
   case column
    when String, Symbol
      @columns_lookup.index(column.to_s.downcase) or raise ArgumentError,
        "Not a valid column name: #{column}"
    when Integer
      column
    when NilClass
      nil
    else
      raise ArgumentError, "Invalid column type: #{column} as #{column.class}"
  end
end

#columnsObject

An array of all column names present in this 2da table.



67
# File 'lib/nwn/twoda.rb', line 67

def columns; @columns; end

#columns=(c) ⇒ Object



68
69
70
71
# File 'lib/nwn/twoda.rb', line 68

def columns=(c)
  @columns = c
  @columns_lookup = @columns.map(&:downcase)
end

#parse(bytes) ⇒ Object

Parses a string that represents a valid 2da definition. Replaces any content this table may already have. This will cope with all misformatting in the same way that NWN1 itself does. NWN2 employs slightly different parsing rules, and may or may not be compatible in the fringe cases.

Will raise an ArgumentError if the given bytes do not contain a valid 2DA header, or the file is so badly misshaped that it will not ever be parsed correctly by NWN1.

Raises:

  • (ArgumentError)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/nwn/twoda.rb', line 123

def parse bytes
  magic, *data = *bytes.split(/\r?\n/).map {|v| v.strip }

  raise ArgumentError, "Not valid 2da: No valid header found (got: #{magic[0,20].inspect}..)" if
    magic !~ /^2DA\s+V2.0$/

  # strip all empty lines; they are regarded as comments
  data.reject! {|ln| ln.strip == ""}

  header = data.shift

  header = colsplit(header.strip)
  data.map! {|line|
    colsplit(line.strip)
  }

  new_row_data = []

  id_offset = 0
  idx_offset = 0
  data.each_with_index {|row, idx|
    id = row.shift

    NWN.log_debug "Warning: invalid ID in line #{idx}: #{id.inspect}" if id !~ /^\d+$/

    id = id.to_i + id_offset

    # Its an empty row - NWN strictly numbers by counted lines - then so do we.
    while id > idx + idx_offset
      NWN.log_debug "Warning: missing ID at #{id - id_offset}, fixing that for you."
      idx_offset += 1
    end

    # NWN automatically increments duplicate IDs - so do we.
    while id < idx + idx_offset
      NWN.log_debug "Warning: duplicate ID found at row #{idx} (id: #{id}); fixing that for you."
      id_offset += 1
      id += 1
    end

    # NWN fills in missing columns with an empty value - so do we.
    NWN.log_debug "Warning: row #{id} (real: #{id - id_offset}) misses " +
      "#{header.size - row.size} columns at the end, fixed" if
        row.size < header.size

    row << "" while row.size < header.size

    new_row_data << k_row = Row.new(row)
    k_row.table = self

    k_row.map! {|cell|
      cell = case cell
        when nil; raise "Bug in parser: nil-value for cell"
        when "****"; ""
        else cell
      end
    }

    NWN.log_debug "Warning: row #{idx} has too many cells (has #{k_row.size}, want <= #{header.size})" if
      k_row.size > header.size

    k_row.pop while k_row.size > header.size
  }

  self.columns = header
  @rows = new_row_data
end

#to_2daObject

Returns this table as a valid 2da to be written to a file.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/nwn/twoda.rb', line 262

def to_2da
  ret = []

  # Contains the maximum string length by each column,
  # from which we can calulate the padding we need that
  # things align properly.
  id_cell_size = @rows.size.to_s.size + CELL_PAD_SPACES
  max_cell_size_by_column = @columns.map {|col|
    ([col] + by_col(col)).inject(0) {|max, cell|
      cell = '"%s"' % cell if cell =~ /\s/
      cell.to_s.size > max ? cell.to_s.size : max
    } + CELL_PAD_SPACES
  }

  ret << "2DA V2.0"
  ret << ""

  rv = []
  rv << " " * id_cell_size
  @columns.each_with_index {|column, column_idx|
    rv << column + " " * (max_cell_size_by_column[column_idx] - column.size)
  }
  ret << rv.join("").rstrip

  @rows.each_with_index {|row, row_idx|
    rv = []
    rv << row_idx.to_s + " " * (id_cell_size - row_idx.to_s.size)
    row.each_with_index {|cell, column_idx|
      cell = cell ? 1 : 0 if cell.is_a?(TrueClass) || cell.is_a?(FalseClass)
      cell = "****" if cell == ""
      cell = '"%s"' % cell if cell =~ /\s/
      cell = cell.to_s
      rv << cell + " " * (max_cell_size_by_column[column_idx] - cell.size)
    }
    ret << rv.join("").rstrip
  }

  # Append an empty newline.
  ret << ""

  ret.join(case NWN.setting("2da_newline")
    when "0", false
      "\r\n"
    when "1"
      "\n"
    when "2"
      "\r"
    when nil
      @newline
  end)
end

#write_to(io) ⇒ Object

Dump this table to a IO object.



101
102
103
# File 'lib/nwn/twoda.rb', line 101

def write_to io
  io.write(self.to_2da)
end