OpenDiablo2/d2core/d2asset/composite.go

386 lines
9.4 KiB
Go
Raw Normal View History

package d2asset
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
const (
hardcodedFPS = 25.0
hardcodedDivisor = 1.0 / 256.0
speedUnit = hardcodedFPS * hardcodedDivisor
)
// Composite is a composite entity animation
type Composite struct {
remove d2asset singleton (#726) * export d2asset singleton * add *d2asset.AssetManager to d2app - d2app now has a reference to an asset manager which it will use for loading - added asset loader methods to the asset manager - functions in d2asset are now wrappers for asset manager methods * add asset manager reference to audio provider - d2app asset manager reference is now passed to audio provider - asset manager is created in main.go for now to pass into audio provider - CreateSoundEffect is now a method, no longer exported, uses the asset manager reference * d2app passes asset manager refence to map engine test * in d2asset, all calls to LoadFile replaced with call to Singleton.Loadfile * blizzard intro and credits screen - d2app passes reference to the asset manager to these screens * asset manager for d2map - adding MapStampFactory, takes an asset manager reference - embedded MapStampFactory into the MapEngine - LoadStamp is now a method of the MapStampFactory * d2asset: removed LoadFileStream, LoadFile, and FileExists * d2gui changes - singleton now has an asset manager reference - calls to d2asset loader functions removed - createButton is now a method of LayoutManager - moved LayoutEntry to its own file * map entity factory - Map engine has an embedded map entity factory - Map stamp factory gets a reference to the map engine's entity factory - Stamps are given a reference to the map engine entity factory when created - Character select gets a map entity factory - Embedded the stamp factory into the MapEngine * asset manager for d2ui - d2ui is passed an asset manager reference when created - all calls to d2asset loader functions in d2ui now refer to the asset manager - d2gamescreen gets a ui manager when created - help overlay is now passed a ui manager when created * d2gamescreen + d2player: asset manager references added an asset manager reference to - inventory panel + inventory grid - mini panel - game controls - help overlay - character select - main menu - select hero class - hero stats panel * Removed d2asset.LoadAnimation all references to this function have been replaced with calls to the asset manager method * adding asset to help overlay, bugfix for 4d59c91 * Removed d2asset.LoadFont and d2asset.LoadAnimationWithEffect all references to these have been replaced with calls to the asset manager methods * MapRenderer now gets an asset manager reference * removed d2asset.LoadPalette all references have been replaced with calls to an asset manager instance * merged d2object with d2mapentity d2object was only being used to create objects in the map, so the provider function is now a method of the map entity factory. calls to d2asset have been removed. * removed d2asset.LoadComposite all calls are now made to the asset manager method * removed d2asset singleton all singleton references have been removed, a single instance of the asset manager is passed around the entire app * rename Initialize to NewAssetManager
2020-09-12 20:51:30 +00:00
*AssetManager
baseType d2enum.ObjectType
basePath string
token string
palettePath string
direction int
equipment [d2enum.CompositeTypeMax]string
mode *compositeMode
size *size
}
type size struct {
2020-08-11 22:01:33 +00:00
Width int
Height int
}
// Advance moves the composite animation forward for a given elapsed time in nanoseconds.
func (c *Composite) Advance(elapsed float64) error {
if c.mode == nil {
return nil
}
c.mode.lastFrameTime += elapsed
framesToAdd := int(c.mode.lastFrameTime / c.mode.animationSpeed)
c.mode.lastFrameTime -= float64(framesToAdd) * c.mode.animationSpeed
c.mode.frameIndex += framesToAdd
c.mode.playedCount += c.mode.frameIndex / c.mode.frameCount
c.mode.frameIndex %= c.mode.frameCount
for _, layer := range c.mode.layers {
if layer != nil {
if err := layer.Advance(elapsed); err != nil {
return err
}
}
}
return nil
}
// Render performs drawing of the Composite on the rendered d2interface.Surface.
func (c *Composite) Render(target d2interface.Surface) error {
if c.mode == nil {
return nil
}
direction := d2cof.Dir64ToCof(c.direction, c.mode.cof.NumberOfDirections)
2020-07-26 23:29:37 +00:00
for _, layerIndex := range c.mode.cof.Priority[direction][c.mode.frameIndex] {
layer := c.mode.layers[layerIndex]
Decouple asset manager from renderer (#730) * improve AssetManager implementation Notable changes are: * removed the individual managers inside of d2asset, only one asset manager * AssetManager now has caches for the types of files it loads * created a type for TextDictionary (the txt file structs) * fixed a file path bug in d2loader Source * fixed a asset stream bug in d2loader Asset * d2loader.Loader now needs a d2config.Config on creation (for resolving locale files) * updated the mpq file in d2asset test data, added test case for "sub-directory" * added a Data method to d2asset.Asset. The data is cached on first full read. * renamed ArchiveDataStream to DataStream in d2interface * moved palette utility func out of d2asset and into d2util * bugfix for MacOS mpq loader issue * lint fixes, added data caching to filesystem asset * adding comment for mpq asset close * Decouple d2asset from d2render Notable changes in d2common: * d2dcc.Load now fully decodes the dcc and stores the directions/frames in the dcc struct * un-exported dcc.decodeDirection, it is only used in d2dcc * removed font interface from d2interface, we only have one font implementation * added `Renderer` method to d2interface.Surface, animations use this to bind to a renderer and create surfaces as they need * added `BindRenderer` method to animation interface Notable changes in d2common/d2asset: * **d2asset.NewAssetManager only needs to be passed a d2config.Config**, it is decoupled from d2render * exported Animation * Animation implementation binds to the renderer to create surfaces only on the first time it is rendered * font, dcc, dc6 initialization logic moved out of asset_manager.go * for dc6 and dcc animations, the process of decoding and creating render surfaces has been broken into different methods * the d2asset.Font struct now stores font table data for initialization purposes Notable changes in d2core/d2render: * Surfaces store a renderer reference, this allows animations to bind to the renderer and create a surface just-in-time **These last changes should have been a separate PR, sorry.** Notable changes in d2core/d2ui: * ui.NewSprite now handles creating an animation internally, only needs image and palette path as arguments Notable Changes in d2game: Because of the change in d2ui, all instances of this code pattern... ```golang animation, err := screen.asset.LoadAnimation(imgPath, palettePath) sprite, err := screen.ui.NewSprite(animation) ``` ... becomes this ... ```golang sprite, err := screen.ui.NewSprite(imgPath, palettePath) ```
2020-09-14 21:31:45 +00:00
2020-07-26 23:29:37 +00:00
if layer != nil {
layer.RenderFromOrigin(target, true)
2020-07-26 23:29:37 +00:00
}
}
for _, layerIndex := range c.mode.cof.Priority[direction][c.mode.frameIndex] {
layer := c.mode.layers[layerIndex]
if layer != nil {
layer.RenderFromOrigin(target, false)
}
}
return nil
}
// ObjectAnimationMode returns the object animation mode
func (c *Composite) ObjectAnimationMode() d2enum.ObjectAnimationMode {
return c.mode.animationMode.(d2enum.ObjectAnimationMode)
}
// GetAnimationMode returns the animation mode the Composite should render with.
func (c *Composite) GetAnimationMode() string {
return c.mode.animationMode.String()
}
// GetCurrentFrame returns the frame index in the current animation mode.
func (c *Composite) GetCurrentFrame() int {
return c.mode.frameIndex
}
// GetFrameCount returns the number of frames in the current animation mode.
func (c *Composite) GetFrameCount() int {
return c.mode.frameCount
}
// GetWeaponClass returns the currently loaded weapon class
func (c *Composite) GetWeaponClass() string {
return c.mode.weaponClass
}
// SetMode sets the Composite's animation mode weapon class and direction
func (c *Composite) SetMode(animationMode animationMode, weaponClass string) error {
if c.mode != nil && c.mode.animationMode.String() == animationMode.String() && c.mode.weaponClass == weaponClass {
return nil
}
mode, err := c.createMode(animationMode, weaponClass)
if err != nil {
return err
}
c.resetPlayedCount()
c.mode = mode
return nil
}
// Equip changes the current layer configuration
func (c *Composite) Equip(equipment *[d2enum.CompositeTypeMax]string) error {
c.equipment = *equipment
if c.mode == nil {
return nil
}
mode, err := c.createMode(c.mode.animationMode, c.mode.weaponClass)
if err != nil {
return err
}
c.mode = mode
return nil
}
// SetAnimSpeed sets the speed at which the Composite's animation should advance through its frames
func (c *Composite) SetAnimSpeed(speed int) {
c.mode.animationSpeed = 1.0 / (float64(speed) * speedUnit) //nolint:gomnd // taking inverse
for layerIdx := range c.mode.layers {
layer := c.mode.layers[layerIdx]
if layer != nil {
layer.SetPlaySpeed(c.mode.animationSpeed)
}
}
}
// SetDirection sets the direction of the composite and its layers
func (c *Composite) SetDirection(direction int) {
Decouple asset manager from renderer (#730) * improve AssetManager implementation Notable changes are: * removed the individual managers inside of d2asset, only one asset manager * AssetManager now has caches for the types of files it loads * created a type for TextDictionary (the txt file structs) * fixed a file path bug in d2loader Source * fixed a asset stream bug in d2loader Asset * d2loader.Loader now needs a d2config.Config on creation (for resolving locale files) * updated the mpq file in d2asset test data, added test case for "sub-directory" * added a Data method to d2asset.Asset. The data is cached on first full read. * renamed ArchiveDataStream to DataStream in d2interface * moved palette utility func out of d2asset and into d2util * bugfix for MacOS mpq loader issue * lint fixes, added data caching to filesystem asset * adding comment for mpq asset close * Decouple d2asset from d2render Notable changes in d2common: * d2dcc.Load now fully decodes the dcc and stores the directions/frames in the dcc struct * un-exported dcc.decodeDirection, it is only used in d2dcc * removed font interface from d2interface, we only have one font implementation * added `Renderer` method to d2interface.Surface, animations use this to bind to a renderer and create surfaces as they need * added `BindRenderer` method to animation interface Notable changes in d2common/d2asset: * **d2asset.NewAssetManager only needs to be passed a d2config.Config**, it is decoupled from d2render * exported Animation * Animation implementation binds to the renderer to create surfaces only on the first time it is rendered * font, dcc, dc6 initialization logic moved out of asset_manager.go * for dc6 and dcc animations, the process of decoding and creating render surfaces has been broken into different methods * the d2asset.Font struct now stores font table data for initialization purposes Notable changes in d2core/d2render: * Surfaces store a renderer reference, this allows animations to bind to the renderer and create a surface just-in-time **These last changes should have been a separate PR, sorry.** Notable changes in d2core/d2ui: * ui.NewSprite now handles creating an animation internally, only needs image and palette path as arguments Notable Changes in d2game: Because of the change in d2ui, all instances of this code pattern... ```golang animation, err := screen.asset.LoadAnimation(imgPath, palettePath) sprite, err := screen.ui.NewSprite(animation) ``` ... becomes this ... ```golang sprite, err := screen.ui.NewSprite(imgPath, palettePath) ```
2020-09-14 21:31:45 +00:00
if c.mode == nil {
return
}
wg := sync.WaitGroup{}
c.direction = direction
wg.Add(len(c.mode.layers))
for layerIdx := range c.mode.layers {
go func(idx int) {
defer wg.Done()
layer := c.mode.layers[idx]
if layer != nil {
if err := layer.SetDirection(c.direction); err != nil {
fmt.Printf("failed to set direction of layer: %d, err: %v\n", idx, err)
}
2020-07-23 16:56:50 +00:00
}
}(layerIdx)
}
wg.Wait()
}
// GetDirection returns the current direction the composite is facing
func (c *Composite) GetDirection() int {
return c.direction
}
// GetPlayedCount returns the number of times the current animation mode has completed all its distinct frames
func (c *Composite) GetPlayedCount() int {
if c.mode == nil {
return 0
}
return c.mode.playedCount
}
// SetPlayLoop turns on or off animation looping
func (c *Composite) SetPlayLoop(loop bool) {
for layerIdx := range c.mode.layers {
layer := c.mode.layers[layerIdx]
if layer != nil {
layer.SetPlayLoop(loop)
}
}
}
// SetSubLoop sets a loop to be between the specified frame indices
func (c *Composite) SetSubLoop(startFrame, endFrame int) {
for layerIdx := range c.mode.layers {
layer := c.mode.layers[layerIdx]
if layer != nil {
layer.SetSubLoop(startFrame, endFrame)
}
}
}
// SetCurrentFrame sets the current frame index of the animation
func (c *Composite) SetCurrentFrame(frame int) {
for layerIdx := range c.mode.layers {
layer := c.mode.layers[layerIdx]
if layer != nil {
2020-07-23 16:56:50 +00:00
if err := layer.SetCurrentFrame(frame); err != nil {
fmt.Printf("failed to set current frame of layer: %d, err: %v\n", layerIdx, err)
}
}
}
}
func (c *Composite) resetPlayedCount() {
if c.mode != nil {
c.mode.playedCount = 0
}
}
type animationMode interface {
String() string
}
type compositeMode struct {
cof *d2cof.COF
animationMode animationMode
weaponClass string
playedCount int
layers []d2interface.Animation
frameCount int
frameIndex int
animationSpeed float64
lastFrameTime float64
}
func (c *Composite) createMode(animationMode animationMode, weaponClass string) (*compositeMode, error) {
cofPath := fmt.Sprintf("%s/%s/COF/%s%s%s.COF", c.basePath, c.token, c.token, animationMode, weaponClass)
if exists, err := c.FileExists(cofPath); !exists {
return nil, fmt.Errorf("composite not found at path '%s': %v", cofPath, err)
}
cof, err := c.LoadCOF(cofPath)
if err != nil {
return nil, err
}
animationKey := strings.ToUpper(c.token + animationMode.String() + weaponClass)
animationData := c.Records.Animation.Data.GetRecords(animationKey)
if len(animationData) == 0 {
Decouple asset manager from renderer (#730) * improve AssetManager implementation Notable changes are: * removed the individual managers inside of d2asset, only one asset manager * AssetManager now has caches for the types of files it loads * created a type for TextDictionary (the txt file structs) * fixed a file path bug in d2loader Source * fixed a asset stream bug in d2loader Asset * d2loader.Loader now needs a d2config.Config on creation (for resolving locale files) * updated the mpq file in d2asset test data, added test case for "sub-directory" * added a Data method to d2asset.Asset. The data is cached on first full read. * renamed ArchiveDataStream to DataStream in d2interface * moved palette utility func out of d2asset and into d2util * bugfix for MacOS mpq loader issue * lint fixes, added data caching to filesystem asset * adding comment for mpq asset close * Decouple d2asset from d2render Notable changes in d2common: * d2dcc.Load now fully decodes the dcc and stores the directions/frames in the dcc struct * un-exported dcc.decodeDirection, it is only used in d2dcc * removed font interface from d2interface, we only have one font implementation * added `Renderer` method to d2interface.Surface, animations use this to bind to a renderer and create surfaces as they need * added `BindRenderer` method to animation interface Notable changes in d2common/d2asset: * **d2asset.NewAssetManager only needs to be passed a d2config.Config**, it is decoupled from d2render * exported Animation * Animation implementation binds to the renderer to create surfaces only on the first time it is rendered * font, dcc, dc6 initialization logic moved out of asset_manager.go * for dc6 and dcc animations, the process of decoding and creating render surfaces has been broken into different methods * the d2asset.Font struct now stores font table data for initialization purposes Notable changes in d2core/d2render: * Surfaces store a renderer reference, this allows animations to bind to the renderer and create a surface just-in-time **These last changes should have been a separate PR, sorry.** Notable changes in d2core/d2ui: * ui.NewSprite now handles creating an animation internally, only needs image and palette path as arguments Notable Changes in d2game: Because of the change in d2ui, all instances of this code pattern... ```golang animation, err := screen.asset.LoadAnimation(imgPath, palettePath) sprite, err := screen.ui.NewSprite(animation) ``` ... becomes this ... ```golang sprite, err := screen.ui.NewSprite(imgPath, palettePath) ```
2020-09-14 21:31:45 +00:00
return nil, errors.New("could not find Animation data")
}
mode := &compositeMode{
cof: cof,
animationMode: animationMode,
weaponClass: weaponClass,
layers: make([]d2interface.Animation, d2enum.CompositeTypeMax),
frameCount: animationData[0].FramesPerDirection(),
animationSpeed: 1.0 / (float64(animationData[0].Speed()) * speedUnit), //nolint:gomnd // taking inverse
}
for _, cofLayer := range cof.CofLayers {
layerValue := c.equipment[cofLayer.Type]
if layerValue == "" {
layerValue = "lit"
}
drawEffect := d2enum.DrawEffectNone
if cofLayer.Transparent {
drawEffect = cofLayer.DrawEffect
}
layer, err := c.loadCompositeLayer(cofLayer.Type.String(), layerValue, animationMode.String(),
cofLayer.WeaponClass.String(), c.palettePath, drawEffect)
if err == nil {
layer.SetPlaySpeed(mode.animationSpeed)
layer.PlayForward()
layer.SetShadow(cofLayer.Shadow != 0)
if err := layer.SetDirection(c.direction); err != nil {
return nil, err
}
mode.layers[cofLayer.Type] = layer
}
}
return mode, nil
}
func (c *Composite) loadCompositeLayer(layerKey, layerValue, animationMode, weaponClass,
palettePath string, drawEffect d2enum.DrawEffect) (d2interface.Animation, error) {
animationPaths := []string{
fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", c.basePath, c.token, layerKey, c.token, layerKey, layerValue, animationMode, weaponClass),
fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dc6", c.basePath, c.token, layerKey, c.token, layerKey, layerValue, animationMode, weaponClass),
}
for idx := range animationPaths {
exists, err := c.FileExists(animationPaths[idx])
if !exists || err != nil {
return nil, fmt.Errorf("animation path '%s' not found: %v", animationPaths[idx], err)
}
animation, err := c.LoadAnimationWithEffect(animationPaths[idx], palettePath, drawEffect)
if err == nil {
return animation, nil
}
}
return nil, errors.New("animation not found")
}
2020-08-11 22:01:33 +00:00
// GetSize returns the size of the composite
func (c *Composite) GetSize() (w, h int) {
c.updateSize()
return c.size.Width, c.size.Height
}
func (c *Composite) updateSize() {
if c.mode == nil {
return
}
direction := d2cof.Dir64ToCof(c.direction, c.mode.cof.NumberOfDirections)
biggestW, biggestH := 0, 0
for _, layerIndex := range c.mode.cof.Priority[direction][c.mode.frameIndex] {
layer := c.mode.layers[layerIndex]
if layer != nil {
w, h := layer.GetCurrentFrameSize()
if biggestW < w {
biggestW = w
}
if biggestH < h {
biggestH = h
}
}
}
if c.size == nil {
c.size = &size{}
}
c.size.Width = biggestW
c.size.Height = biggestH
}
func baseString(baseType d2enum.ObjectType) string {
switch baseType {
case d2enum.ObjectTypePlayer:
return "/data/global/chars"
case d2enum.ObjectTypeCharacter:
return "/data/global/monsters"
case d2enum.ObjectTypeItem:
return "/data/global/objects"
default:
return ""
}
}