wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
dyn_table.cpp
Go to the documentation of this file.
1
6
8
9#include <cstring>
10#include <format>
11#include <utility>
12
13namespace wowlib::db {
14 namespace detail {
15 namespace {
18 AccessKind kindOf(const Column& column) {
19 switch (column.type) {
23 case ColumnType::Int: break;
24 }
25 switch (column.bits) {
26 case 8: return column.isSigned ? AccessKind::I8 : AccessKind::U8;
27 case 16: return column.isSigned ? AccessKind::I16 : AccessKind::U16;
28 case 64: return column.isSigned ? AccessKind::I64 : AccessKind::U64;
29 default: return column.isSigned ? AccessKind::I32 : AccessKind::U32;
30 }
31 }
32
36 void storePodInt(std::byte* p, AccessKind kind, std::int64_t v) {
37 switch (kind) {
38 case AccessKind::I8: {
39 auto x = static_cast<std::int8_t>(v);
40 std::memcpy(p, &x, sizeof x);
41 break;
42 }
43 case AccessKind::U8: {
44 auto x = static_cast<std::uint8_t>(v);
45 std::memcpy(p, &x, sizeof x);
46 break;
47 }
48 case AccessKind::I16: {
49 auto x = static_cast<std::int16_t>(v);
50 std::memcpy(p, &x, sizeof x);
51 break;
52 }
53 case AccessKind::U16: {
54 auto x = static_cast<std::uint16_t>(v);
55 std::memcpy(p, &x, sizeof x);
56 break;
57 }
58 case AccessKind::I32: {
59 auto x = static_cast<std::int32_t>(v);
60 std::memcpy(p, &x, sizeof x);
61 break;
62 }
63 case AccessKind::U32: {
64 auto x = static_cast<std::uint32_t>(v);
65 std::memcpy(p, &x, sizeof x);
66 break;
67 }
68 case AccessKind::I64: {
69 auto x = static_cast<std::int64_t>(v);
70 std::memcpy(p, &x, sizeof x);
71 break;
72 }
73 case AccessKind::U64: {
74 auto x = static_cast<std::uint64_t>(v);
75 std::memcpy(p, &x, sizeof x);
76 break;
77 }
78 case AccessKind::F32:
79 case AccessKind::Str:
81 break; // never POD-int-stored
82 }
83 }
84
87 std::int64_t loadPodInt(const std::byte* p, AccessKind kind) {
88 switch (kind) {
89 case AccessKind::I8: {
90 std::int8_t x;
91 std::memcpy(&x, p, sizeof x);
92 return x;
93 }
94 case AccessKind::U8: {
95 std::uint8_t x;
96 std::memcpy(&x, p, sizeof x);
97 return x;
98 }
99 case AccessKind::I16: {
100 std::int16_t x;
101 std::memcpy(&x, p, sizeof x);
102 return x;
103 }
104 case AccessKind::U16: {
105 std::uint16_t x;
106 std::memcpy(&x, p, sizeof x);
107 return x;
108 }
109 case AccessKind::I32: {
110 std::int32_t x;
111 std::memcpy(&x, p, sizeof x);
112 return x;
113 }
114 case AccessKind::U32: {
115 std::uint32_t x;
116 std::memcpy(&x, p, sizeof x);
117 return x;
118 }
119 case AccessKind::I64:
120 case AccessKind::U64: {
121 std::int64_t x;
122 std::memcpy(&x, p, sizeof x);
123 return x;
124 }
125 case AccessKind::F32:
126 case AccessKind::Str:
128 return 0;
129 }
130 return 0;
131 }
132 }
133
134 ColumnRows::ColumnRows(std::span<const Column> schema) : _schema{schema} {
135 _facts.reserve(schema.size());
136 _cols.resize(schema.size());
137 for (std::size_t c = 0; c < schema.size(); ++c) {
138 const Column& col = schema[c];
139 Facts f{};
140 f.kind = kindOf(col);
141 switch (col.type) {
142 case ColumnType::Int:
143 f.elemBytes = static_cast<std::uint8_t>(col.bits / 8);
144 f.podSlots = col.arrayLen;
145 break;
147 f.elemBytes = 4;
148 f.podSlots = col.arrayLen;
149 break;
151 f.strSlots = col.arrayLen;
152 break;
154 f.strSlots = col.localeCount;
155 f.hasFlags = true;
156 break;
157 }
158 _facts.push_back(f);
159 if (col.isId && _idColumn == SIZE_MAX) _idColumn = c;
160 }
161 }
162
163 std::byte* ColumnRows::_podPtr(std::size_t record, std::size_t column, std::size_t element) {
164 const Facts& f = _facts[column];
165 return _cols[column].pod.data() + (record * f.podSlots + element) * f.elemBytes;
166 }
167
168 const std::byte* ColumnRows::_podPtr(std::size_t record, std::size_t column, std::size_t element) const {
169 const Facts& f = _facts[column];
170 return _cols[column].pod.data() + (record * f.podSlots + element) * f.elemBytes;
171 }
172
174 for (Store& s : _cols) {
175 s.pod.clear();
176 s.strs.clear();
177 s.flags.clear();
178 }
179 _rows = 0;
180 }
181
182 void ColumnRows::reserve(std::size_t n) {
183 for (std::size_t c = 0; c < _cols.size(); ++c) {
184 const Facts& f = _facts[c];
185 if (f.podSlots != 0) _cols[c].pod.reserve(n * f.podSlots * f.elemBytes);
186 if (f.strSlots != 0) _cols[c].strs.reserve(n * f.strSlots);
187 if (f.hasFlags) _cols[c].flags.reserve(n);
188 }
189 }
190
191 std::size_t ColumnRows::add() {
192 for (std::size_t c = 0; c < _cols.size(); ++c) {
193 const Facts& f = _facts[c];
194 if (f.podSlots != 0)
195 _cols[c].pod.resize(_cols[c].pod.size() + std::size_t{f.podSlots} * f.elemBytes, std::byte{0});
196 if (f.strSlots != 0) _cols[c].strs.resize(_cols[c].strs.size() + f.strSlots);
197 if (f.hasFlags) _cols[c].flags.push_back(0);
198 }
199 return _rows++;
200 }
201
202 std::size_t ColumnRows::size() const { return _rows; }
203
204 std::uint32_t ColumnRows::idOf(std::size_t record) const {
205 if (_idColumn == SIZE_MAX) return 0;
206 return static_cast<std::uint32_t>(loadPodInt(_podPtr(record, _idColumn, 0), _facts[_idColumn].kind));
207 }
208
209 void ColumnRows::setInt(std::size_t record, std::size_t column, std::size_t element, std::int64_t value) {
210 const Facts& f = _facts[column];
211 if (f.kind == AccessKind::LocStr) {
212 _cols[column].flags[record] = static_cast<std::uint32_t>(value);
213 return;
214 }
215 if (f.podSlots != 0 && f.kind != AccessKind::F32) storePodInt(_podPtr(record, column, element), f.kind, value);
216 // float / string columns never receive setInt (record_bridge parity).
217 }
218
219 void ColumnRows::setFloat(std::size_t record, std::size_t column, std::size_t element, float value) {
220 if (_facts[column].kind == AccessKind::F32) std::memcpy(_podPtr(record, column, element), &value, sizeof value);
221 }
222
223 void ColumnRows::setString(std::size_t record, std::size_t column, std::size_t element, std::string_view value) {
224 const Facts& f = _facts[column];
225 if (f.strSlots != 0) _cols[column].strs[record * f.strSlots + element] = std::string{value};
226 }
227
228 void ColumnRows::cloneWithId(std::size_t src, std::uint32_t newId) {
229 const std::size_t fresh = add();
230 for (std::size_t c = 0; c < _cols.size(); ++c) {
231 const Facts& f = _facts[c];
232 Store& s = _cols[c];
233 if (f.podSlots != 0) {
234 const std::size_t stride = std::size_t{f.podSlots} * f.elemBytes;
235 std::memcpy(s.pod.data() + fresh * stride, s.pod.data() + src * stride, stride);
236 }
237 for (std::size_t e = 0; e < f.strSlots; ++e) s.strs[fresh * f.strSlots + e] = s.strs[src * f.strSlots + e];
238 if (f.hasFlags) s.flags[fresh] = s.flags[src];
239 }
240 if (_idColumn != SIZE_MAX)
241 storePodInt(_podPtr(fresh, _idColumn, 0), _facts[_idColumn].kind, newId);
242 }
243
244 std::size_t ColumnRows::findById(std::uint32_t id) const {
245 for (std::size_t r = 0; r < _rows; ++r)
246 if (idOf(r) == id) return r;
247 return _rows;
248 }
249
250 std::int64_t ColumnRows::getInt(std::size_t record, std::size_t column, std::size_t element) const {
251 const Facts& f = _facts[column];
252 if (f.kind == AccessKind::LocStr) return _cols[column].flags[record];
253 if (f.podSlots == 0 || f.kind == AccessKind::F32) return 0;
254 // as the row-store bridge answered for non-int members
255 return loadPodInt(_podPtr(record, column, element), f.kind);
256 }
257
258 std::uint32_t ColumnRows::getSlot(std::size_t record, std::size_t column, std::size_t element) const {
259 const Facts& f = _facts[column];
260 switch (f.kind) {
261 case AccessKind::F32: {
262 std::uint32_t x;
263 std::memcpy(&x, _podPtr(record, column, element), sizeof x);
264 return x;
265 }
266 case AccessKind::Str:
268 return 0; // string refs are journaled, never slot-read
269 default:
270 return static_cast<std::uint32_t>(loadPodInt(_podPtr(record, column, element), f.kind));
271 }
272 }
273
274 std::string_view ColumnRows::getString(std::size_t record, std::size_t column, std::size_t element) const {
275 const Facts& f = _facts[column];
276 if (f.strSlots == 0) return {};
277 return _cols[column].strs[record * f.strSlots + element];
278 }
279
280 void ColumnRows::eraseRow(std::size_t record) {
281 for (std::size_t c = 0; c < _cols.size(); ++c) {
282 const Facts& f = _facts[c];
283 Store& s = _cols[c];
284 if (f.podSlots != 0) {
285 const std::size_t stride = std::size_t{f.podSlots} * f.elemBytes;
286 s.pod.erase(s.pod.begin() + static_cast<std::ptrdiff_t>(record * stride),
287 s.pod.begin() + static_cast<std::ptrdiff_t>((record + 1) * stride));
288 }
289 if (f.strSlots != 0)
290 s.strs.erase(s.strs.begin() + static_cast<std::ptrdiff_t>(record * f.strSlots),
291 s.strs.begin() + static_cast<std::ptrdiff_t>((record + 1) * f.strSlots));
292 if (f.hasFlags) s.flags.erase(s.flags.begin() + static_cast<std::ptrdiff_t>(record));
293 }
294 --_rows;
295 }
296
297 std::string& ColumnRows::stringSlot(std::size_t record, std::size_t column, std::size_t element) {
298 return _cols[column].strs[record * _facts[column].strSlots + element];
299 }
300
301 std::uint32_t& ColumnRows::flagsSlot(std::size_t record, std::size_t column) {
302 return _cols[column].flags[record];
303 }
304
305 std::uint32_t ColumnRows::flagsSlot(std::size_t record, std::size_t column) const {
306 return _cols[column].flags[record];
307 }
308 }
309
310 // --- DynTable -------------------------------------------------------------
311
312#if WOWLIB_DB_SCHEMA_EMBEDDED
314 DynTable::open(std::string_view table, ClientVersion version) {
315 return SchemaCatalog::embedded().lookup(table, version).transform([&](const TableSchema& schema) {
316 return fromSchema(schema, version);
317 });
318 }
319#endif
320
322 DynTable out{};
323 out._rows = detail::ColumnRows{schema.columns};
324 out._version = version;
325 out._name = schema.name;
326 out._diskName = schema.diskName;
327 out._wire();
328 return out;
329 }
330
332 : TableBase{other}, _rows{other._rows}, _version{other._version}, _name{other._name}, _diskName{other._diskName} {
333 _wire();
334 }
335
337 if (this == &other) return *this;
338 TableBase::operator=(other);
339 _rows = other._rows;
340 _version = other._version;
341 _name = other._name;
342 _diskName = other._diskName;
343 _wire();
344 return *this;
345 }
346
347 DynTable::DynTable(DynTable&& other) noexcept
348 : TableBase{std::move(other)},
349 _rows{std::move(other._rows)},
350 _version{other._version},
351 _name{other._name},
352 _diskName{other._diskName} {
353 _wire();
354 }
355
357 if (this == &other) return *this;
358 TableBase::operator=(std::move(other));
359 _rows = std::move(other._rows);
360 _version = other._version;
361 _name = other._name;
362 _diskName = other._diskName;
363 _wire();
364 return *this;
365 }
366
367 Result<Column> DynTable::columnInfo(std::size_t column) const {
368 const std::span<const Column> schema = _rows.schema();
369 if (column >= schema.size())
371 std::format("{}: column {} out of range ({} columns)", _name, column, schema.size()));
372 return schema[column];
373 }
374
376 const std::span<const Column> schema = _rows.schema();
377 // The canonical snake spelling first, then the verbatim WoWDBDefs alias —
378 // in two passes, so a (theoretical) DBD name colliding with another
379 // column's snake spelling cannot shadow the canonical match.
380 for (std::size_t c = 0; c < schema.size(); ++c)
381 if (schema[c].nameView() == name) return c;
382 for (std::size_t c = 0; c < schema.size(); ++c)
383 if (!schema[c].dbdNameView().empty() && schema[c].dbdNameView() == name) return c;
384 return makeError(ErrorCode::TableUnknown, std::format("{}: no column named '{}'", _name, name));
385 }
386
388 if (row >= _rows.size())
390 std::format("{}: row {} out of range ({} rows)", _name, row, _rows.size()));
391 _rows.eraseRow(row);
392 return {};
393 }
394
395 Result<std::size_t> DynTable::findById(std::uint32_t recordId) const {
396 const std::size_t at = _rows.findById(recordId);
397 if (at == _rows.size())
398 return makeError(ErrorCode::TableUnknown, std::format("{}: no row with id {}", _name, recordId));
399 return at;
400 }
401
402 Result<const detail::ColumnRows::Facts*> DynTable::_checkCell(std::size_t row,
403 std::size_t column,
404 std::size_t element,
405 ColumnType expected) const {
406 const std::span<const Column> schema = _rows.schema();
407 if (row >= _rows.size() || column >= schema.size())
409 std::format("{}: cell ({}, {}) out of range ({} rows, " "{} columns)", _name, row, column,
410 _rows.size(), schema.size()));
411 const Column& col = schema[column];
412 if (col.type != expected)
413 return makeError(ErrorCode::SchemaMismatch, std::format("{}.{}: the column holds {}, not {}", _name,
414 col.nameView(), enumName(col.type),
415 enumName(expected)));
416 const detail::ColumnRows::Facts& f = _rows.facts(column);
417 const std::size_t slots = f.podSlots != 0 ? f.podSlots : std::size_t{f.strSlots};
418 if (element >= slots)
420 std::format("{}.{}: element {} out of range ({} slots)", _name, col.nameView(), element,
421 slots));
422 return &f;
423 }
424
425 Result<std::int64_t> DynTable::getInt(std::size_t row, std::size_t column, std::size_t element) const {
426 return _checkCell(row, column, element, ColumnType::Int).transform([&](auto) {
427 return _rows.getInt(row, column, element);
428 });
429 }
430
431 Result<void> DynTable::setInt(std::size_t row, std::size_t column, std::int64_t value, std::size_t element) {
432 return _checkCell(row, column, element, ColumnType::Int).transform([&](auto) {
433 _rows.setInt(row, column, element, value);
434 });
435 }
436
437 Result<float> DynTable::getFloat(std::size_t row, std::size_t column, std::size_t element) const {
438 return _checkCell(row, column, element, ColumnType::Float).transform([&](auto) {
439 float out;
440 std::memcpy(&out, _rows.podBytes(column).data() + (row * _rows.facts(column).podSlots + element) * 4,
441 sizeof out);
442 return out;
443 });
444 }
445
446 Result<void> DynTable::setFloat(std::size_t row, std::size_t column, float value, std::size_t element) {
447 return _checkCell(row, column, element, ColumnType::Float).transform([&](auto) {
448 _rows.setFloat(row, column, element, value);
449 });
450 }
451
452 Result<std::string_view> DynTable::getString(std::size_t row, std::size_t column, std::size_t element) const {
453 const auto str = _checkCell(row, column, element, ColumnType::String);
454 if (str) return _rows.getString(row, column, element);
455 // A LocString locale slot reads through the same accessor.
456 return _checkCell(row, column, element, ColumnType::LocString).transform([&](auto) {
457 return _rows.getString(row, column, element);
458 });
459 }
460
461 Result<void> DynTable::setString(std::size_t row, std::size_t column, std::string_view value, std::size_t element) {
462 const auto str = _checkCell(row, column, element, ColumnType::String);
463 if (str) {
464 _rows.setString(row, column, element, value);
465 return {};
466 }
467 return _checkCell(row, column, element, ColumnType::LocString).transform([&](auto) {
468 _rows.setString(row, column, element, value);
469 });
470 }
471
472 Result<std::uint32_t> DynTable::locstringFlags(std::size_t row, std::size_t column) const {
473 return _checkCell(row, column, 0, ColumnType::LocString).transform([&](auto) {
474 return _rows.flagsSlot(row, column);
475 });
476 }
477
478 Result<void> DynTable::setLocstringFlags(std::size_t row, std::size_t column, std::uint32_t flags) {
479 return _checkCell(row, column, 0, ColumnType::LocString).transform([&](auto) {
480 _rows.flagsSlot(row, column) = flags;
481 });
482 }
483
484 Result<PodColumnView> DynTable::podColumn(std::size_t column) const {
485 const std::span<const Column> schema = _rows.schema();
486 if (column >= schema.size())
488 std::format("{}: column {} out of range ({} columns)", _name, column, schema.size()));
489 const Column& col = schema[column];
490 if (col.type != ColumnType::Int && col.type != ColumnType::Float)
492 std::format("{}.{}: only numeric columns have a " "zero-copy view", _name, col.nameView()));
493 const detail::ColumnRows::Facts& f = _rows.facts(column);
494 PodColumnView out{};
495 out.bytes = _rows.podBytes(column);
496 out.elemBytes = f.elemBytes;
497 out.elemsPerRow = f.podSlots;
498 out.isSigned = col.isSigned;
499 out.isFloat = col.type == ColumnType::Float;
500 return out;
501 }
502}
Result< void > setString(std::size_t row, std::size_t column, std::string_view value, std::size_t element=0)
Write a string element.
Result< Column > columnInfo(std::size_t column) const
Column column's schema entry, by value (the welded metadata face of schema).
static DynTable fromSchema(const TableSchema &schema, ClientVersion version)
Open a table over an explicitly resolved schema — the caller guarantees the schema's backing catalog ...
Result< std::string_view > getString(std::size_t row, std::size_t column, std::size_t element=0) const
Read a string element (String columns: the array element; LocString columns: the locale slot).
Result< void > setFloat(std::size_t row, std::size_t column, float value, std::size_t element=0)
Write a float element.
Result< PodColumnView > podColumn(std::size_t column) const
The zero-copy view of numeric/float column column.
Result< std::int64_t > getInt(std::size_t row, std::size_t column, std::size_t element=0) const
Read an integer element.
Result< std::uint32_t > locstringFlags(std::size_t row, std::size_t column) const
Read a LocString column's flags word.
std::string_view name() const
The table name (WoWDBDefs identifier).
Result< void > setLocstringFlags(std::size_t row, std::size_t column, std::uint32_t flags)
Write a LocString column's flags word.
DynTable()=default
An unwired table: every inherited operation reports InvalidEntityState until one is opened properly.
Result< void > eraseRow(std::size_t row)
Remove row row.
Result< void > setInt(std::size_t row, std::size_t column, std::int64_t value, std::size_t element=0)
Write an integer element (truncates to the column's exact width, as the typed member assignment did).
Result< float > getFloat(std::size_t row, std::size_t column, std::size_t element=0) const
Read a float element.
DynTable & operator=(const DynTable &other)
std::span< const Column > schema() const
ClientVersion version() const
The client version the table serializes for.
Result< std::size_t > columnIndex(std::string_view name) const
The index of column name.
Result< std::size_t > findById(std::uint32_t id) const
The first row whose $id$ equals id.
The welded supertype of every generated table class: the whole table surface — decode,...
The column-store row storage: one exact-width POD buffer per numeric column, one string vector per st...
Definition dyn_table.hpp:45
std::uint32_t getSlot(std::size_t record, std::size_t column, std::size_t element) const override
std::size_t size() const override
One override serves both interfaces (identical signatures).
std::size_t findById(std::uint32_t id) const override
The first record whose id equals id, or size() when none (copy source lookup).
void setInt(std::size_t record, std::size_t column, std::size_t element, std::int64_t value) override
std::int64_t getInt(std::size_t record, std::size_t column, std::size_t element) const override
std::string_view getString(std::size_t record, std::size_t column, std::size_t element) const override
std::uint32_t & flagsSlot(std::size_t record, std::size_t column)
The LocString flags of record in column.
void setFloat(std::size_t record, std::size_t column, std::size_t element, float value) override
const Facts & facts(std::size_t column) const
Definition dyn_table.hpp:93
void cloneWithId(std::size_t src, std::uint32_t newId) override
Clone record src into a fresh record and set its id column to newId (WDC copy-table materialization).
void reserve(std::size_t n) override
void setString(std::size_t record, std::size_t column, std::size_t element, std::string_view value) override
std::uint32_t idOf(std::size_t record) const override
std::size_t add() override
std::span< const Column > schema() const
std::string & stringSlot(std::size_t record, std::size_t column, std::size_t element)
Mutable string slot access (the public setter's target).
void eraseRow(std::size_t record)
Remove row record (all columns' slices).
DynTable — the generic, runtime-schema client-database table: the ONE table class every binding welds...
AccessKind
The storage kind of one record member, as the erased accessors see it: exact width and signedness for...
@ U64
Integer elements, by shape.
@ LocStr
A LocString<N>: strings at offset, flags on the side.
@ Str
A std::string element.
ColumnType
The logical value class of a column.
Definition schema.hpp:34
@ Int
An integer column; Column::bits / Column::isSigned give the shape.
Definition schema.hpp:35
@ Float
A 32-bit IEEE float column.
Definition schema.hpp:36
@ String
A string-block reference column (u32 offset on disk).
Definition schema.hpp:37
@ LocString
A pre-Cataclysm localized string column (see LocString).
Definition schema.hpp:38
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ TableUnknown
The schema catalog knows no table of the requested name.
Definition error.hpp:49
@ SchemaMismatch
A client-database record layout disagrees with the generated WoWDBDefs schema.
Definition error.hpp:46
@ OffsetOutOfBounds
An offset array (M2Array) points outside its base buffer.
Definition error.hpp:39
constexpr std::string_view enumName(E value)
The enumerator name of value, straight from reflection — no handwritten stringification tables anywhe...
Definition reflect.hpp:19
One column of a record schema, derived from a record member by reflection (typed records) or from the...
Definition schema.hpp:46
constexpr std::string_view nameView() const
The member spelling as a view.
Definition schema.hpp:79
std::uint8_t localeCount
LocString language slots (8 or 16); 0 otherwise.
Definition schema.hpp:62
bool isId
Whether this is the table's primary key ($id$).
Definition schema.hpp:64
std::uint16_t arrayLen
Element count; 1 for scalar columns.
Definition schema.hpp:60
ColumnType type
The logical value class.
Definition schema.hpp:54
std::uint8_t bits
Integer element width in bits; 32 for float/string refs.
Definition schema.hpp:56
bool isSigned
Integer signedness; false for non-Int columns.
Definition schema.hpp:58
A scalar column's whole storage, for zero-copy array views: the raw exact-width buffer plus the facts...
std::span< const std::byte > bytes
The buffer, row-major.
bool isFloat
float32 elements.
bool isSigned
Integer signedness.
std::uint16_t elemsPerRow
Array length (1 = scalar).
std::uint8_t elemBytes
element width in bytes.
One resolved (table, client version) schema: what SchemaCatalog::lookup hands the engine.
The per-column access facts, derived from the schema at bind.
Definition dyn_table.hpp:84
std::uint8_t elemBytes
POD element width; 0 for strings.
Definition dyn_table.hpp:86
AccessKind kind
The element storage kind.
Definition dyn_table.hpp:85
bool hasFlags
LocString: a flags word per row.
Definition dyn_table.hpp:89
std::uint16_t strSlots
String slots per row.
Definition dyn_table.hpp:88
std::uint16_t podSlots
POD elements per row (arrayLen).
Definition dyn_table.hpp:87