adding animdata loader (#718)

* adding animdata loader

* utility methods, more tests, export record struct

- added methods for fps and frame duration calculation to the AnimationDataRecord
- exported AnimationDataRecord
- split the various structs into their own files
- added getter methods for retrieving records by name
- added tests for the new utility methods
This commit is contained in:
lord 2020-09-08 12:59:38 -07:00 committed by GitHub
parent 0218cad717
commit 65cce60eab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 369 additions and 0 deletions

View File

@ -0,0 +1,130 @@
package d2animdata
import (
"errors"
"fmt"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
)
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]
}
// Load loads the data into an AnimationData struct
func Load(data []byte) (*AnimationData, error) {
reader := d2common.CreateStreamReader(data)
animdata := &AnimationData{}
hashIdx := 0
animdata.entries = make(map[string][]*AnimationDataRecord)
for blockIdx := range animdata.blocks {
recordCount := reader.GetUInt32()
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 := reader.ReadBytes(byteCountName)
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 := reader.GetUInt32()
speed := reader.GetUInt16()
reader.SkipBytes(byteCountSpeedPadding)
events := make(map[int]AnimationEvent)
for eventIdx := 0; eventIdx < numEvents; eventIdx++ {
event := AnimationEvent(reader.GetByte())
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.GetPosition() != uint64(len(data)) {
return nil, errors.New("unable to parse animation data")
}
return animdata, nil
}

View File

@ -0,0 +1,147 @@
package d2animdata
import (
"os"
"testing"
)
func TestLoad(t *testing.T) {
testFile, fileErr := os.Open("testdata/AnimData.d2")
if fileErr != nil {
t.Error("cannot open test data file")
return
}
data := make([]byte, 0)
buf := make([]byte, 16)
for {
numRead, err := testFile.Read(buf)
data = append(data, buf[:numRead]...)
if err != nil {
break
}
}
_, loadErr := Load(data)
if loadErr != nil {
t.Error(loadErr)
}
_ = testFile.Close()
}
func TestLoad_BadData(t *testing.T) {
testFile, fileErr := os.Open("testdata/BadData.d2")
if fileErr != nil {
t.Error("cannot open test data file")
return
}
data := make([]byte, 0)
buf := make([]byte, 16)
for {
numRead, err := testFile.Read(buf)
data = append(data, buf[:numRead]...)
if err != nil {
break
}
}
_, loadErr := Load(data)
if loadErr == nil {
t.Error("bad data file should not be parsed")
}
_ = testFile.Close()
}
func TestAnimationData_GetRecordNames(t *testing.T) {
animdata := &AnimationData{
hashTable: hashTable{},
blocks: [256]*block{},
entries: map[string][]*AnimationDataRecord{
"a": {{}},
"b": {{}},
"c": {{}},
},
}
names := animdata.GetRecordNames()
if len(names) != 3 {
t.Error("record name count mismatch")
}
}
func TestAnimationData_GetRecords(t *testing.T) {
animdata := &AnimationData{
hashTable: hashTable{},
blocks: [256]*block{},
entries: map[string][]*AnimationDataRecord{
"a": {
{name: "a", speed: 1, framesPerDirection: 1},
{name: "a", speed: 2, framesPerDirection: 2},
{name: "a", speed: 3, framesPerDirection: 3},
},
},
}
if len(animdata.GetRecords("a")) != 3 {
t.Error("record count is incorrect")
}
if len(animdata.GetRecords("b")) > 0 {
t.Error("retrieved records for unknown record name")
}
}
func TestAnimationData_GetRecord(t *testing.T) {
animdata := &AnimationData{
hashTable: hashTable{},
blocks: [256]*block{},
entries: map[string][]*AnimationDataRecord{
"a": {
{name: "a", speed: 1, framesPerDirection: 1},
{name: "a", speed: 2, framesPerDirection: 2},
{name: "a", speed: 3, framesPerDirection: 3},
},
},
}
record := animdata.GetRecord("a")
if record.speed != 3 {
t.Error("record returned is incorrect")
}
}
func TestAnimationDataRecord_FPS(t *testing.T) {
record := &AnimationDataRecord{}
var fps float64
record.speed = 256
fps = record.FPS()
if fps != float64(speedBaseFPS) {
t.Error("incorrect fps")
}
record.speed = 512
fps = record.FPS()
if fps != float64(speedBaseFPS)*2 {
t.Error("incorrect fps")
}
record.speed = 128
fps = record.FPS()
if fps != float64(speedBaseFPS)/2 {
t.Error("incorrect fps")
}
}

View File

@ -0,0 +1,6 @@
package d2animdata
type block struct {
recordCount uint32
records []*AnimationDataRecord
}

View File

@ -0,0 +1,32 @@
// Package d2animdata provides a file parser for AnimData files. AnimData files have the '.d2'
// file extension, but we do not call this package `d2d2` because multiple file types share this
// extension, and because the project namespace prefix makes it sound terrible.
package d2animdata
/*
The AnimData.d2 file is a binary file that contains speed and event data for animations.
The data is encoded as little-endian binary data
The file contents look like this:
type animData struct {
blocks [256]struct{
recordCount uint8
records []struct{ // *see note below
name [8]byte // last byte is always \0
framesPerDirection uint32
speed uint16 // **see note below
_ uint16 // just padded 0's
events [144]uint8 // ***see not below
}
}
}
*NOTE: can contain 0 to 67 records
**NOTE: game fps is assumed to be 25, the speed is calculated as (25 * record.speed / 256)
**NOTE: Animation events can be one of `None`, `Attack`, `Missile`, `Sound`, or `Skill`
*/

View File

@ -0,0 +1,13 @@
package d2animdata
// AnimationEvent represents an event that can happen on a frame of animation
type AnimationEvent byte
// Animation events
const (
AnimationEventNone AnimationEvent = iota
AnimationEventAttack
AnimationEventMissile
AnimationEventSound
AnimationEventSkill
)

View File

@ -0,0 +1,18 @@
package d2animdata
import "strings"
type hashTable [numBlocks]byte
func hashName(name string) byte {
hashBytes := []byte(strings.ToUpper(name))
var hash uint32
for hashByteIdx := range hashBytes {
hash += uint32(hashBytes[hashByteIdx])
}
hash %= numBlocks
return byte(hash)
}

View File

@ -0,0 +1,23 @@
package d2animdata
// AnimationDataRecord represents a single record from the AnimData.d2 file
type AnimationDataRecord struct {
name string
framesPerDirection uint32
speed uint16
events map[int]AnimationEvent
}
// FPS returns the frames per second for this animation record
func (r *AnimationDataRecord) FPS() float64 {
speedf := float64(r.speed)
divisorf := float64(speedDivisor)
basef := float64(speedBaseFPS)
return basef * speedf / divisorf
}
// FrameDurationMS returns the duration in milliseconds that a frame is displayed
func (r *AnimationDataRecord) FrameDurationMS() float64 {
return milliseconds / r.FPS()
}

Binary file not shown.

Binary file not shown.