mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-06 03:24:15 -04:00
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:
parent
0218cad717
commit
65cce60eab
130
d2common/d2fileformats/d2animdata/animdata.go
Normal file
130
d2common/d2fileformats/d2animdata/animdata.go
Normal 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
|
||||||
|
}
|
147
d2common/d2fileformats/d2animdata/animdata_test.go
Normal file
147
d2common/d2fileformats/d2animdata/animdata_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
6
d2common/d2fileformats/d2animdata/block.go
Normal file
6
d2common/d2fileformats/d2animdata/block.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package d2animdata
|
||||||
|
|
||||||
|
type block struct {
|
||||||
|
recordCount uint32
|
||||||
|
records []*AnimationDataRecord
|
||||||
|
}
|
32
d2common/d2fileformats/d2animdata/doc.go
Normal file
32
d2common/d2fileformats/d2animdata/doc.go
Normal 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`
|
||||||
|
|
||||||
|
*/
|
13
d2common/d2fileformats/d2animdata/events.go
Normal file
13
d2common/d2fileformats/d2animdata/events.go
Normal 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
|
||||||
|
)
|
18
d2common/d2fileformats/d2animdata/hash.go
Normal file
18
d2common/d2fileformats/d2animdata/hash.go
Normal 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)
|
||||||
|
}
|
23
d2common/d2fileformats/d2animdata/record.go
Normal file
23
d2common/d2fileformats/d2animdata/record.go
Normal 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()
|
||||||
|
}
|
BIN
d2common/d2fileformats/d2animdata/testdata/AnimData.d2
vendored
Normal file
BIN
d2common/d2fileformats/d2animdata/testdata/AnimData.d2
vendored
Normal file
Binary file not shown.
BIN
d2common/d2fileformats/d2animdata/testdata/BadData.d2
vendored
Normal file
BIN
d2common/d2fileformats/d2animdata/testdata/BadData.d2
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user