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() }