4ba9c42829
En 3.4.3 `item_template` ya no existe: los datos del juego viven en ficheros
DB2 del cliente, y aquí acore_world está vacío. Se extraen a `home_item_data`
(44873 ítems): nombre es/en, calidad, nivel de objeto, tipo de inventario y
displayId.
No hay ninguna librería en PyPI que lea WDC4 (wdbx, dbc-extract3, wow-db2 no
existen), así que el lector va incluido en sql/db2/:
- `wdc_db2.py`: WDC3/WDC4 con registros fijos (bitpacked, common y pallet).
- `itemsparse.py`: ItemSparse es una tabla *sparse*, que es otro mundo: los
locstring van inline y terminados en \0, así que desplazan todo lo que va
detrás y los offsets de field_storage_info no valen. Hay que recorrer los
campos en orden, y el fichero no dice cuáles son strings: eso sale de las
definiciones oficiales de WoWDBDefs (ItemSparse.dbd, incluido), casando el
layout_hash del propio fichero (D532973D = 3.4.3).
- `db2_to_sql.py`: genera sql/item_data.sql. Regenerable y multi-locale: basta
con dejar el ItemSparse.db2 de cada idioma en /root/<locale>/.
Verificado: los 3068 ítems de la tienda están cubiertos; 2949 nombres (96%)
coinciden literalmente con el catálogo antiguo y los 119 restantes son
revisiones de traducción de Blizzard, no fallos del parser ("Bordón de Luz" ->
"Batuta de Luz"). El ítem 45327 sale q4 / ilvl 238 / "Siren's Cry", idéntico a
lo que devuelve wowhead. La distribución de calidad es realista (53
legendarios, 23 artefactos).
Ojo: la tabla está cargada pero TODAVÍA no la usa ningún código de la web.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
4.6 KiB
Python
109 lines
4.6 KiB
Python
"""Extrae ItemSparse.db2 (WDC4, tabla sparse) usando las definiciones oficiales
|
|
de WoWDBDefs para el layout_hash del propio fichero.
|
|
|
|
Por qué hace falta el DBD: en una tabla sparse los locstring van INLINE y
|
|
terminados en \0, así que desplazan todo lo que va detrás. Los offsets de
|
|
field_storage_info asumen 4 bytes por string y NO valen; hay que recorrer los
|
|
campos en orden. El fichero no dice qué campos son strings: eso está en el DBD.
|
|
"""
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
|
|
def parse_dbd(path, layout_hash):
|
|
txt = open(path, encoding='utf-8').read()
|
|
# Tipos por columna (COLUMNS ... hasta línea en blanco)
|
|
cols = {}
|
|
block = txt.split('COLUMNS', 1)[1].split('\n\n', 1)[0]
|
|
for line in block.strip().split('\n'):
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
cols[parts[1].split('<')[0].rstrip('?')] = parts[0].split('<')[0]
|
|
|
|
# Bloque del layout pedido
|
|
blocks = txt.split('\n\n')
|
|
for b in blocks:
|
|
if f'LAYOUT {layout_hash}' in b:
|
|
fields = []
|
|
for line in b.strip().split('\n'):
|
|
if line.startswith(('LAYOUT', 'BUILD', 'COMMENT')):
|
|
continue
|
|
m = re.match(r'^(\$[^$]*\$)?([A-Za-z_0-9]+)(<[^>]*>)?(\[(\d+)\])?', line)
|
|
if not m:
|
|
continue
|
|
ann, name, size, _, arr = m.groups()
|
|
fields.append(dict(name=name, noninline='noninline' in (ann or ''),
|
|
array=int(arr) if arr else 1,
|
|
type=cols.get(name, 'int')))
|
|
return fields
|
|
raise SystemExit(f'layout {layout_hash} no encontrado')
|
|
|
|
|
|
class ItemSparse:
|
|
def __init__(self, db2_path, dbd_path):
|
|
d = self.d = open(db2_path, 'rb').read()
|
|
hdr = struct.unpack_from('<IIIIIIIIIHHIIIIIII', d, 4)
|
|
(self.record_count, self.field_count, self.record_size, _sts, _th, layout_hash,
|
|
self.min_id, self.max_id, _loc, self.flags, self.id_index, _tfc, _bdo,
|
|
_lcc, fsi_size, common_size, pallet_size, self.section_count) = hdr
|
|
|
|
self.fields = [f for f in parse_dbd(dbd_path, '%08X' % layout_hash) if not f['noninline']]
|
|
if len(self.fields) != self.field_count:
|
|
print(f'aviso: DBD tiene {len(self.fields)} campos y el fichero {self.field_count}',
|
|
file=sys.stderr)
|
|
|
|
o = 4 + struct.calcsize('<IIIIIIIIIHHIIIIIII')
|
|
self.sections = []
|
|
for _ in range(self.section_count):
|
|
(_tact, fo, rc, sts, ore, ils, rds, omic, ctc) = struct.unpack_from('<QIIIIIIII', d, o)
|
|
o += 40
|
|
self.sections.append(dict(file_offset=fo, record_count=rc, string_table_size=sts,
|
|
offset_records_end=ore, id_list_size=ils,
|
|
relationship_data_size=rds, offset_map_id_count=omic,
|
|
copy_table_count=ctc))
|
|
o += self.field_count * 4 # field_structure: no hace falta
|
|
self.storage = []
|
|
for _ in range(self.field_count):
|
|
(ob, sb, extra, t, c1, c2, c3) = struct.unpack_from('<HHIIIII', d, o); o += 24
|
|
self.storage.append(dict(size_bits=sb, type=t))
|
|
|
|
self.rows = []
|
|
for s in self.sections:
|
|
self._read(s)
|
|
|
|
def _read(self, s):
|
|
d = self.d
|
|
# Orden en disco: datos variables, id_list, copy_table, offset_map
|
|
p = s['offset_records_end']
|
|
ids = list(struct.unpack_from(f'<{s["id_list_size"] // 4}I', d, p))
|
|
p += s['id_list_size']
|
|
p += s['copy_table_count'] * 8
|
|
omap = [struct.unpack_from('<IH', d, p + i * 6) for i in range(s['offset_map_id_count'])]
|
|
|
|
for i, (off, size) in enumerate(omap):
|
|
if not size:
|
|
continue
|
|
self.rows.append((ids[i], self._record(d[off:off + size])))
|
|
|
|
def _record(self, rec):
|
|
out = {}
|
|
pos = 0 # en bytes: los campos de una sparse van alineados a byte
|
|
for fi, f in enumerate(self.fields):
|
|
if f['type'] == 'locstring':
|
|
end = rec.find(b'\x00', pos)
|
|
out[f['name']] = rec[pos:end].decode('utf-8', 'replace')
|
|
pos = end + 1
|
|
continue
|
|
# OJO: size_bits ya es el TOTAL del campo, arrays incluidos
|
|
# (StatPercentageOfSocket[10] -> 320 bits), no el de un elemento.
|
|
nbytes = self.storage[fi]['size_bits'] // 8
|
|
if not nbytes:
|
|
continue
|
|
if f['array'] == 1:
|
|
out[f['name']] = int.from_bytes(rec[pos:pos + nbytes], 'little')
|
|
pos += nbytes
|
|
out['__consumed'] = pos
|
|
out['__len'] = len(rec)
|
|
return out
|