mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-10-31 16:27:18 -04:00
2ec5dd2d85
* Fixed another go build error
518 lines
16 KiB
Go
518 lines
16 KiB
Go
package Common
|
|
|
|
import (
|
|
"log"
|
|
)
|
|
|
|
type DCCPixelBufferEntry struct {
|
|
Value []byte
|
|
Frame int
|
|
FrameCellIndex int
|
|
}
|
|
|
|
type DCCCell struct {
|
|
Width int
|
|
Height int
|
|
XOffset int
|
|
YOffset int
|
|
LastWidth int
|
|
LastHeight int
|
|
LastXOffset int
|
|
LastYOffset int
|
|
}
|
|
|
|
type DCCDirectionFrame struct {
|
|
Width int
|
|
Height int
|
|
XOffset int
|
|
YOffset int
|
|
NumberOfOptionalBytes int
|
|
NumberOfCodedBytes int
|
|
FrameIsBottomUp bool
|
|
Box Rectangle
|
|
Cells []DCCCell
|
|
PixelData []byte
|
|
HorizontalCellCount int
|
|
VerticalCellCount int
|
|
}
|
|
|
|
type DCCDirection struct {
|
|
OutSizeCoded int
|
|
CompressionFlags int
|
|
Variable0Bits int
|
|
WidthBits int
|
|
HeightBits int
|
|
XOffsetBits int
|
|
YOffsetBits int
|
|
OptionalDataBits int
|
|
CodedBytesBits int
|
|
EqualCellsBitstreamSize int
|
|
PixelMaskBitstreamSize int
|
|
EncodingTypeBitsreamSize int
|
|
RawPixelCodesBitstreamSize int
|
|
Frames []*DCCDirectionFrame
|
|
PaletteEntries []byte
|
|
Box Rectangle
|
|
Cells []*DCCCell
|
|
PixelData []byte
|
|
HorizontalCellCount int
|
|
VerticalCellCount int
|
|
PixelBuffer []*DCCPixelBufferEntry
|
|
}
|
|
|
|
type DCC struct {
|
|
Signature int
|
|
Version int
|
|
NumberOfDirections int
|
|
FramesPerDirection int
|
|
Directions []*DCCDirection
|
|
}
|
|
|
|
var crazyBitTable = []byte{0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 26, 28, 30, 32}
|
|
var pixelMaskLookup = []int{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}
|
|
|
|
func CreateDCCDirectionFrame(bits *BitMuncher, direction *DCCDirection) *DCCDirectionFrame {
|
|
result := &DCCDirectionFrame{}
|
|
bits.GetBits(direction.Variable0Bits) // Variable0
|
|
result.Width = int(bits.GetBits(direction.WidthBits))
|
|
result.Height = int(bits.GetBits(direction.HeightBits))
|
|
result.XOffset = bits.GetSignedBits(direction.XOffsetBits)
|
|
result.YOffset = bits.GetSignedBits(direction.YOffsetBits)
|
|
result.NumberOfOptionalBytes = int(bits.GetBits(direction.OptionalDataBits))
|
|
result.NumberOfCodedBytes = int(bits.GetBits(direction.CodedBytesBits))
|
|
result.FrameIsBottomUp = bits.GetBit() == 1
|
|
result.Box = Rectangle{
|
|
result.XOffset,
|
|
result.YOffset - result.Height - 1,
|
|
result.Width,
|
|
result.Height,
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (v *DCCDirectionFrame) CalculateCells(direction *DCCDirection) {
|
|
var w = 4 - ((v.Box.Left - direction.Box.Left) % 4) // Width of the first column (in pixels)
|
|
if (v.Width - w) <= 1 {
|
|
v.HorizontalCellCount = 1
|
|
} else {
|
|
tmp := v.Width - w - 1
|
|
v.HorizontalCellCount = 2 + (tmp / 4)
|
|
if (tmp % 4) == 0 {
|
|
v.HorizontalCellCount--
|
|
}
|
|
}
|
|
h := 4 - ((v.Box.Top - direction.Box.Top) % 4) // Height of the first column (in pixels)
|
|
if (v.Height - h) <= 1 {
|
|
v.VerticalCellCount = 1
|
|
} else {
|
|
tmp := v.Height - h - 1
|
|
v.VerticalCellCount = 2 + (tmp / 4)
|
|
if (tmp % 4) == 0 {
|
|
v.VerticalCellCount--
|
|
}
|
|
}
|
|
// Calculate the cell widths and heights
|
|
cellWidths := make([]int, v.HorizontalCellCount)
|
|
if v.HorizontalCellCount == 1 {
|
|
cellWidths[0] = v.Width
|
|
} else {
|
|
cellWidths[0] = w
|
|
for i := 1; i < (v.HorizontalCellCount - 1); i++ {
|
|
cellWidths[i] = 4
|
|
}
|
|
cellWidths[v.HorizontalCellCount-1] = v.Width - w - (4 * (v.HorizontalCellCount - 2))
|
|
}
|
|
|
|
cellHeights := make([]int, v.VerticalCellCount)
|
|
if v.VerticalCellCount == 1 {
|
|
cellHeights[0] = v.Height
|
|
} else {
|
|
cellHeights[0] = h
|
|
for i := 1; i < (v.VerticalCellCount - 1); i++ {
|
|
cellHeights[i] = 4
|
|
}
|
|
cellHeights[v.VerticalCellCount-1] = v.Height - h - (4 * (v.VerticalCellCount - 2))
|
|
}
|
|
|
|
v.Cells = make([]DCCCell, v.HorizontalCellCount*v.VerticalCellCount)
|
|
offsetY := v.Box.Top - direction.Box.Top
|
|
for y := 0; y < v.VerticalCellCount; y++ {
|
|
offsetX := v.Box.Left - direction.Box.Left
|
|
for x := 0; x < v.HorizontalCellCount; x++ {
|
|
v.Cells[x+(y*v.HorizontalCellCount)] = DCCCell{
|
|
XOffset: offsetX,
|
|
YOffset: offsetY,
|
|
Width: cellWidths[x],
|
|
Height: cellHeights[y],
|
|
}
|
|
offsetX += cellWidths[x]
|
|
}
|
|
offsetY += cellHeights[y]
|
|
}
|
|
}
|
|
|
|
func CreateDCCDirection(bm *BitMuncher, file *DCC) *DCCDirection {
|
|
result := &DCCDirection{}
|
|
result.OutSizeCoded = int(bm.GetUInt32())
|
|
result.CompressionFlags = int(bm.GetBits(2))
|
|
result.Variable0Bits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.WidthBits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.HeightBits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.XOffsetBits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.YOffsetBits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.OptionalDataBits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.CodedBytesBits = int(crazyBitTable[bm.GetBits(4)])
|
|
result.Frames = make([]*DCCDirectionFrame, file.FramesPerDirection)
|
|
minx := 9223372036854775807
|
|
miny := 9223372036854775807
|
|
maxx := -9223372036854775808
|
|
maxy := -9223372036854775808
|
|
// Load the frame headers
|
|
for frameIdx := 0; frameIdx < file.FramesPerDirection; frameIdx++ {
|
|
result.Frames[frameIdx] = CreateDCCDirectionFrame(bm, result)
|
|
minx = int(MinInt32(int32(result.Frames[frameIdx].Box.Left), int32(minx)))
|
|
miny = int(MinInt32(int32(result.Frames[frameIdx].Box.Top), int32(miny)))
|
|
maxx = int(MaxInt32(int32(result.Frames[frameIdx].Box.Right()), int32(maxx)))
|
|
maxy = int(MaxInt32(int32(result.Frames[frameIdx].Box.Bottom()), int32(maxy)))
|
|
}
|
|
result.Box = Rectangle{minx, miny, maxx - minx, maxy - miny}
|
|
if result.OptionalDataBits > 0 {
|
|
log.Panic("Optional bits in DCC data is not currently supported.")
|
|
}
|
|
if (result.CompressionFlags & 0x2) > 0 {
|
|
result.EqualCellsBitstreamSize = int(bm.GetBits(20))
|
|
}
|
|
result.PixelMaskBitstreamSize = int(bm.GetBits(20))
|
|
if (result.CompressionFlags & 0x1) > 0 {
|
|
result.EncodingTypeBitsreamSize = int(bm.GetBits(20))
|
|
result.RawPixelCodesBitstreamSize = int(bm.GetBits(20))
|
|
}
|
|
// PixelValuesKey
|
|
paletteEntries := make([]bool, 0)
|
|
paletteEntryCount := 0
|
|
for i := 0; i < 256; i++ {
|
|
valid := bm.GetBit() != 0
|
|
paletteEntries = append(paletteEntries, valid)
|
|
if valid {
|
|
paletteEntryCount++
|
|
}
|
|
}
|
|
result.PaletteEntries = make([]byte, paletteEntryCount)
|
|
paletteOffset := 0
|
|
for i := 0; i < 256; i++ {
|
|
if !paletteEntries[i] {
|
|
continue
|
|
}
|
|
result.PaletteEntries[paletteOffset] = byte(i)
|
|
paletteOffset++
|
|
}
|
|
// HERE BE GIANTS:
|
|
// Because of the way this thing mashes bits together, BIT offset matters
|
|
// here. For example, if you are on byte offset 3, bit offset 6, and
|
|
// the EqualCellsBitstreamSize is 20 bytes, then the next bit stream
|
|
// will be located at byte 23, bit offset 6!
|
|
equalCellsBitstream := CopyBitMuncher(bm)
|
|
bm.SkipBits(result.EqualCellsBitstreamSize)
|
|
pixelMaskBitstream := CopyBitMuncher(bm)
|
|
bm.SkipBits(result.PixelMaskBitstreamSize)
|
|
encodingTypeBitsream := CopyBitMuncher(bm)
|
|
bm.SkipBits(result.EncodingTypeBitsreamSize)
|
|
rawPixelCodesBitstream := CopyBitMuncher(bm)
|
|
bm.SkipBits(result.RawPixelCodesBitstreamSize)
|
|
pixelCodeandDisplacement := CopyBitMuncher(bm)
|
|
// Calculate the cells for the direction
|
|
result.CalculateCells()
|
|
// Calculate the cells for each of the frames
|
|
for _, frame := range result.Frames {
|
|
frame.CalculateCells(result)
|
|
}
|
|
// Fill in the pixel buffer
|
|
result.FillPixelBuffer(pixelCodeandDisplacement, equalCellsBitstream, pixelMaskBitstream, encodingTypeBitsream, rawPixelCodesBitstream)
|
|
// Generate the actual frame pixel data
|
|
result.GenerateFrames(pixelCodeandDisplacement)
|
|
// Verify that everything we expected to read was actually read (sanity check)...
|
|
if equalCellsBitstream.BitsRead != result.EqualCellsBitstreamSize {
|
|
log.Panic("Did not read the correct number of bits!")
|
|
}
|
|
if pixelMaskBitstream.BitsRead != result.PixelMaskBitstreamSize {
|
|
log.Panic("Did not read the correct number of bits!")
|
|
}
|
|
if encodingTypeBitsream.BitsRead != result.EncodingTypeBitsreamSize {
|
|
log.Panic("Did not read the correct number of bits!")
|
|
}
|
|
if rawPixelCodesBitstream.BitsRead != result.RawPixelCodesBitstreamSize {
|
|
log.Panic("Did not read the correct number of bits!")
|
|
}
|
|
bm.SkipBits(pixelCodeandDisplacement.BitsRead)
|
|
return result
|
|
}
|
|
|
|
func (v *DCCDirection) GenerateFrames(pcd *BitMuncher) {
|
|
pbIdx := 0
|
|
for _, cell := range v.Cells {
|
|
cell.LastWidth = -1
|
|
cell.LastHeight = -1
|
|
}
|
|
v.PixelData = make([]byte, v.Box.Width*v.Box.Height)
|
|
frameIndex := -1
|
|
for _, frame := range v.Frames {
|
|
frameIndex++
|
|
frame.PixelData = make([]byte, v.Box.Width*v.Box.Height)
|
|
c := -1
|
|
for _, cell := range frame.Cells {
|
|
c++
|
|
cellX := cell.XOffset / 4
|
|
cellY := cell.YOffset / 4
|
|
cellIndex := cellX + (cellY * v.HorizontalCellCount)
|
|
bufferCell := v.Cells[cellIndex]
|
|
pbe := v.PixelBuffer[pbIdx]
|
|
if (pbe.Frame != frameIndex) || (pbe.FrameCellIndex != c) {
|
|
// This buffer cell has an EqualCell bit set to 1, so copy the frame cell or clear it
|
|
if (cell.Width != bufferCell.LastWidth) || (cell.Height != bufferCell.LastHeight) {
|
|
// Different sizes
|
|
/// TODO: Clear the pixels of the frame cell
|
|
for y := 0; y < cell.Height; y++ {
|
|
for x := 0; x < cell.Width; x++ {
|
|
v.PixelData[x+cell.XOffset+((y+cell.YOffset)*frame.Width)] = 0
|
|
}
|
|
}
|
|
} else {
|
|
// Same sizes
|
|
// Copy the old frame cell into the new position
|
|
for fy := 0; fy < cell.Height; fy++ {
|
|
for fx := 0; fx < cell.Width; fx++ {
|
|
// Frame (buff.lastx, buff.lasty) -> Frame (cell.offx, cell.offy)
|
|
// Cell (0, 0,) ->
|
|
// blit(dir->bmp, dir->bmp, buff_cell->last_x0, buff_cell->last_y0, cell->x0, cell->y0, cell->w, cell->h );
|
|
v.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] = v.PixelData[fx+bufferCell.LastXOffset+((fy+bufferCell.LastYOffset)*v.Box.Width)]
|
|
}
|
|
}
|
|
// Copy it again into the final frame image
|
|
for fy := 0; fy < cell.Height; fy++ {
|
|
for fx := 0; fx < cell.Width; fx++ {
|
|
// blit(cell->bmp, frm_bmp, 0, 0, cell->x0, cell->y0, cell->w, cell->h );
|
|
frame.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] = v.PixelData[cell.XOffset+fx+((cell.YOffset+fy)*v.Box.Width)]
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if pbe.Value[0] == pbe.Value[1] {
|
|
// Clear the frame
|
|
//cell.PixelData = new byte[cell.Width * cell.Height];
|
|
for y := 0; y < cell.Height; y++ {
|
|
for x := 0; x < cell.Width; x++ {
|
|
v.PixelData[x+cell.XOffset+((y+cell.YOffset)*v.Box.Width)] = pbe.Value[0]
|
|
}
|
|
}
|
|
} else {
|
|
// Fill the frame cell with the pixels
|
|
bitsToRead := 1
|
|
if pbe.Value[1] != pbe.Value[2] {
|
|
bitsToRead = 2
|
|
}
|
|
for y := 0; y < cell.Height; y++ {
|
|
for x := 0; x < cell.Width; x++ {
|
|
paletteIndex := pcd.GetBits(bitsToRead)
|
|
v.PixelData[x+cell.XOffset+((y+cell.YOffset)*v.Box.Width)] = pbe.Value[paletteIndex]
|
|
}
|
|
}
|
|
}
|
|
// Copy the frame cell into the frame
|
|
for fy := 0; fy < cell.Height; fy++ {
|
|
for fx := 0; fx < cell.Width; fx++ {
|
|
//blit(cell->bmp, frm_bmp, 0, 0, cell->x0, cell->y0, cell->w, cell->h );
|
|
frame.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] = v.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)]
|
|
}
|
|
}
|
|
pbIdx++
|
|
}
|
|
bufferCell.LastWidth = cell.Width
|
|
bufferCell.LastHeight = cell.Height
|
|
bufferCell.LastXOffset = cell.XOffset
|
|
bufferCell.LastYOffset = cell.YOffset
|
|
}
|
|
// Free up the stuff we no longer need
|
|
frame.Cells = nil
|
|
}
|
|
v.Cells = nil
|
|
v.PixelData = nil
|
|
}
|
|
|
|
func (v *DCCDirection) FillPixelBuffer(pcd, ec, pm, et, rp *BitMuncher) {
|
|
lastPixel := uint32(0)
|
|
maxCellX := 0
|
|
maxCellY := 0
|
|
for _, frame := range v.Frames {
|
|
maxCellX += frame.HorizontalCellCount
|
|
maxCellY += frame.VerticalCellCount
|
|
}
|
|
v.PixelBuffer = make([]*DCCPixelBufferEntry, maxCellX*maxCellY)
|
|
for i := 0; i < maxCellX*maxCellY; i++ {
|
|
v.PixelBuffer[i] = &DCCPixelBufferEntry{
|
|
Value: make([]byte, 4),
|
|
Frame: -1,
|
|
FrameCellIndex: -1,
|
|
}
|
|
}
|
|
cellBuffer := make([]*DCCPixelBufferEntry, v.HorizontalCellCount*v.VerticalCellCount)
|
|
frameIndex := -1
|
|
pbIndex := -1
|
|
pixelMask := uint32(0x00)
|
|
for _, frame := range v.Frames {
|
|
frameIndex++
|
|
originCellX := (frame.Box.Left - v.Box.Left) / 4
|
|
originCellY := (frame.Box.Top - v.Box.Top) / 4
|
|
frameCellIndex := 0
|
|
for cellY := 0; cellY < frame.VerticalCellCount; cellY++ {
|
|
currentCellY := cellY + originCellY
|
|
for cellX := 0; cellX < frame.HorizontalCellCount; cellX++ {
|
|
frameCellIndex++
|
|
currentCell := originCellX + cellX + (currentCellY * v.HorizontalCellCount)
|
|
nextCell := false
|
|
tmp := 0
|
|
if cellBuffer[currentCell] != nil {
|
|
if v.EqualCellsBitstreamSize > 0 {
|
|
tmp = int(ec.GetBit())
|
|
} else {
|
|
tmp = 0
|
|
}
|
|
if tmp == 0 {
|
|
pixelMask = pm.GetBits(4)
|
|
} else {
|
|
nextCell = true
|
|
}
|
|
} else {
|
|
pixelMask = 0x0F
|
|
}
|
|
if nextCell {
|
|
continue
|
|
}
|
|
// Decode the pixels
|
|
pixelStack := make([]uint32, 4)
|
|
lastPixel = 0
|
|
numberOfPixelBits := pixelMaskLookup[pixelMask]
|
|
encodingType := 0
|
|
if (numberOfPixelBits != 0) && (v.EncodingTypeBitsreamSize > 0) {
|
|
encodingType = int(et.GetBit())
|
|
} else {
|
|
encodingType = 0
|
|
}
|
|
decodedPixel := 0
|
|
for i := 0; i < numberOfPixelBits; i++ {
|
|
if encodingType != 0 {
|
|
pixelStack[i] = rp.GetBits(8)
|
|
} else {
|
|
pixelStack[i] = lastPixel
|
|
pixelDisplacement := pcd.GetBits(4)
|
|
pixelStack[i] += pixelDisplacement
|
|
for pixelDisplacement == 15 {
|
|
pixelDisplacement = pcd.GetBits(4)
|
|
pixelStack[i] += pixelDisplacement
|
|
}
|
|
}
|
|
if pixelStack[i] == lastPixel {
|
|
pixelStack[i] = 0
|
|
i = numberOfPixelBits // Just break here....
|
|
} else {
|
|
lastPixel = pixelStack[i]
|
|
decodedPixel++
|
|
}
|
|
}
|
|
oldEntry := cellBuffer[currentCell]
|
|
pbIndex++
|
|
newEntry := v.PixelBuffer[pbIndex]
|
|
curIdx := decodedPixel - 1
|
|
for i := 0; i < 4; i++ {
|
|
if (pixelMask & (1 << uint(i))) != 0 {
|
|
if curIdx >= 0 {
|
|
newEntry.Value[i] = byte(pixelStack[curIdx])
|
|
curIdx--
|
|
} else {
|
|
newEntry.Value[i] = 0
|
|
}
|
|
} else {
|
|
newEntry.Value[i] = oldEntry.Value[i]
|
|
}
|
|
}
|
|
cellBuffer[currentCell] = newEntry
|
|
newEntry.Frame = frameIndex
|
|
newEntry.FrameCellIndex = cellX + (cellY * frame.HorizontalCellCount)
|
|
}
|
|
}
|
|
}
|
|
// Convert the palette entry index into actual palette entries
|
|
for i := 0; i <= pbIndex; i++ {
|
|
for x := 0; x < 4; x++ {
|
|
v.PixelBuffer[i].Value[x] = v.PaletteEntries[v.PixelBuffer[i].Value[x]]
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *DCCDirection) CalculateCells() {
|
|
// Calculate the number of vertical and horizontal cells we need
|
|
v.HorizontalCellCount = 1 + (v.Box.Width-1)/4
|
|
v.VerticalCellCount = 1 + (v.Box.Height-1)/4
|
|
// Calculate the cell widths
|
|
cellWidths := make([]int, v.HorizontalCellCount)
|
|
if v.HorizontalCellCount == 1 {
|
|
cellWidths[0] = v.Box.Width
|
|
} else {
|
|
for i := 0; i < v.HorizontalCellCount-1; i++ {
|
|
cellWidths[i] = 4
|
|
}
|
|
cellWidths[v.HorizontalCellCount-1] = v.Box.Width - (4 * (v.HorizontalCellCount - 1))
|
|
}
|
|
// Calculate the cell heights
|
|
cellHeights := make([]int, v.VerticalCellCount)
|
|
if v.VerticalCellCount == 1 {
|
|
cellHeights[0] = v.Box.Height
|
|
} else {
|
|
for i := 0; i < v.VerticalCellCount-1; i++ {
|
|
cellHeights[i] = 4
|
|
}
|
|
cellHeights[v.VerticalCellCount-1] = v.Box.Height - (4 * (v.VerticalCellCount - 1))
|
|
}
|
|
// Set the cell widths and heights in the cell buffer
|
|
v.Cells = make([]*DCCCell, v.VerticalCellCount*v.HorizontalCellCount)
|
|
yOffset := 0
|
|
for y := 0; y < v.VerticalCellCount; y++ {
|
|
xOffset := 0
|
|
for x := 0; x < v.HorizontalCellCount; x++ {
|
|
v.Cells[x+(y*v.HorizontalCellCount)] = &DCCCell{
|
|
Width: cellWidths[x],
|
|
Height: cellHeights[y],
|
|
XOffset: xOffset,
|
|
YOffset: yOffset,
|
|
}
|
|
xOffset += 4
|
|
}
|
|
yOffset += 4
|
|
}
|
|
}
|
|
|
|
func LoadDCC(path string, fileProvider FileProvider) *DCC {
|
|
result := &DCC{}
|
|
fileData := fileProvider.LoadFile(path)
|
|
var bm = CreateBitMuncher(fileData, 0)
|
|
result.Signature = int(bm.GetByte())
|
|
if result.Signature != 0x74 {
|
|
log.Fatal("Signature expected to be 0x74 but it is not.")
|
|
}
|
|
result.Version = int(bm.GetByte())
|
|
result.NumberOfDirections = int(bm.GetByte())
|
|
result.FramesPerDirection = int(bm.GetInt32())
|
|
if bm.GetInt32() != 1 {
|
|
log.Fatal("This value isn't 1. It has to be 1.")
|
|
}
|
|
bm.GetInt32() // TotalSizeCoded
|
|
directionOffsets := make([]int, result.NumberOfDirections)
|
|
for i := 0; i < result.NumberOfDirections; i++ {
|
|
directionOffsets[i] = int(bm.GetInt32())
|
|
}
|
|
result.Directions = make([]*DCCDirection, result.NumberOfDirections)
|
|
for i := 0; i < result.NumberOfDirections; i++ {
|
|
result.Directions[i] = CreateDCCDirection(CreateBitMuncher(fileData, directionOffsets[i]*8), result)
|
|
}
|
|
return result
|
|
}
|