1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-15 20:15:24 +00:00

Merge branch 'master' into anim-data-encoder

This commit is contained in:
gucio321 2021-02-26 21:04:14 +01:00 committed by GitHub
commit ab14168f50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 273 additions and 162 deletions

View File

@ -80,24 +80,17 @@ type COF struct {
} }
// Unmarshal a byte slice to this COF // Unmarshal a byte slice to this COF
// nolint:funlen // no need to change
func (c *COF) Unmarshal(fileData []byte) error { func (c *COF) Unmarshal(fileData []byte) error {
streamReader := d2datautils.CreateStreamReader(fileData)
var b []byte
var err error var err error
b, err = streamReader.ReadBytes(numHeaderBytes) streamReader := d2datautils.CreateStreamReader(fileData)
headerBytes, err := streamReader.ReadBytes(numHeaderBytes)
if err != nil { if err != nil {
return err return err
} }
c.NumberOfLayers = int(b[headerNumLayers]) c.loadHeader(headerBytes)
c.FramesPerDirection = int(b[headerFramesPerDir])
c.NumberOfDirections = int(b[headerNumDirs])
c.unknownHeaderBytes = b[headerNumDirs+1 : headerSpeed]
c.Speed = int(b[headerSpeed])
c.unknownBodyBytes, err = streamReader.ReadBytes(numUnknownBodyBytes) c.unknownBodyBytes, err = streamReader.ReadBytes(numUnknownBodyBytes)
if err != nil { if err != nil {
@ -107,10 +100,44 @@ func (c *COF) Unmarshal(fileData []byte) error {
c.CofLayers = make([]CofLayer, c.NumberOfLayers) c.CofLayers = make([]CofLayer, c.NumberOfLayers)
c.CompositeLayers = make(map[d2enum.CompositeType]int) c.CompositeLayers = make(map[d2enum.CompositeType]int)
err = c.loadCOFLayers(streamReader)
if err != nil {
return err
}
animationFramesData, err := streamReader.ReadBytes(c.FramesPerDirection)
if err != nil {
return err
}
c.loadAnimationFrames(animationFramesData)
priorityLen := c.FramesPerDirection * c.NumberOfDirections * c.NumberOfLayers
c.Priority = make([][][]d2enum.CompositeType, c.NumberOfDirections)
priorityBytes, err := streamReader.ReadBytes(priorityLen)
if err != nil {
return err
}
c.loadPriority(priorityBytes)
return nil
}
func (c *COF) loadHeader(b []byte) {
c.NumberOfLayers = int(b[headerNumLayers])
c.FramesPerDirection = int(b[headerFramesPerDir])
c.NumberOfDirections = int(b[headerNumDirs])
c.unknownHeaderBytes = b[headerNumDirs+1 : headerSpeed]
c.Speed = int(b[headerSpeed])
}
func (c *COF) loadCOFLayers(streamReader *d2datautils.StreamReader) error {
for i := 0; i < c.NumberOfLayers; i++ { for i := 0; i < c.NumberOfLayers; i++ {
layer := CofLayer{} layer := CofLayer{}
b, err = streamReader.ReadBytes(numLayerBytes) b, err := streamReader.ReadBytes(numLayerBytes)
if err != nil { if err != nil {
return err return err
} }
@ -128,25 +155,18 @@ func (c *COF) Unmarshal(fileData []byte) error {
c.CompositeLayers[layer.Type] = i c.CompositeLayers[layer.Type] = i
} }
b, err = streamReader.ReadBytes(c.FramesPerDirection) return nil
if err != nil { }
return err
}
func (c *COF) loadAnimationFrames(b []byte) {
c.AnimationFrames = make([]d2enum.AnimationFrame, c.FramesPerDirection) c.AnimationFrames = make([]d2enum.AnimationFrame, c.FramesPerDirection)
for i := range b { for i := range b {
c.AnimationFrames[i] = d2enum.AnimationFrame(b[i]) c.AnimationFrames[i] = d2enum.AnimationFrame(b[i])
} }
}
priorityLen := c.FramesPerDirection * c.NumberOfDirections * c.NumberOfLayers func (c *COF) loadPriority(priorityBytes []byte) {
c.Priority = make([][][]d2enum.CompositeType, c.NumberOfDirections)
priorityBytes, err := streamReader.ReadBytes(priorityLen)
if err != nil {
return err
}
priorityIndex := 0 priorityIndex := 0
for direction := 0; direction < c.NumberOfDirections; direction++ { for direction := 0; direction < c.NumberOfDirections; direction++ {
@ -159,8 +179,6 @@ func (c *COF) Unmarshal(fileData []byte) error {
} }
} }
} }
return nil
} }
// Marshal this COF to a byte slice // Marshal this COF to a byte slice

View File

@ -0,0 +1,27 @@
package d2cof
// FPS returns FPS value basing on cof's speed
func (c *COF) FPS() float64 {
const (
baseFPS = 25
speedDivisor = 256
)
fps := baseFPS * (float64(c.Speed) / speedDivisor)
if fps == 0 {
fps = baseFPS
}
return fps
}
// Duration returns animation's duration
func (c *COF) Duration() float64 {
const (
milliseconds = 1000
)
frameDelay := milliseconds / c.FPS()
return float64(c.FramesPerDirection) * frameDelay
}

View File

@ -41,3 +41,15 @@ func Load(data []byte) (*PL2, error) {
return result, nil return result, nil
} }
// Marshal encodes PL2 back into byte slice
func (p *PL2) Marshal() []byte {
restruct.EnableExprBeta()
data, err := restruct.Pack(binary.LittleEndian, p)
if err != nil {
panic(err)
}
return data
}

View File

@ -0,0 +1,40 @@
package d2pl2
import (
"testing"
)
func exampleData() *PL2 {
result := &PL2{
BasePalette: PL2Palette{},
SelectedUintShift: PL2PaletteTransform{},
RedTones: PL2PaletteTransform{},
GreenTones: PL2PaletteTransform{},
BlueTones: PL2PaletteTransform{},
DarkendColorShift: PL2PaletteTransform{},
}
result.BasePalette.Colors[0].R = 8
result.DarkendColorShift.Indices[0] = 123
return result
}
func TestPL2_MarshalUnmarshal(t *testing.T) {
pl2 := exampleData()
data := pl2.Marshal()
newPL2, err := Load(data)
if err != nil {
t.Error(err)
}
if newPL2.BasePalette.Colors[0] != pl2.BasePalette.Colors[0] {
t.Fatal("unexpected length")
}
if pl2.DarkendColorShift.Indices[0] != newPL2.DarkendColorShift.Indices[0] {
t.Fatal("unexpected index set")
}
}

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),