package d2cof import ( "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" ) const ( numUnknownHeaderBytes = 21 numUnknownBodyBytes = 3 numHeaderBytes = 4 + numUnknownHeaderBytes numLayerBytes = 9 ) const ( headerNumLayers = iota headerFramesPerDir headerNumDirs headerSpeed = numHeaderBytes - 1 ) const ( layerType = iota layerShadow layerSelectable layerTransparent layerDrawEffect layerWeaponClass ) const ( badCharacter = string(byte(0)) ) // New creates a new COF func New() *COF { return &COF{ unknownHeaderBytes: make([]byte, numUnknownHeaderBytes), unknownBodyBytes: make([]byte, numUnknownBodyBytes), NumberOfDirections: 0, FramesPerDirection: 0, NumberOfLayers: 0, Speed: 0, CofLayers: make([]CofLayer, 0), CompositeLayers: make(map[d2enum.CompositeType]int), AnimationFrames: make([]d2enum.AnimationFrame, 0), Priority: make([][][]d2enum.CompositeType, 0), } } // Marshal a COF to a new byte slice func Marshal(c *COF) []byte { return c.Marshal() } // Unmarshal a byte slice to a new COF func Unmarshal(data []byte) (*COF, error) { c := New() err := c.Unmarshal(data) return c, err } // COF is a structure that represents a COF file. type COF struct { // unknown bytes for header unknownHeaderBytes []byte // unknown bytes (first "body's" bytes) unknownBodyBytes []byte NumberOfDirections int FramesPerDirection int NumberOfLayers int Speed int CofLayers []CofLayer CompositeLayers map[d2enum.CompositeType]int AnimationFrames []d2enum.AnimationFrame Priority [][][]d2enum.CompositeType } // Unmarshal a byte slice to this COF func (c *COF) Unmarshal(fileData []byte) error { var err error streamReader := d2datautils.CreateStreamReader(fileData) headerBytes, err := streamReader.ReadBytes(numHeaderBytes) if err != nil { return err } c.loadHeader(headerBytes) c.unknownBodyBytes, err = streamReader.ReadBytes(numUnknownBodyBytes) if err != nil { return err } c.CofLayers = make([]CofLayer, c.NumberOfLayers) 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++ { layer := CofLayer{} b, err := streamReader.ReadBytes(numLayerBytes) if err != nil { return err } layer.Type = d2enum.CompositeType(b[layerType]) layer.Shadow = b[layerShadow] layer.Selectable = b[layerSelectable] > 0 layer.Transparent = b[layerTransparent] > 0 layer.DrawEffect = d2enum.DrawEffect(b[layerDrawEffect]) layer.WeaponClass = d2enum.WeaponClassFromString(strings.TrimSpace(strings.ReplaceAll( string(b[layerWeaponClass:]), badCharacter, ""))) c.CofLayers[i] = layer c.CompositeLayers[layer.Type] = i } return nil } func (c *COF) loadAnimationFrames(b []byte) { c.AnimationFrames = make([]d2enum.AnimationFrame, c.FramesPerDirection) for i := range b { c.AnimationFrames[i] = d2enum.AnimationFrame(b[i]) } } func (c *COF) loadPriority(priorityBytes []byte) { priorityIndex := 0 for direction := 0; direction < c.NumberOfDirections; direction++ { c.Priority[direction] = make([][]d2enum.CompositeType, c.FramesPerDirection) for frame := 0; frame < c.FramesPerDirection; frame++ { c.Priority[direction][frame] = make([]d2enum.CompositeType, c.NumberOfLayers) for i := 0; i < c.NumberOfLayers; i++ { c.Priority[direction][frame][i] = d2enum.CompositeType(priorityBytes[priorityIndex]) priorityIndex++ } } } } // Marshal this COF to a byte slice func (c *COF) Marshal() []byte { sw := d2datautils.CreateStreamWriter() sw.PushBytes(byte(c.NumberOfLayers)) sw.PushBytes(byte(c.FramesPerDirection)) sw.PushBytes(byte(c.NumberOfDirections)) sw.PushBytes(c.unknownHeaderBytes...) sw.PushBytes(byte(c.Speed)) sw.PushBytes(c.unknownBodyBytes...) for i := range c.CofLayers { sw.PushBytes(byte(c.CofLayers[i].Type)) sw.PushBytes(c.CofLayers[i].Shadow) if c.CofLayers[i].Selectable { sw.PushBytes(byte(1)) } else { sw.PushBytes(byte(0)) } if c.CofLayers[i].Transparent { sw.PushBytes(byte(1)) } else { sw.PushBytes(byte(0)) } sw.PushBytes(byte(c.CofLayers[i].DrawEffect)) const ( maxCodeLength = 3 // we assume item codes to look like 'hax' or 'kit' terminator = 0 ) weaponCode := c.CofLayers[i].WeaponClass.String() for idx, letter := range weaponCode { if idx > maxCodeLength { break } sw.PushBytes(byte(letter)) } sw.PushBytes(terminator) } for _, i := range c.AnimationFrames { sw.PushBytes(byte(i)) } for direction := 0; direction < c.NumberOfDirections; direction++ { for frame := 0; frame < c.FramesPerDirection; frame++ { for i := 0; i < c.NumberOfLayers; i++ { sw.PushBytes(byte(c.Priority[direction][frame][i])) } } } return sw.GetBytes() }