OpenDiablo2/d2common/d2fileformats/d2animdata/animdata.go

155 lines
3.5 KiB
Go

package d2animdata
import (
"errors"
"fmt"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
)
const (
numBlocks = 256
maxRecordsPerBlock = 67
byteCountName = 8
byteCountSpeedPadding = 2
numEvents = 144
speedDivisor = 256
speedBaseFPS = 25
milliseconds = 1000
)
// AnimationData is a representation of the binary data from `data/global/AnimData.d2`
type AnimationData struct {
hashTable
blocks [numBlocks]*block
entries map[string][]*AnimationDataRecord
}
// GetRecordNames returns a slice of all record name strings
func (ad *AnimationData) GetRecordNames() []string {
result := make([]string, 0)
for name := range ad.entries {
result = append(result, name)
}
return result
}
// GetRecord returns a single AnimationDataRecord with the given name string. If there is more
// than one record with the given name string, the last record entry will be returned.
func (ad *AnimationData) GetRecord(name string) *AnimationDataRecord {
records := ad.GetRecords(name)
numRecords := len(records)
if numRecords < 1 {
return nil
}
return records[numRecords-1]
}
// GetRecords returns all records that have the given name string. The AnimData.d2 files have
// multiple records with the same name, but other values in the record are different.
func (ad *AnimationData) GetRecords(name string) []*AnimationDataRecord {
return ad.entries[name]
}
func (ad *AnimationData) GetRecordsCount() int {
return len(ad.entries)
}
// Load loads the data into an AnimationData struct
//nolint:gocognit,funlen // can't reduce
func Load(data []byte) (*AnimationData, error) {
reader := d2datautils.CreateStreamReader(data)
animdata := &AnimationData{}
hashIdx := 0
animdata.entries = make(map[string][]*AnimationDataRecord)
for blockIdx := range animdata.blocks {
recordCount, err := reader.ReadUInt32()
if err != nil {
return nil, err
}
if recordCount > maxRecordsPerBlock {
return nil, fmt.Errorf("more than %d records in block", maxRecordsPerBlock)
}
records := make([]*AnimationDataRecord, recordCount)
for recordIdx := uint32(0); recordIdx < recordCount; recordIdx++ {
nameBytes, err := reader.ReadBytes(byteCountName)
if err != nil {
return nil, err
}
if nameBytes[byteCountName-1] != byte(0) {
return nil, errors.New("animdata AnimationDataRecord name missing null terminator byte")
}
name := string(nameBytes)
name = strings.ReplaceAll(name, string(byte(0)), "")
animdata.hashTable[hashIdx] = hashName(name)
frames, err := reader.ReadUInt32()
if err != nil {
return nil, err
}
speed, err := reader.ReadUInt16()
if err != nil {
return nil, err
}
reader.SkipBytes(byteCountSpeedPadding)
events := make(map[int]AnimationEvent)
for eventIdx := 0; eventIdx < numEvents; eventIdx++ {
eventByte, err := reader.ReadByte()
if err != nil {
return nil, err
}
event := AnimationEvent(eventByte)
if event != AnimationEventNone {
events[eventIdx] = event
}
}
r := &AnimationDataRecord{
name,
frames,
speed,
events,
}
records[recordIdx] = r
if _, found := animdata.entries[r.name]; !found {
animdata.entries[r.name] = make([]*AnimationDataRecord, 0)
}
animdata.entries[r.name] = append(animdata.entries[r.name], r)
}
b := &block{
recordCount,
records,
}
animdata.blocks[blockIdx] = b
}
if reader.Position() != uint64(len(data)) {
return nil, errors.New("unable to parse animation data")
}
return animdata, nil
}