mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-03 07:07:25 -05:00
261 lines
5.4 KiB
Go
261 lines
5.4 KiB
Go
package d2tbl
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
|
)
|
|
|
|
// TextDictionary is a string map
|
|
type TextDictionary map[string]string
|
|
|
|
func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, br *d2datautils.StreamReader) error {
|
|
for i := 0; i < len(hashEntries); i++ {
|
|
entry := textDictionaryHashEntry{}
|
|
|
|
active, err := br.ReadByte()
|
|
if err != nil {
|
|
return fmt.Errorf("reading active: %v", err)
|
|
}
|
|
|
|
entry.IsActive = active > 0
|
|
|
|
entry.Index, err = br.ReadUInt16()
|
|
if err != nil {
|
|
return fmt.Errorf("reading Index: %v", err)
|
|
}
|
|
|
|
entry.HashValue, err = br.ReadUInt32()
|
|
if err != nil {
|
|
return fmt.Errorf("reading hash value: %v", err)
|
|
}
|
|
|
|
entry.IndexString, err = br.ReadUInt32()
|
|
if err != nil {
|
|
return fmt.Errorf("reading index string pos: %v", err)
|
|
}
|
|
|
|
entry.NameString, err = br.ReadUInt32()
|
|
if err != nil {
|
|
return fmt.Errorf("reading name string pos: %v", err)
|
|
}
|
|
|
|
entry.NameLength, err = br.ReadUInt16()
|
|
if err != nil {
|
|
return fmt.Errorf("reading name length: %v", err)
|
|
}
|
|
|
|
hashEntries[i] = &entry
|
|
}
|
|
|
|
for idx := range hashEntries {
|
|
if !hashEntries[idx].IsActive {
|
|
continue
|
|
}
|
|
|
|
if err := td.loadHashEntry(idx, hashEntries[idx], br); err != nil {
|
|
return fmt.Errorf("loading entry %d: %v", idx, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEntry, br *d2datautils.StreamReader) error {
|
|
br.SetPosition(uint64(hashEntry.NameString))
|
|
|
|
nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1))
|
|
if err != nil {
|
|
return fmt.Errorf("reading name value: %v", err)
|
|
}
|
|
|
|
value := string(nameVal)
|
|
|
|
br.SetPosition(uint64(hashEntry.IndexString))
|
|
|
|
key := ""
|
|
|
|
for {
|
|
b, err := br.ReadByte()
|
|
if b == 0 {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("reading kay char: %v", err)
|
|
}
|
|
|
|
key += string(b)
|
|
}
|
|
|
|
if key == "x" || key == "X" {
|
|
key = "#" + strconv.Itoa(idx)
|
|
}
|
|
|
|
_, exists := td[key]
|
|
if !exists {
|
|
td[key] = value
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type textDictionaryHashEntry struct {
|
|
IsActive bool
|
|
Index uint16
|
|
HashValue uint32
|
|
IndexString uint32
|
|
NameString uint32
|
|
NameLength uint16
|
|
}
|
|
|
|
const (
|
|
crcByteCount = 2
|
|
)
|
|
|
|
// LoadTextDictionary loads the text dictionary from the given data
|
|
func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) {
|
|
lookupTable := make(TextDictionary)
|
|
|
|
br := d2datautils.CreateStreamReader(dictionaryData)
|
|
|
|
// skip past the CRC
|
|
_, _ = br.ReadBytes(crcByteCount)
|
|
|
|
var err error
|
|
|
|
/*
|
|
number of indicates
|
|
(https://d2mods.info/forum/viewtopic.php?p=202077#p202077)
|
|
Indices ...
|
|
An array of WORD. Each entry is an index into the hash table.
|
|
The actual string key index in the .bin file is an index into this table.
|
|
So to get a string from a key index ...
|
|
*/
|
|
numberOfElements, err := br.ReadUInt16()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading number of elements: %v", err)
|
|
}
|
|
|
|
hashTableSize, err := br.ReadUInt32()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading hash table size: %v", err)
|
|
}
|
|
|
|
// Version
|
|
_, err = br.ReadByte()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading version: %v", err)
|
|
}
|
|
|
|
_, _ = br.ReadUInt32() // StringOffset
|
|
|
|
// When the number of times you have missed a match with a
|
|
// hash key equals this value, you give up because it is not there.
|
|
_, _ = br.ReadUInt32()
|
|
|
|
_, _ = br.ReadUInt32() // FileSize
|
|
|
|
elementIndex := make([]uint16, numberOfElements)
|
|
for i := 0; i < int(numberOfElements); i++ {
|
|
elementIndex[i], err = br.ReadUInt16()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading element index %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
hashEntries := make([]*textDictionaryHashEntry, hashTableSize)
|
|
|
|
err = lookupTable.loadHashEntries(hashEntries, br)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading has entries: %v", err)
|
|
}
|
|
|
|
return lookupTable, nil
|
|
}
|
|
|
|
// Marshal encodes text dictionary back into byte slice
|
|
func (td *TextDictionary) Marshal() []byte {
|
|
sw := d2datautils.CreateStreamWriter()
|
|
|
|
// https://github.com/OpenDiablo2/OpenDiablo2/issues/1043
|
|
sw.PushBytes(0, 0)
|
|
|
|
sw.PushUint16(0)
|
|
|
|
keys := make([]string, 0)
|
|
for key := range *td {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
sw.PushUint32(uint32(len(keys)))
|
|
|
|
// version (always 0)
|
|
sw.PushBytes(0)
|
|
|
|
// offset of start of data (unnecessary for our decoder)
|
|
sw.PushUint32(0)
|
|
|
|
// Max retry count for a hash hit.
|
|
sw.PushUint32(0)
|
|
|
|
// offset to end of data (noop)
|
|
sw.PushUint32(0)
|
|
|
|
// indicates (len = 0, so nothing here)
|
|
|
|
// nolint:gomnd // 17 comes from the size of one "data-header index"
|
|
// dataPos is a position, when we're placing data stream
|
|
dataPos := len(sw.GetBytes()) + 17*len(*td)
|
|
|
|
for _, key := range keys {
|
|
value := (*td)[key]
|
|
// non-zero if record is used (for us, every record is used ;-)
|
|
sw.PushBytes(1)
|
|
|
|
// generally unused;
|
|
// string key index (used in .bin)
|
|
sw.PushUint16(0)
|
|
|
|
// also unused in our decoder
|
|
// calculated hash of the string.
|
|
sw.PushUint32(0)
|
|
|
|
sw.PushUint32(uint32(dataPos))
|
|
|
|
if key[0] == '#' {
|
|
// 1 for X, and 1 for separator
|
|
dataPos += 2
|
|
} else {
|
|
dataPos += len(key) + 1
|
|
}
|
|
|
|
sw.PushUint32(uint32(dataPos))
|
|
dataPos += len(value) + 1
|
|
|
|
sw.PushUint16(uint16(len(value) + 1))
|
|
}
|
|
|
|
// data stream: put all data in appropriate order
|
|
for _, key := range keys {
|
|
value := (*td)[key]
|
|
|
|
if key[0] == '#' {
|
|
key = "x"
|
|
}
|
|
|
|
sw.PushBytes([]byte(key)...)
|
|
|
|
// 0 as separator
|
|
sw.PushBytes(0)
|
|
|
|
sw.PushBytes([]byte(value)...)
|
|
|
|
// 0 as separator
|
|
sw.PushBytes(0)
|
|
}
|
|
|
|
return sw.GetBytes()
|
|
}
|