Skip to content

Commit

Permalink
Screen effect support (#551)
Browse files Browse the repository at this point in the history
* parser for screen effect

* screen effect documentation

* ruff

* linting fix

* update readme

---------

Co-authored-by: Marco Köpcke <[email protected]>
  • Loading branch information
audinowho and theCapypara authored Dec 20, 2024
1 parent d7163a5 commit 7af86f4
Show file tree
Hide file tree
Showing 6 changed files with 399 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 @@ -79,6 +79,7 @@
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.effect_screen.handler import ScreenEffectHandler
from skytemple_files.graphics.wan_wat.handler import WanHandler
from skytemple_files.graphics.wte.handler import WteHandler
from skytemple_files.graphics.wtu.handler import WtuHandler
Expand Down Expand Up @@ -148,6 +149,7 @@ class FileType:
WTE = WteHandler
WTU = WtuHandler

SCREEN_FX = ScreenEffectHandler
WAN = WanHandler
WAT = WanHandler

Expand Down
87 changes: 87 additions & 0 deletions skytemple_files/graphics/effect_screen/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
Screen Effect File Format
===============
The file ``/EFFECT/effect.bin`` contains vfx used for dungeon battle, weather effects, and some ground mode vfx.

The file contains 293 effect entries, most of which are WAN format.
Specifically the files effect0268-00289 are not WAN.
They are used for screen effects in moves and cutscenes.


The file uses SIR0 headers to store its pointers. General SIR0 details can be found in the main SIR0 documentation.

+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Name | Offset | Size (Per Element) | # of Elements | Description |
+=======================+=============================+=====================+==============================+==================================================================================================+
| SIR0 Header | 0x00 | 16 Bytes | 1 | Details in the SIR0 documentation |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Animation Data | Pointed by Animation Ptrs | Varies | Specified by Content Header | A 36-byte header with draw parameters, plus a list of draw instructions to put textures onscreen |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Animation Pointers | Pointed by Content Header | 4 Bytes | Specified by Content Header | List of pointers to each frame of Animation Data |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| 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. |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| Image Data | Pointed by Content Header | Varies | 1 | One continuous block of image data that is read nibble-by-nibble. |
+-----------------------+-----------------------------+---------------------+------------------------------+--------------------------------------------------------------------------------------------------+
| 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 6 sections, each with 4 bytes:

1. Number of frames in the animation
2. Pointer to start of Animation Data
3. Unknown
4. Pointer to image data
5. Pointer to palette data
6. Unknown

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

Represents one frame of screen animation per element.
It is always a header of 36 bytes, followed by a variable number of 2-byte draw instructions.
This frame of animation is drawn tile-by-tile: Starting from the top-left, row by row, then column by column.
Each tile is 8x8 texture. First the number of tiles to draw are specified by the header (rows x columns)
Then, by reading each two-byte value and interpreting it as either a draw instruction that advances one tile,
or a skip instruction that advances the specified number of tiles.
Once all tiles (rows x columns) have been traversed, the frame has finished drawing and will not be read any further.

Header
------

The header 36 bytes is split up into the following regions:

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

A: Always a multiple of section A
B: Always a multiple of section D
C: Number of textures per row.
D: Number of textures per column.
E: Frame duration in 1/60th of a second.
F: Transparency.
G: Unknown. One of various possible numbers: 0x00,0x40,0x60,0x7F,0x80,0xC0,0xF0,0xFF

Draw Instructions
-----------------

Two bytes that tell the game how to draw a single 8x8 texture in the current position, or how many tiles to skip.
The draw instruction is a little endian, 16 bit value made up of 4 parts.

AAA0 BCDD DDDD DDDD

A: If 1, draw the texture specified in D. If 0, skip a number of tiles specified in D.
B: Flip the source image on Y axis before drawing
C: Flip the source image on X axis before drawing
D: Draw value. If selected as a tile to draw, interpret this as the point in imgData to start reading an 8x8 texture.


Credits
-------
Thanks to BitDrifter for experimenting and deducing Header and Draw Instructions.
22 changes: 22 additions & 0 deletions skytemple_files/graphics/effect_screen/__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/effect_screen/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.effect_screen.model import ScreenEffectFile
from skytemple_files.graphics.effect_screen.sheets import ExportSheets


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

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

@classmethod
def serialize(cls, data: ScreenEffectFile, **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, screen_effect: ScreenEffectFile, include_alpha: bool) -> None:
return ExportSheets(out_dir, screen_effect, include_alpha) # type: ignore
166 changes: 166 additions & 0 deletions skytemple_files/graphics/effect_screen/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# 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
SCREEN_ATTR_DrawMask = 0x8000 # 1000 0000 0000 0000
SCREEN_ATTR_FlipYMask = 0x0800 # 0000 1000 0000 0000
SCREEN_ATTR_FlipXMask = 0x0400 # 0000 0100 0000 0000
SCREEN_ATTR_ValueMask = 0x03FF # 0000 0011 1111 1111

DEBUG_PRINT = False


class ScreenEffectFile(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.ImportScreenEffect(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 ImportScreenEffect(self, data, ptrEffect=0):
in_file = BytesIO()
in_file.write(data)
in_file.seek(0)

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

##Read palette info
nbColorsPerRow = 16
in_file.seek(ptrPaletteDataBlock)
totalColors = (ptrImgData - 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() < ptrEffect:
px = int.from_bytes(in_file.read(1), "little")
self.imgData.append(px % 16)
self.imgData.append(px // 16)

ptrFrames = []
in_file.seek(ptrAnimData)
for idx in range(nbFrames):
##read the location
ptrFrame = int.from_bytes(in_file.read(4), "little")
ptrFrames.append(ptrFrame)

self.animData = []
for frame_idx, ptrFrame in enumerate(ptrFrames):
in_file.seek(ptrFrame)

updateUnusedStats([], "Unk#5", int.from_bytes(in_file.read(2), "little"))
updateUnusedStats([], "Unk#7", int.from_bytes(in_file.read(2), "little"))

# Must be 0x21 or else the animation doesn't play
updateUnusedStats([], "Unk#6", int.from_bytes(in_file.read(2), "little"))
row_height = int.from_bytes(in_file.read(2), "little")
frame_dur = int.from_bytes(in_file.read(2), "little")
in_file.read(18)
alpha = int.from_bytes(in_file.read(2), "little")
in_file.read(3)
updateUnusedStats([], "Unk#4", int.from_bytes(in_file.read(1), "little"))
in_file.read(2)

pieces = []
totalSlots = 0
while True:
drawValue = int.from_bytes(in_file.read(2), "little")
skip = (SCREEN_ATTR_DrawMask & drawValue) == 0
flipX = (SCREEN_ATTR_FlipXMask & drawValue) != 0
flipY = (SCREEN_ATTR_FlipYMask & drawValue) != 0
drawArg = SCREEN_ATTR_ValueMask & drawValue

pieces.append(ScreenPiece(drawArg, flipX, flipY, skip))
if skip:
totalSlots += drawArg
else:
totalSlots += 1

if totalSlots >= row_height * 33:
break

end_ptr = ptrAnimData
if frame_idx < len(ptrFrames) - 1:
end_ptr = ptrFrames[frame_idx + 1]

cur_pos = in_file.tell()
if cur_pos != end_ptr and cur_pos != end_ptr - 2:
raise Exception()

self.animData.append(ScreenFrame(frame_dur, alpha, row_height, pieces))


class ScreenFrame(object):
def __init__(self, duration, alpha, rowHeight, pieces):
self.duration = duration
self.alpha = alpha
self.rowHeight = rowHeight
self.pieces = pieces


class ScreenPiece(object):
def __init__(self, index, flipX, flipY, skip):
self.index = index
self.flipX = flipX
self.flipY = flipY
self.skip = skip


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))
Loading

0 comments on commit 7af86f4

Please sign in to comment.