diff --git a/d2common/d2datautils/stream_writer.go b/d2common/d2datautils/stream_writer.go index f2c384e8..98de3746 100644 --- a/d2common/d2datautils/stream_writer.go +++ b/d2common/d2datautils/stream_writer.go @@ -52,7 +52,6 @@ func (v *StreamWriter) PushBit(b bool) { } // PushBits pushes bits (with max range 8) -//func (v *StreamWriter) PushBits(b byte, bits int) { func (v *StreamWriter) PushBits(b byte, bits int) { if bits > bitsPerByte { log.Print("input bits number must be less (or equal) then 8") diff --git a/d2common/d2fileformats/d2tbl/text_dictionary.go b/d2common/d2fileformats/d2tbl/text_dictionary.go index b744bfc9..8aea6139 100644 --- a/d2common/d2fileformats/d2tbl/text_dictionary.go +++ b/d2common/d2fileformats/d2tbl/text_dictionary.go @@ -2,25 +2,36 @@ package d2tbl import ( "errors" + "sort" "strconv" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" ) // TextDictionary is a string map -type TextDictionary map[string]string +type TextDictionary struct { + crcBytes []byte + elementIndex []uint16 + hashTableSize uint32 + version byte + stringOffset uint32 + unknown1 uint32 + fileSize uint32 + hashEntries []*textDictionaryHashEntry + Entries map[string]string +} -func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, br *d2datautils.StreamReader) error { - for i := 0; i < len(hashEntries); i++ { +func (td *TextDictionary) loadHashEntries(br *d2datautils.StreamReader) error { + var err error + + for i := 0; i < len(td.hashEntries); i++ { entry := textDictionaryHashEntry{} - active, err := br.ReadByte() + entry.active, err = br.ReadByte() if err != nil { return err } - entry.IsActive = active > 0 - entry.Index, err = br.ReadUInt16() if err != nil { return err @@ -46,15 +57,15 @@ func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, return err } - hashEntries[i] = &entry + td.hashEntries[i] = &entry } - for idx := range hashEntries { - if !hashEntries[idx].IsActive { + for idx := range td.hashEntries { + if !td.hashEntries[idx].IsActive() { continue } - if err := td.loadHashEntry(idx, hashEntries[idx], br); err != nil { + if err := td.loadHashEntry(idx, td.hashEntries[idx], br); err != nil { return err } } @@ -62,7 +73,9 @@ func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, return nil } -func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEntry, br *d2datautils.StreamReader) error { +func (td *TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEntry, br *d2datautils.StreamReader) error { + var err error + br.SetPosition(uint64(hashEntry.NameString)) nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1)) @@ -70,7 +83,7 @@ func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEnt return err } - value := string(nameVal) + hashEntry.name = string(nameVal) br.SetPosition(uint64(hashEntry.IndexString)) @@ -89,25 +102,33 @@ func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEnt key += string(b) } - if key == "x" || key == "X" { + hashEntry.key = key + + if hashEntry.key == "x" || hashEntry.key == "X" { key = "#" + strconv.Itoa(idx) } - _, exists := td[key] + _, exists := td.Entries[key] if !exists { - td[key] = value + td.Entries[key] = hashEntry.name } return nil } type textDictionaryHashEntry struct { - IsActive bool - Index uint16 + name string + key string HashValue uint32 IndexString uint32 NameString uint32 + Index uint16 NameLength uint16 + active byte +} + +func (t *textDictionaryHashEntry) IsActive() bool { + return t.active > 0 } const ( @@ -115,38 +136,54 @@ const ( ) // LoadTextDictionary loads the text dictionary from the given data -func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) { - lookupTable := make(TextDictionary) +func LoadTextDictionary(dictionaryData []byte) (*TextDictionary, error) { + var err error + + lookupTable := &TextDictionary{ + Entries: make(map[string]string), + } br := d2datautils.CreateStreamReader(dictionaryData) // skip past the CRC - _, _ = br.ReadBytes(crcByteCount) - - var err error + lookupTable.crcBytes, err = br.ReadBytes(crcByteCount) + if err != nil { + return nil, err + } numberOfElements, err := br.ReadUInt16() if err != nil { return nil, err } - hashTableSize, err := br.ReadUInt32() + lookupTable.hashTableSize, err = br.ReadUInt32() if err != nil { return nil, err } // Version (always 0) - if _, err = br.ReadByte(); err != nil { + if lookupTable.version, err = br.ReadByte(); err != nil { return nil, errors.New("error reading Version record") } - _, _ = br.ReadUInt32() // StringOffset + // StringOffset + lookupTable.stringOffset, err = br.ReadUInt32() + if err != nil { + return nil, errors.New("error reading string offset") + } // 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() + lookupTable.unknown1, err = br.ReadUInt32() + if err != nil { + return nil, err + } - _, _ = br.ReadUInt32() // FileSize + // FileSize + lookupTable.fileSize, err = br.ReadUInt32() + if err != nil { + return nil, err + } elementIndex := make([]uint16, numberOfElements) for i := 0; i < int(numberOfElements); i++ { @@ -156,12 +193,78 @@ func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) { } } - hashEntries := make([]*textDictionaryHashEntry, hashTableSize) + lookupTable.elementIndex = elementIndex - err = lookupTable.loadHashEntries(hashEntries, br) + lookupTable.hashEntries = make([]*textDictionaryHashEntry, lookupTable.hashTableSize) + + err = lookupTable.loadHashEntries(br) if err != nil { return nil, err } return lookupTable, nil } + +// Marshal encodes text dictionary back to byte slice +func (td *TextDictionary) Marshal() []byte { + // create stream writter + sw := d2datautils.CreateStreamWriter() + + sw.PushBytes(td.crcBytes...) + sw.PushUint16(uint16(len(td.elementIndex))) + sw.PushUint32(td.hashTableSize) + sw.PushBytes(td.version) + sw.PushUint32(td.stringOffset) + sw.PushUint32(td.unknown1) + + sw.PushUint32(td.fileSize) + + for _, i := range td.elementIndex { + sw.PushUint16(i) + } + + for i := 0; i < len(td.hashEntries); i++ { + sw.PushBytes(td.hashEntries[i].active) + sw.PushUint16(td.hashEntries[i].Index) + sw.PushUint32(td.hashEntries[i].HashValue) + sw.PushUint32(td.hashEntries[i].IndexString) + sw.PushUint32(td.hashEntries[i].NameString) + sw.PushUint16(td.hashEntries[i].NameLength) + } + + // values are table entries data (key & values) + var values map[int]string = make(map[int]string) + // valuesSorted are sorted values + var valuesSorted map[int]string = make(map[int]string) + + // add values key / names to map + for _, i := range td.hashEntries { + values[int(i.IndexString)] = i.key + values[int(i.NameString)] = i.name + } + + // added map keys + keys := make([]int, 0, len(values)) + for k := range values { + keys = append(keys, k) + } + + // sort keys + sort.Ints(keys) + + // create sorted values map + for _, k := range keys { + valuesSorted[k] = values[k] + } + + // add first value (without 0-byte separator) + sw.PushBytes([]byte(valuesSorted[keys[0]])...) + + // adds values to result + for i := 1; i < len(valuesSorted); i++ { + sw.PushBytes([]byte(valuesSorted[keys[i]])...) + sw.PushBytes(0) + } + + return sw.GetBytes() +} diff --git a/d2core/d2asset/asset_manager.go b/d2core/d2asset/asset_manager.go index 99678238..59444de8 100644 --- a/d2core/d2asset/asset_manager.go +++ b/d2core/d2asset/asset_manager.go @@ -64,7 +64,8 @@ const ( type AssetManager struct { *d2util.Logger *d2loader.Loader - tables []d2tbl.TextDictionary + + tables []*d2tbl.TextDictionary dt1s d2interface.Cache ds1s d2interface.Cache cofs d2interface.Cache @@ -270,7 +271,7 @@ func (am *AssetManager) LoadPalette(palettePath string) (d2interface.Palette, er } // LoadStringTable loads a string table from the given path -func (am *AssetManager) LoadStringTable(tablePath string) (d2tbl.TextDictionary, error) { +func (am *AssetManager) LoadStringTable(tablePath string) (*d2tbl.TextDictionary, error) { data, err := am.LoadFile(tablePath) if err != nil { return nil, err @@ -304,7 +305,7 @@ func (am *AssetManager) TranslateString(input interface{}) string { } for idx := range am.tables { - if value, found := am.tables[idx][key]; found { + if value, found := am.tables[idx].Entries[key]; found { return value } } diff --git a/d2core/d2asset/d2asset.go b/d2core/d2asset/d2asset.go index ed5260c4..768a2ac0 100644 --- a/d2core/d2asset/d2asset.go +++ b/d2core/d2asset/d2asset.go @@ -27,7 +27,7 @@ func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) { manager := &AssetManager{ Logger: logger, Loader: loader, - tables: make([]d2tbl.TextDictionary, 0), + tables: make([]*d2tbl.TextDictionary, 0), animations: d2cache.CreateCache(animationBudget), fonts: d2cache.CreateCache(fontBudget), palettes: d2cache.CreateCache(paletteBudget),