Skip to content

Commit

Permalink
sma support (#548)
Browse files Browse the repository at this point in the history
* sma

* linting and hook up to filetypes

* Update README.rst

* dependencies lint cleanup

* ruff fix

* ruff format fix

* fix animations for icons

---------

Co-authored-by: Marco Köpcke <[email protected]>
  • Loading branch information
audinowho and theCapypara authored Dec 20, 2024
1 parent 27d9b8a commit 0ac0a2e
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 0 deletions.
2 changes: 2 additions & 0 deletions skytemple_files/common/types/file_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
from skytemple_files.compression.custom_999.handler import Custom999Handler
from skytemple_files.compression.rle_nibble.handler import RleNibbleHandler
from skytemple_files.graphics.bg_list_dat.handler import BgListDatHandler
from skytemple_files.graphics.sma.handler import SmaHandler
from skytemple_files.graphics.w16.handler import W16Handler
from skytemple_files.graphics.wan_wat.handler import WanHandler
from skytemple_files.graphics.wte.handler import WteHandler
Expand Down Expand Up @@ -154,6 +155,7 @@ class FileType:
STR = StrHandler
LSD = LsdHandler

SMA = SmaHandler
SSA = SsaHandler
SSE = SsaHandler
SSS = SsaHandler
Expand Down
55 changes: 55 additions & 0 deletions skytemple_files/graphics/sma/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
SMA File Format
===============
manpu_su.sma and manpu_ma.sma are both found in the /SYSTEM/ folder. manpu_su.sma contains status effect icon data and has the format detailed in this file. manpu_ma.sma contains no image data and its structure/use is currently unknown.


The file uses SIR0 headers to store its pointers. General SIR0 details can be found in the main SIR0 documentation. The sections below will cover only manpu_su.sma-specific blocks of data.

+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Name | Offset | Size (Per Element) | # of Elements | Description |
+=======================+=============================+=====================+==============================+==================================================================================================+
| SIR0 Header | 0x00 | 16 Bytes | 1 | Details in the SIR0 documentation |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Animation Data | Pointed by Content Header | 12 | Specified by Content Header | Each element contains animation data of a status icon, including size and number of frames. |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Image Data | Pointed by Content Header | Varies | 1 | One continuous block of image data that is read nibble-by-nibble. |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Palette Data | Pointed by Content Header | 64 Bytes | 16 | A block of palette data that is separated into 16 palettes, each with 16 colors of 4 bytes each. |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Content Header | Pointed by SIR0 Header | 32 Bytes | 1 | Contains the pointers to Animation Data, Image Data, Palette Data, and the number of animations. |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Pointer Offsets List | Pointed by SIR0 Header | 1 Byte | Varies | Details in the SIR0 documentation |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| SIR0 Padding | After Pointer Offsets List | Varies | --- | Details in the SIR0 documentation |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+


Content Header
~~~~~~~~~~~~~~

The 32-byte header appears to be split into 8 sections, each with 4 bytes:

1. Unknown
2. Pointer to start of Animation Data
3. Number of animations
4. Pointer to image data
5. Unknown
6. Pointer to palette data
7. Unknown
8. Unknown

Animation Data
~~~~~~~~~~~~~~

This block contains an array of elements, each 12 bytes and representing an animation. Contains 7 elements:

AA BB CC CC DD DD 00 00 EE EE FF FF

A: Width of all frames in this animation, in blocks (8 pixels)
B: Height of all frames in this animation, in blocks (8 pixels)
C: Unknown
D: The offset, in bytes, from which to start reading from the image data.
E. The number of frames in this animation.
F. Unknown. Possibly a mapping table for the destination of where to load in memory?

A fully zeroed out animation exists as the first element.
22 changes: 22 additions & 0 deletions skytemple_files/graphics/sma/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2020-2024 Capypara and the SkyTemple Contributors
#
# This file is part of SkyTemple.
#
# SkyTemple is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SkyTemple is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SkyTemple. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations

BANNER_FONT_ENTRY_LEN = 0x8
BANNER_FONT_DATA_LEN = 576
BANNER_FONT_SIZE = 24
40 changes: 40 additions & 0 deletions skytemple_files/graphics/sma/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2020-2024 Capypara and the SkyTemple Contributors
#
# This file is part of SkyTemple.
#
# SkyTemple is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SkyTemple is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SkyTemple. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations

from skytemple_files.common.types.data_handler import DataHandler
from skytemple_files.common.util import OptionalKwargs
from skytemple_files.graphics.sma.model import SmaFile
from skytemple_files.graphics.sma.sheets import ExportSheets


class SmaHandler(DataHandler[SmaFile]):
@classmethod
def deserialize(cls, data: bytes, **kwargs: OptionalKwargs) -> SmaFile:
from skytemple_files.common.types.file_types import FileType

return FileType.SIR0.unwrap_obj(FileType.SIR0.deserialize(data), SmaFile) # type: ignore

@classmethod
def serialize(cls, data: SmaFile, **kwargs: OptionalKwargs) -> bytes:
from skytemple_files.common.types.file_types import FileType

return FileType.SIR0.serialize(FileType.SIR0.wrap_obj(data)) # type: ignore

@classmethod
def export_sheets(cls, out_dir: str, sma: SmaFile, palette_idx: int) -> None:
return ExportSheets(out_dir, sma, palette_idx) # type: ignore
116 changes: 116 additions & 0 deletions skytemple_files/graphics/sma/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright 2020-2024 Capypara and the SkyTemple Contributors
#
# This file is part of SkyTemple.
#
# SkyTemple is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SkyTemple is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SkyTemple. If not, see <https://www.gnu.org/licenses/>.
# mypy: ignore-errors
from __future__ import annotations

from range_typed_integers import u32

from io import BytesIO

from skytemple_files.container.sir0.sir0_serializable import Sir0Serializable

TEX_SIZE = 8

DEBUG_PRINT = False


class SmaFile(Sir0Serializable):
def __init__(self, data: bytes | None = None, header_pnt: int = 0):
if data is None:
self.imgData = None
self.animData = None
self.customPalette = None
else:
self.ImportSma(data, header_pnt)

@classmethod
def sir0_unwrap(
cls,
content_data: bytes,
data_pointer: int,
) -> Sir0Serializable:
return cls(content_data, data_pointer)

def sir0_serialize_parts(self) -> tuple[bytes, list[u32], u32 | None]:
raise NotImplementedError("Serialization not currently supported.")

def ImportSma(self, data, ptrSMA=0):
in_file = BytesIO()
in_file.write(data)
in_file.seek(0)

##Read SMA header: ptr to AnimData, ptr to ImgData, PaletteData
in_file.seek(ptrSMA)
updateUnusedStats([], "Unk#1", int.from_bytes(in_file.read(4), "little"))
ptrAnimData = int.from_bytes(in_file.read(4), "little")
nbFrames = int.from_bytes(in_file.read(4), "little")
ptrImgData = int.from_bytes(in_file.read(4), "little")
updateUnusedStats([], "Unk#2", int.from_bytes(in_file.read(4), "little"))
ptrPaletteDataBlock = int.from_bytes(in_file.read(4), "little")
updateUnusedStats([], "Unk#3", int.from_bytes(in_file.read(4), "little"))
updateUnusedStats([], "Unk#4", int.from_bytes(in_file.read(4), "little"))

##Read palette info
nbColorsPerRow = 16
in_file.seek(ptrPaletteDataBlock)
totalColors = (ptrSMA - ptrPaletteDataBlock) // 4
totalPalettes = totalColors // nbColorsPerRow
self.customPalette = []
for ii in range(totalPalettes):
palette = []
for jj in range(nbColorsPerRow):
red = int.from_bytes(in_file.read(1), "little")
blue = int.from_bytes(in_file.read(1), "little")
green = int.from_bytes(in_file.read(1), "little")
in_file.read(1)
palette.append((red, blue, green, 255))
self.customPalette.append(palette)

##read image data
self.imgData = []
in_file.seek(ptrImgData)
while in_file.tell() < ptrPaletteDataBlock:
px = int.from_bytes(in_file.read(1), "little")
self.imgData.append(px % 16)
self.imgData.append(px // 16)

self.animData = []
in_file.seek(ptrAnimData)
for ii in range(nbFrames):
blockX = int.from_bytes(in_file.read(1), "little")
blockY = int.from_bytes(in_file.read(1), "little")
updateUnusedStats([], "Unk#5", int.from_bytes(in_file.read(2), "little"))
byteOffset = int.from_bytes(in_file.read(2), "little")
in_file.read(2)
blockLength = int.from_bytes(in_file.read(2), "little")
updateUnusedStats([], "Unk#6", int.from_bytes(in_file.read(2), "little"))
anim = SmaAnim(blockX, blockY, byteOffset, blockLength)
self.animData.append(anim)


class SmaAnim(object):
def __init__(self, blockWidth, blockHeight, byteOffset, frameCount):
self.blockWidth = blockWidth
self.blockHeight = blockHeight
self.byteOffset = byteOffset
self.frameCount = frameCount


def updateUnusedStats(log_params, name, val):
# stats.append([log_params[0], log_params[1], name, log_params[2:], val])
if DEBUG_PRINT and val != 0:
print(" " + name + ":" + str(val))
89 changes: 89 additions & 0 deletions skytemple_files/graphics/sma/sheets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2020-2024 Capypara and the SkyTemple Contributors
#
# This file is part of SkyTemple.
#
# SkyTemple is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SkyTemple is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SkyTemple. If not, see <https://www.gnu.org/licenses/>.
# mypy: ignore-errors

from __future__ import annotations

import os

from PIL import Image

from skytemple_files.graphics.sma.model import TEX_SIZE


def ExportSheets(outDir, effectData, paletteIndex):
if not os.path.isdir(outDir):
os.makedirs(outDir)

for anim_idx, statusEffectAnim in enumerate(effectData.animData):
if statusEffectAnim.blockWidth == 0:
continue
frames = []
for idx in range(statusEffectAnim.frameCount):
frameImg = GenerateStatusFrame(
effectData.imgData,
effectData.customPalette,
paletteIndex,
statusEffectAnim.byteOffset,
idx,
statusEffectAnim.blockWidth,
statusEffectAnim.blockHeight,
)
frames.append(frameImg)
animImg = CombineFramesIntoAnim(frames)
animImg.save(os.path.join(outDir, "A-" + format(anim_idx, "02d") + "-" + format(paletteIndex, "02d") + ".png"))


def GenerateStatusFrame(imgData, inPalette, paletteIndex, byteOffset, idx, width, height):
##creates a tex piece out of the imgdata, with the specified piece index and dimensions
newImg = Image.new("RGBA", (width * TEX_SIZE, height * TEX_SIZE), (0, 0, 0, 0))
datas = [(0, 0, 0, 0)] * (width * TEX_SIZE * height * TEX_SIZE)

lengthPixels = TEX_SIZE * TEX_SIZE * width * height
imgPx = []
# flatten the list to include all strips
for nn in range(lengthPixels):
imgPx.append(imgData[byteOffset * 2 + idx * lengthPixels + nn])

for yy in range(height):
for xx in range(width):
blockIndex = yy * width + xx
texPosition = blockIndex * TEX_SIZE * TEX_SIZE

for py in range(TEX_SIZE):
for px in range(TEX_SIZE):
paletteElement = imgPx[texPosition + py * TEX_SIZE + px]
##print('palette:' + str(paletteIndex) + ' element:' + str(paletteElement))
if paletteElement == 0:
color = (0, 0, 0, 0)
else:
color = inPalette[paletteIndex][paletteElement]

imgPosition = (xx * TEX_SIZE + px, yy * TEX_SIZE + py)
datas[imgPosition[1] * width * TEX_SIZE + imgPosition[0]] = color
newImg.putdata(datas)
return newImg


def CombineFramesIntoAnim(img_list):
##combines all frames into a horizontal animation sheet
##ASSUMES ALL IMGS ARE THE SAME SIZE
size = img_list[0].size
imgNew = Image.new("RGBA", (size[0] * len(img_list), size[1]), (0, 0, 0, 0))
for img_index in range(len(img_list)):
imgNew.paste(img_list[img_index], (size[0] * img_index, 0), img_list[img_index])
return imgNew

0 comments on commit 0ac0a2e

Please sign in to comment.