Skip to content

Commit c0f56a1

Browse files
authored
Merge pull request #243 from MetroidAdvRandomizerSystem/zm-palettes
[ZM] Random palettes
2 parents 1589604 + bf4d5f8 commit c0f56a1

6 files changed

Lines changed: 83 additions & 65 deletions

File tree

src/mars_patcher/constants/game_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def samus_palettes(rom: Rom) -> list[tuple[int, int]]:
224224
elif rom.region == Region.C:
225225
return [(0x2900C8, 0x5E), (0x290E48, 0x70), (0x56CC68, 3)]
226226
elif rom.game == Game.ZM:
227-
addr = rom.read_ptr(ReservedPointersZM.AREA_DOORS_PTR.value)
227+
addr = rom.read_ptr(ReservedPointersZM.SAMUS_PALETTES_PTR.value)
228228
return [(addr, 0xA3)]
229229
raise ValueError(rom.game, rom.region)
230230

src/mars_patcher/palette.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
from mars_patcher.convert_array import u16_to_u8
66
from mars_patcher.rom import Rom
77

8+
PAL_ROW_COUNT = 16
9+
"""The number of colors in a palette row."""
10+
PAL_ROW_SIZE = PAL_ROW_COUNT * 2
11+
"""The byte size of a palette row."""
12+
813
HUE_VARIATION_RANGE = 180.0
914
"""The maximum range that hue can be additionally rotated."""
1015

1116

1217
class SineWave:
13-
STEP = (2 * math.pi) / 16
18+
STEP = (2 * math.pi) / PAL_ROW_COUNT
1419

1520
def __init__(self, amplitude: float, frequency: float, phase: float):
1621
self.amplitude = amplitude
@@ -36,7 +41,7 @@ def generate(max_range: float) -> "SineWave":
3641
return SineWave(amplitude, frequency, phase)
3742

3843
def calculate_variation(self, x: int) -> float:
39-
assert 0 <= x < 16
44+
assert 0 <= x < PAL_ROW_COUNT
4045
return self.amplitude * math.sin(self.frequency * x * self.STEP + self.phase)
4146

4247

@@ -69,7 +74,7 @@ class Palette:
6974
def __init__(self, rows: int, rom: Rom, addr: int):
7075
assert rows >= 1
7176
self.colors: list[RgbColor] = []
72-
for i in range(rows * 16):
77+
for i in range(rows * PAL_ROW_COUNT):
7378
rgb = rom.read_16(addr + i * 2)
7479
color = RgbColor.from_rgb(rgb, RgbBitSize.Rgb5)
7580
self.colors.append(color)
@@ -78,7 +83,7 @@ def __getitem__(self, key: int) -> RgbColor:
7883
return self.colors[key]
7984

8085
def rows(self) -> int:
81-
return len(self.colors) // 16
86+
return len(self.colors) // PAL_ROW_COUNT
8287

8388
def byte_data(self) -> bytes:
8489
values = [c.rgb_15() for c in self.colors]
@@ -95,8 +100,8 @@ def change_colors_hsv(self, change: ColorChange, excluded_rows: set[int]) -> Non
95100
for row in range(self.rows()):
96101
if row in excluded_rows:
97102
continue
98-
offset = row * 16
99-
for i in range(16):
103+
offset = row * PAL_ROW_COUNT
104+
for i in range(PAL_ROW_COUNT):
100105
# Skip black and white
101106
rgb = self.colors[offset + i]
102107
if rgb == black or rgb == white:
@@ -117,8 +122,8 @@ def change_colors_oklab(self, change: ColorChange, excluded_rows: set[int]) -> N
117122
for row in range(self.rows()):
118123
if row in excluded_rows:
119124
continue
120-
offset = row * 16
121-
for i in range(16):
125+
offset = row * PAL_ROW_COUNT
126+
for i in range(PAL_ROW_COUNT):
122127
rgb = self.colors[offset + i]
123128
lab = change.change_oklab(rgb.oklab(), i)
124129
self.colors[offset + i] = lab.rgb()

src/mars_patcher/random_palettes.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import random
2-
from enum import Enum
2+
from enum import Enum, auto
33
from typing import TypeAlias
44

55
from typing_extensions import Self
@@ -19,20 +19,30 @@
1919
TILESET_ANIM_PALS,
2020
)
2121
from mars_patcher.mf.constants.sprites import SpriteIdMF
22-
from mars_patcher.palette import ColorChange, Palette, SineWave
23-
from mars_patcher.rom import Game, Rom
22+
from mars_patcher.palette import PAL_ROW_SIZE, ColorChange, Palette, SineWave
23+
from mars_patcher.rom import Rom
24+
from mars_patcher.tileset import Tileset
25+
from mars_patcher.zm.auto_generated_types import (
26+
MarsschemazmPalettes,
27+
MarsschemazmPalettesColorspace,
28+
MarsschemazmPalettesRandomize,
29+
)
2430
from mars_patcher.zm.constants.game_data import statues_cutscene_palette_addr
2531
from mars_patcher.zm.constants.palettes import ENEMY_GROUPS_ZM, EXCLUDED_ENEMIES_ZM
2632
from mars_patcher.zm.constants.sprites import SpriteIdZM
2733

34+
SchemaPalettes = MarsschemamfPalettes | MarsschemazmPalettes
35+
SchemaPalettesColorspace = MarsschemamfPalettesColorspace | MarsschemazmPalettesColorspace
36+
SchemaPalettesRandomize = MarsschemamfPalettesRandomize | MarsschemazmPalettesRandomize
37+
2838
HueRange: TypeAlias = tuple[int, int]
2939

3040

3141
class PaletteType(Enum):
32-
TILESETS = 1
33-
ENEMIES = 2
34-
SAMUS = 3
35-
BEAMS = 4
42+
TILESETS = auto()
43+
ENEMIES = auto()
44+
SAMUS = auto()
45+
BEAMS = auto()
3646

3747

3848
class PaletteSettings:
@@ -47,18 +57,18 @@ def __init__(
4757
self,
4858
seed: int,
4959
pal_types: dict[PaletteType, HueRange],
50-
color_space: MarsschemamfPalettesColorspace,
60+
color_space: SchemaPalettesColorspace,
5161
symmetric: bool,
5262
extra_variation: bool,
5363
):
5464
self.seed = seed
5565
self.pal_types = pal_types
56-
self.color_space: MarsschemamfPalettesColorspace = color_space
66+
self.color_space: SchemaPalettesColorspace = color_space
5767
self.symmetric = symmetric
5868
self.extra_variation = extra_variation
5969

6070
@classmethod
61-
def from_json(cls, data: MarsschemamfPalettes) -> Self:
71+
def from_json(cls, data: SchemaPalettes) -> Self:
6272
seed = data.get("Seed", random.randint(0, 2**31 - 1))
6373
random.seed(seed)
6474
pal_types = {}
@@ -72,7 +82,7 @@ def from_json(cls, data: MarsschemamfPalettes) -> Self:
7282
return cls(seed, pal_types, color_space, symmetric, True)
7383

7484
@classmethod
75-
def get_hue_range(cls, data: MarsschemamfPalettesRandomize) -> HueRange:
85+
def get_hue_range(cls, data: SchemaPalettesRandomize) -> HueRange:
7686
hue_min = data.get("HueMin")
7787
hue_max = data.get("HueMax")
7888
if hue_min is None or hue_max is None:
@@ -141,8 +151,8 @@ def randomize(self) -> None:
141151
if PaletteType.BEAMS in pal_types:
142152
self.randomize_beams(pal_types[PaletteType.BEAMS])
143153
# Fix any sprite/tileset palettes that should be the same
144-
# if self.rom.is_zm():
145-
# self.fix_zm_palettes()
154+
if self.rom.is_zm():
155+
self.fix_zm_palettes()
146156

147157
def change_palettes(self, pals: list[tuple[int, int]], change: ColorChange) -> None:
148158
for addr, rows in pals:
@@ -157,7 +167,8 @@ def randomize_samus(self, hue_range: HueRange) -> None:
157167
change = self.generate_palette_change(hue_range)
158168
self.change_palettes(gd.samus_palettes(self.rom), change)
159169
self.change_palettes(gd.helmet_cursor_palettes(self.rom), change)
160-
self.change_palettes(sax_palettes(self.rom), change)
170+
if self.rom.is_mf():
171+
self.change_palettes(sax_palettes(self.rom), change)
161172

162173
def randomize_beams(self, hue_range: HueRange) -> None:
163174
change = self.generate_palette_change(hue_range)
@@ -179,7 +190,7 @@ def randomize_tilesets(self, hue_range: HueRange) -> None:
179190
continue
180191
# Get excluded palette rows
181192
excluded_rows = set()
182-
if rom.game == Game.MF:
193+
if rom.is_mf():
183194
row = MF_TILESET_ALT_PAL_ROWS.get(pal_addr)
184195
if row is not None:
185196
excluded_rows = {row}
@@ -223,6 +234,7 @@ def randomize_enemies(self, hue_range: HueRange) -> None:
223234
raise ValueError(rom.game)
224235
excluded = {en_id.value for en_id in _excluded}
225236
sp_count = gd.sprite_count(rom)
237+
# The first 0x10 sprites have no graphics
226238
to_randomize = set(range(0x10, sp_count))
227239
to_randomize -= excluded
228240

@@ -287,9 +299,9 @@ def get_sprite_addr(self, sprite_id: int) -> int:
287299
addr = gd.sprite_palette_ptrs(self.rom) + (sprite_id - 0x10) * 4
288300
return self.rom.read_ptr(addr)
289301

290-
def get_tileset_addr(self, sprite_id: int) -> int:
291-
addr = gd.tileset_entries(self.rom) + sprite_id * 0x14 + 4
292-
return self.rom.read_ptr(addr)
302+
def get_tileset_addr(self, tileset_id: int) -> int:
303+
tileset = Tileset(self.rom, tileset_id)
304+
return tileset.palette_addr()
293305

294306
def fix_nettori(self, change: ColorChange) -> None:
295307
"""Nettori has extra palettes stored separately, so they require the same color change."""
@@ -303,25 +315,25 @@ def fix_zm_palettes(self) -> None:
303315
PaletteType.ENEMIES in self.settings.pal_types
304316
or PaletteType.TILESETS in self.settings.pal_types
305317
):
306-
# Fix kraid's body
318+
# Fix kraid's body (copy row from sprite to tileset)
307319
sp_addr = self.get_sprite_addr(SpriteIdZM.KRAID)
308320
ts_addr = self.get_tileset_addr(9)
309-
self.rom.copy_bytes(sp_addr, ts_addr + 0x100, 0x20)
321+
self.rom.copy_bytes(sp_addr, ts_addr + (8 * PAL_ROW_SIZE), PAL_ROW_SIZE)
310322

311323
if PaletteType.TILESETS in self.settings.pal_types:
312324
# Fix kraid elevator statue
313325
sp_addr = self.get_sprite_addr(SpriteIdZM.KRAID_ELEVATOR_STATUE)
314326
ts_addr = self.get_tileset_addr(0x35)
315-
self.rom.copy_bytes(ts_addr + 0x20, sp_addr, 0x20)
327+
self.rom.copy_bytes(ts_addr + PAL_ROW_SIZE, sp_addr, PAL_ROW_SIZE)
316328

317329
# Fix ridley elevator statue
318330
ts_addr = self.get_tileset_addr(7)
319-
self.rom.copy_bytes(ts_addr + 0x20, sp_addr + 0x20, 0x20)
331+
self.rom.copy_bytes(ts_addr + PAL_ROW_SIZE, sp_addr + PAL_ROW_SIZE, PAL_ROW_SIZE)
320332

321333
# Fix tourian statues
322334
sp_addr = self.get_sprite_addr(SpriteIdZM.KRAID_STATUE)
323335
ts_addr = self.get_tileset_addr(0x41)
324-
self.rom.copy_bytes(ts_addr + 0x60, sp_addr, 0x20)
336+
self.rom.copy_bytes(ts_addr + (3 * PAL_ROW_SIZE), sp_addr, PAL_ROW_SIZE)
325337
# Fix cutscene
326338
sp_addr = statues_cutscene_palette_addr(self.rom)
327-
self.rom.copy_bytes(ts_addr, sp_addr, 0xC0)
339+
self.rom.copy_bytes(ts_addr, sp_addr, 6 * PAL_ROW_SIZE)

src/mars_patcher/zm/auto_generated_types.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -403,42 +403,42 @@ class MarsschemazmDoorLocksItem(typ.TypedDict):
403403
"""The type of cover on the hatch."""
404404

405405
MarsschemazmPalettesRandomizeKey = typ.Literal[
406-
'tilesets',
407-
'enemies',
408-
'samus',
409-
'beams'
406+
'Tilesets',
407+
'Enemies',
408+
'Samus',
409+
'Beams'
410410
]
411411

412412
@typ.final
413413
class MarsschemazmPalettesRandomize(typ.TypedDict, total=False):
414414
"""The range to use for rotating palette hues."""
415415

416-
hue_min: HueRotation = None
416+
HueMin: HueRotation = None
417417
"""The minimum value to use for rotating palette hues. If not specified, the patcher will randomly generate one."""
418418

419-
hue_max: HueRotation = None
419+
HueMax: HueRotation = None
420420
"""The maximum value to use for rotating palette hues. If not specified, the patcher will randomly generate one."""
421421

422422

423-
MarsschemazmPalettesColorSpace = typ.Literal[
423+
MarsschemazmPalettesColorspace = typ.Literal[
424424
'HSV',
425-
'OKLAB'
425+
'Oklab'
426426
]
427427

428428
@typ.final
429429
class MarsschemazmPalettes(typ.TypedDict, total=False):
430430
"""Properties for randomized in-game palettes."""
431431

432-
seed: Seed = None
432+
Seed: Seed = None
433433
"""A number used to initialize the random number generator for palettes. If not specified, the patcher will randomly generate one."""
434434

435-
randomize: typ.Required[dict[MarsschemazmPalettesRandomizeKey, MarsschemazmPalettesRandomize]]
435+
Randomize: typ.Required[dict[MarsschemazmPalettesRandomizeKey, MarsschemazmPalettesRandomize]]
436436
"""What kind of palettes should be randomized."""
437437

438-
color_space: MarsschemazmPalettesColorSpace = 'OKLAB'
438+
ColorSpace: MarsschemazmPalettesColorspace = 'Oklab'
439439
"""The color space to use for rotating palette hues."""
440440

441-
symmetric: bool = True
441+
Symmetric: bool = True
442442
"""Randomly rotates hues in the positive or negative direction true."""
443443

444444

@@ -544,7 +544,7 @@ class Marsschemazm(typ.TypedDict, total=False):
544544
door_locks: list[MarsschemazmDoorLocksItem]
545545
"""List of all lockable doors and their lock type."""
546546

547-
palettes: MarsschemazmPalettes = None
547+
Palettes: MarsschemazmPalettes = None
548548
"""Properties for randomized in-game palettes."""
549549

550550
MusicReplacement: Musicmapping

src/mars_patcher/zm/data/schema.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,37 +307,37 @@
307307
]
308308
}
309309
},
310-
"palettes": {
310+
"Palettes": {
311311
"type": "object",
312312
"description": "Properties for randomized in-game palettes.",
313313
"properties": {
314-
"seed": {
314+
"Seed": {
315315
"$ref": "#/$defs/seed",
316316
"description": "A number used to initialize the random number generator for palettes. If not specified, the patcher will randomly generate one.",
317317
"default": null
318318
},
319-
"randomize": {
319+
"Randomize": {
320320
"type": "object",
321321
"description": "What kind of palettes should be randomized.",
322322
"propertyNames": {
323323
"type": "string",
324324
"enum": [
325-
"tilesets",
326-
"enemies",
327-
"samus",
328-
"beams"
325+
"Tilesets",
326+
"Enemies",
327+
"Samus",
328+
"Beams"
329329
]
330330
},
331331
"additionalProperties": {
332332
"type": "object",
333333
"description": "The range to use for rotating palette hues.",
334334
"properties": {
335-
"hue_min": {
335+
"HueMin": {
336336
"$ref": "#/$defs/hue_rotation",
337337
"description": "The minimum value to use for rotating palette hues. If not specified, the patcher will randomly generate one.",
338338
"default": null
339339
},
340-
"hue_max": {
340+
"HueMax": {
341341
"$ref": "#/$defs/hue_rotation",
342342
"description": "The maximum value to use for rotating palette hues. If not specified, the patcher will randomly generate one.",
343343
"default": null
@@ -346,24 +346,24 @@
346346
"additionalProperties": false
347347
}
348348
},
349-
"color_space": {
349+
"ColorSpace": {
350350
"type": "string",
351351
"description": "The color space to use for rotating palette hues.",
352352
"enum": [
353353
"HSV",
354-
"OKLAB"
354+
"Oklab"
355355
],
356-
"default": "OKLAB"
356+
"default": "Oklab"
357357
},
358-
"symmetric": {
358+
"Symmetric": {
359359
"type": "boolean",
360360
"description": "Randomly rotates hues in the positive or negative direction true.",
361361
"default": true
362362
}
363363
},
364364
"additionalProperties": false,
365365
"required": [
366-
"randomize"
366+
"Randomize"
367367
],
368368
"default": null
369369
},

0 commit comments

Comments
 (0)