Merge pull request #1075 from gucio321/data-encoding2

general refactor of text dictionary encoder
This commit is contained in:
gravestench 2021-02-26 11:58:34 -08:00 committed by GitHub
commit a96170561f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 136 deletions

View File

@ -1,89 +1,77 @@
package d2tbl package d2tbl
import ( import (
"errors" "fmt"
"sort"
"strconv" "strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
) )
// TextDictionary is a string map // TextDictionary is a string map
type TextDictionary struct { type TextDictionary map[string]string
crcBytes []byte
elementIndex []uint16
hashTableSize uint32
version byte
stringOffset uint32
unknown1 uint32
fileSize uint32
hashEntries []*textDictionaryHashEntry
Entries map[string]string
}
func (td *TextDictionary) loadHashEntries(br *d2datautils.StreamReader) error { func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, br *d2datautils.StreamReader) error {
var err error for i := 0; i < len(hashEntries); i++ {
for i := 0; i < len(td.hashEntries); i++ {
entry := textDictionaryHashEntry{} entry := textDictionaryHashEntry{}
entry.active, err = br.ReadByte() active, err := br.ReadByte()
if err != nil { if err != nil {
return err return fmt.Errorf("reading active: %v", err)
} }
entry.IsActive = active > 0
entry.Index, err = br.ReadUInt16() entry.Index, err = br.ReadUInt16()
if err != nil { if err != nil {
return err return fmt.Errorf("reading Index: %v", err)
} }
entry.HashValue, err = br.ReadUInt32() entry.HashValue, err = br.ReadUInt32()
if err != nil { if err != nil {
return err return fmt.Errorf("reading hash value: %v", err)
} }
entry.IndexString, err = br.ReadUInt32() entry.IndexString, err = br.ReadUInt32()
if err != nil { if err != nil {
return err return fmt.Errorf("reading index string pos: %v", err)
} }
entry.NameString, err = br.ReadUInt32() entry.NameString, err = br.ReadUInt32()
if err != nil { if err != nil {
return err return fmt.Errorf("reading name string pos: %v", err)
} }
entry.NameLength, err = br.ReadUInt16() entry.NameLength, err = br.ReadUInt16()
if err != nil { if err != nil {
return err return fmt.Errorf("reading name length: %v", err)
} }
td.hashEntries[i] = &entry hashEntries[i] = &entry
} }
for idx := range td.hashEntries { for idx := range hashEntries {
if !td.hashEntries[idx].IsActive() { if !hashEntries[idx].IsActive {
continue continue
} }
if err := td.loadHashEntry(idx, td.hashEntries[idx], br); err != nil { if err := td.loadHashEntry(idx, hashEntries[idx], br); err != nil {
return err return fmt.Errorf("loading entry %d: %v", idx, err)
} }
} }
return nil 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)) br.SetPosition(uint64(hashEntry.NameString))
nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1)) nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1))
if err != nil { if err != nil {
return err return fmt.Errorf("reading name value: %v", err)
} }
hashEntry.name = string(nameVal) value := string(nameVal)
br.SetPosition(uint64(hashEntry.IndexString)) br.SetPosition(uint64(hashEntry.IndexString))
@ -96,39 +84,31 @@ func (td *TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEn
} }
if err != nil { if err != nil {
return err return fmt.Errorf("reading kay char: %v", err)
} }
key += string(b) key += string(b)
} }
hashEntry.key = key if key == "x" || key == "X" {
if hashEntry.key == "x" || hashEntry.key == "X" {
key = "#" + strconv.Itoa(idx) key = "#" + strconv.Itoa(idx)
} }
_, exists := td.Entries[key] _, exists := td[key]
if !exists { if !exists {
td.Entries[key] = hashEntry.name td[key] = value
} }
return nil return nil
} }
type textDictionaryHashEntry struct { type textDictionaryHashEntry struct {
name string IsActive bool
key string Index uint16
HashValue uint32 HashValue uint32
IndexString uint32 IndexString uint32
NameString uint32 NameString uint32
Index uint16
NameLength uint16 NameLength uint16
active byte
}
func (t *textDictionaryHashEntry) IsActive() bool {
return t.active > 0
} }
const ( const (
@ -136,133 +116,130 @@ const (
) )
// LoadTextDictionary loads the text dictionary from the given data // LoadTextDictionary loads the text dictionary from the given data
func LoadTextDictionary(dictionaryData []byte) (*TextDictionary, error) { func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) {
var err error lookupTable := make(TextDictionary)
lookupTable := &TextDictionary{
Entries: make(map[string]string),
}
br := d2datautils.CreateStreamReader(dictionaryData) br := d2datautils.CreateStreamReader(dictionaryData)
// skip past the CRC // skip past the CRC
lookupTable.crcBytes, err = br.ReadBytes(crcByteCount) _, _ = br.ReadBytes(crcByteCount)
if err != nil {
return nil, err
}
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() numberOfElements, err := br.ReadUInt16()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("reading number of elements: %v", err)
} }
lookupTable.hashTableSize, err = br.ReadUInt32() hashTableSize, err := br.ReadUInt32()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("reading hash table size: %v", err)
} }
// Version (always 0) // Version
if lookupTable.version, err = br.ReadByte(); err != nil { _, err = br.ReadByte()
return nil, errors.New("error reading Version record")
}
// StringOffset
lookupTable.stringOffset, err = br.ReadUInt32()
if err != nil { if err != nil {
return nil, errors.New("error reading string offset") return nil, fmt.Errorf("reading version: %v", err)
} }
_, _ = br.ReadUInt32() // StringOffset
// When the number of times you have missed a match with a // 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. // hash key equals this value, you give up because it is not there.
lookupTable.unknown1, err = br.ReadUInt32() _, _ = br.ReadUInt32()
if err != nil {
return nil, err
}
// FileSize _, _ = br.ReadUInt32() // FileSize
lookupTable.fileSize, err = br.ReadUInt32()
if err != nil {
return nil, err
}
elementIndex := make([]uint16, numberOfElements) elementIndex := make([]uint16, numberOfElements)
for i := 0; i < int(numberOfElements); i++ { for i := 0; i < int(numberOfElements); i++ {
elementIndex[i], err = br.ReadUInt16() elementIndex[i], err = br.ReadUInt16()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("reading element index %d: %v", i, err)
} }
} }
lookupTable.elementIndex = elementIndex hashEntries := make([]*textDictionaryHashEntry, hashTableSize)
lookupTable.hashEntries = make([]*textDictionaryHashEntry, lookupTable.hashTableSize) err = lookupTable.loadHashEntries(hashEntries, br)
err = lookupTable.loadHashEntries(br)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("loading has entries: %v", err)
} }
return lookupTable, nil return lookupTable, nil
} }
// Marshal encodes text dictionary back to byte slice // Marshal encodes text dictionary back into byte slice
func (td *TextDictionary) Marshal() []byte { func (td *TextDictionary) Marshal() []byte {
// create stream writter
sw := d2datautils.CreateStreamWriter() sw := d2datautils.CreateStreamWriter()
sw.PushBytes(td.crcBytes...) // https://github.com/OpenDiablo2/OpenDiablo2/issues/1043
sw.PushUint16(uint16(len(td.elementIndex))) sw.PushBytes(0, 0)
sw.PushUint32(td.hashTableSize)
sw.PushBytes(td.version)
sw.PushUint32(td.stringOffset)
sw.PushUint32(td.unknown1)
sw.PushUint32(td.fileSize) sw.PushUint16(0)
for _, i := range td.elementIndex { sw.PushInt32(int32(len(*td)))
sw.PushUint16(i)
// 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, value := range *td {
// 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))
dataPos += len(key) + 1
sw.PushUint32(uint32(dataPos))
dataPos += len(value) + 1
sw.PushUint16(uint16(len(value) + 1))
} }
for i := 0; i < len(td.hashEntries); i++ { // data stream: put all data in appropriate order
sw.PushBytes(td.hashEntries[i].active) for key, value := range *td {
sw.PushUint16(td.hashEntries[i].Index) for _, i := range key {
sw.PushUint32(td.hashEntries[i].HashValue) sw.PushBytes(byte(i))
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) // 0 as separator
var values map[int]string = make(map[int]string) sw.PushBytes(0)
// valuesSorted are sorted values
var valuesSorted map[int]string = make(map[int]string)
// add values key / names to map for _, i := range value {
for _, i := range td.hashEntries { sw.PushBytes(byte(i))
values[int(i.IndexString)] = i.key }
values[int(i.NameString)] = i.name
}
// added map keys // 0 as separator
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) sw.PushBytes(0)
} }

View File

@ -0,0 +1,37 @@
package d2tbl
import (
"testing"
)
func exampleData() *TextDictionary {
result := &TextDictionary{
"abc": "def",
"someStr": "Some long string",
"teststring": "TeSt",
}
return result
}
func TestTBL_Marshal(t *testing.T) {
tbl := exampleData()
data := tbl.Marshal()
newTbl, err := LoadTextDictionary(data)
if err != nil {
t.Error(err)
}
for key, value := range *tbl {
newValue, ok := newTbl[key]
if !ok {
t.Fatal("string wasn't encoded to table")
}
if newValue != value {
t.Fatal("unexpected value set")
}
}
}

View File

@ -65,7 +65,7 @@ type AssetManager struct {
*d2util.Logger *d2util.Logger
*d2loader.Loader *d2loader.Loader
tables []*d2tbl.TextDictionary tables []d2tbl.TextDictionary
dt1s d2interface.Cache dt1s d2interface.Cache
ds1s d2interface.Cache ds1s d2interface.Cache
cofs d2interface.Cache cofs d2interface.Cache
@ -268,7 +268,7 @@ func (am *AssetManager) LoadPalette(palettePath string) (d2interface.Palette, er
} }
// LoadStringTable loads a string table from the given path // 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) data, err := am.LoadFile(tablePath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -302,7 +302,7 @@ func (am *AssetManager) TranslateString(input interface{}) string {
} }
for idx := range am.tables { for idx := range am.tables {
if value, found := am.tables[idx].Entries[key]; found { if value, found := am.tables[idx][key]; found {
return value return value
} }
} }

View File

@ -27,7 +27,7 @@ func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) {
manager := &AssetManager{ manager := &AssetManager{
Logger: logger, Logger: logger,
Loader: loader, Loader: loader,
tables: make([]*d2tbl.TextDictionary, 0), tables: make([]d2tbl.TextDictionary, 0),
animations: d2cache.CreateCache(animationBudget), animations: d2cache.CreateCache(animationBudget),
fonts: d2cache.CreateCache(fontBudget), fonts: d2cache.CreateCache(fontBudget),
palettes: d2cache.CreateCache(paletteBudget), palettes: d2cache.CreateCache(paletteBudget),