Skip to content

Commit eb27d05

Browse files
committed
Fixed MG2 palette
1 parent bbe7e05 commit eb27d05

5 files changed

Lines changed: 125 additions & 5 deletions

File tree

src/mgtools/mg2/mappings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
DATA_TYPE_MAP = {}
44

5+
# Palette configuration for MG2 sprite sections.
6+
# Section 24 uses two large blocks: block 39 at index 0 (covers 0-90)
7+
# and block 37 at index 96 (covers 96-206).
8+
MG2_PALETTE_SECTION_24_BLOCKS: list[tuple[int, int]] = [(39, 0), (37, 96)]
9+
10+
# Sections 25-45 each use a single palette block whose colours start at
11+
# this offset within the 256-entry palette.
12+
MG2_PALETTE_SPRITE_OFFSET: int = 192
13+
514
FILE_TYPE_MAP = {
615
2: FileType.LOCALE,
716
24: FileType.SPRITE,

src/mgtools/resource/assets/palette.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ def palette_bytes(self) -> bytes:
4141

4242
return palette
4343

44+
@property
45+
def block_count(self) -> int:
46+
return len(self.__color_blocks)
47+
48+
def get_block_size(self, block_index: int) -> int:
49+
return len(self.__color_blocks[block_index])
50+
51+
def build_palette_bytes(self, block_offsets: list[tuple[int, int]]) -> bytes:
52+
"""Build a 256-color RGB palette from specific blocks at given offsets.
53+
54+
Args:
55+
block_offsets: List of (block_index, start_offset) pairs.
56+
Each block's colors are placed starting at start_offset
57+
in the 256-entry palette.
58+
59+
Returns:
60+
768 bytes of RGB palette data (256 * 3).
61+
"""
62+
palette: list[tuple[int, int, int]] = [(0, 0, 0)] * 256
63+
64+
for block_index, start_offset in block_offsets:
65+
for i, color in enumerate(self.__color_blocks[block_index]):
66+
idx = start_offset + i
67+
if 0 <= idx < 256:
68+
palette[idx] = color
69+
70+
result = b""
71+
for r, g, b in palette:
72+
result += bytes([r, g, b])
73+
return result
74+
4475
def __init__(self, data_type: DataType) -> None:
4576
super().__init__(data_type)
4677
self.__colors: list[tuple[int, int, int]] = []

src/mgtools/resource/assets/sprite.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def add_data(self, **kwargs) -> None:
105105

106106
self.__variants.append(decode_sprite_entry(reader.read(data_length)))
107107

108-
def add_palette(self, palette: Palette) -> None:
108+
def add_palette(self, palette: "Palette | bytes") -> None:
109109
apply_palette(self.__variants, palette)
110110

111111
def export(self, output_path: Path, **kwargs) -> None:

src/mgtools/resource/file.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,11 @@ def unpack(self, output_dir: Path, section_index: int, **kwargs) -> None:
219219
if isinstance(file, (Sprite, FramedSprite)):
220220
for section in self.__sections:
221221
if isinstance(section, Palette):
222-
file.add_palette(section)
222+
if self.__game == Game.MG2:
223+
palette_bytes = self.__build_mg2_palette(section_index, section)
224+
file.add_palette(palette_bytes)
225+
else:
226+
file.add_palette(section)
223227
break
224228

225229
file_type = file_type_map.get(section_index, FileType.UNKNOWN)
@@ -243,6 +247,79 @@ def __get_file_type_map(self) -> dict:
243247
return MG2_FILE_TYPE_MAP
244248
raise ValueError(f"Unsupported game: {self.__game}")
245249

250+
def __build_mg2_palette(self, section_index: int, palette: Palette) -> bytes:
251+
"""Build a 256-color palette for a specific MG2 sprite section.
252+
253+
Section 24 uses a combined palette from two large blocks covering
254+
indices 0-90 and 96-206. Sections 25-45 each use a single block
255+
whose colors start at index 192.
256+
"""
257+
from mgtools.mg2.mappings import (
258+
MG2_PALETTE_SECTION_24_BLOCKS,
259+
MG2_PALETTE_SPRITE_OFFSET,
260+
)
261+
262+
if section_index == 24:
263+
return palette.build_palette_bytes(MG2_PALETTE_SECTION_24_BLOCKS)
264+
265+
# Sections 25-45: find a palette block whose size matches the
266+
# number of contiguous colour indices this section uses at 192+.
267+
file_type_map = self.__get_file_type_map()
268+
sprite_sections = sorted(
269+
idx
270+
for idx, ft in file_type_map.items()
271+
if ft == FileType.SPRITE and idx != 24
272+
)
273+
274+
# Determine how many colours this section needs
275+
section_file = self.__sections[section_index]
276+
needed = 0
277+
if hasattr(section_file, "_Sprite__variants"):
278+
all_unique: set[int] = set()
279+
for v in section_file._Sprite__variants: # type: ignore[attr-defined]
280+
all_unique.update(v.tobytes())
281+
needed = len([x for x in all_unique if x >= MG2_PALETTE_SPRITE_OFFSET])
282+
283+
# Reserve large blocks (section-24 group)
284+
used: set[int] = set()
285+
for bi, _ in MG2_PALETTE_SECTION_24_BLOCKS:
286+
used.add(bi)
287+
# Also reserve all blocks whose first colour is magenta (255,0,255)
288+
# — these are section-24 palette components.
289+
for bi in range(palette.block_count):
290+
block_colors = palette._Palette__color_blocks[bi] # type: ignore[attr-defined]
291+
if block_colors and block_colors[0] == (255, 0, 255):
292+
used.add(bi)
293+
294+
# Greedy first-fit: iterate over sprite sections in order (25-45).
295+
# For each section, consume the first unused block with matching size.
296+
assigned: dict[int, int] = {}
297+
for sec in sprite_sections:
298+
sec_file = self.__sections[sec]
299+
sec_needed = 0
300+
if hasattr(sec_file, "_Sprite__variants"):
301+
sec_unique: set[int] = set()
302+
for v in sec_file._Sprite__variants: # type: ignore[attr-defined]
303+
sec_unique.update(v.tobytes())
304+
sec_needed = len(
305+
[x for x in sec_unique if x >= MG2_PALETTE_SPRITE_OFFSET]
306+
)
307+
for bi in range(palette.block_count):
308+
if bi in used:
309+
continue
310+
if palette.get_block_size(bi) == sec_needed:
311+
assigned[sec] = bi
312+
used.add(bi)
313+
break
314+
315+
if section_index in assigned:
316+
return palette.build_palette_bytes(
317+
[(assigned[section_index], MG2_PALETTE_SPRITE_OFFSET)]
318+
)
319+
320+
# Fallback: use the flat palette
321+
return palette.palette_bytes
322+
246323
def add_from_folder(self, input_dir: Path, section_index: int, **kwargs) -> None:
247324
if self.__game == Game.MG1:
248325
default_data_type = DataType.MG1_SECTION

src/mgtools/utilities/sprite.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ def decode_sprite_entry(data: bytes) -> Image.Image:
3434
return Image.frombytes("P", (width, height), pixel_data)
3535

3636

37-
def apply_palette(images: list[Image.Image], palette: Palette) -> None:
38-
"""Apply a Palette to every image in the list."""
39-
palette_bytes = palette.palette_bytes
37+
def apply_palette(images: list[Image.Image], palette: "Palette | bytes") -> None:
38+
"""Apply a Palette (or raw palette bytes) to every image in the list."""
39+
if isinstance(palette, bytes):
40+
palette_bytes = palette
41+
else:
42+
palette_bytes = palette.palette_bytes
4043

4144
for image in images:
4245
image.putpalette(palette_bytes)

0 commit comments

Comments
 (0)