Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/mars_patcher/constants/game_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def samus_palettes(rom: Rom) -> list[tuple[int, int]]:
elif rom.region == Region.C:
return [(0x2900C8, 0x5E), (0x290E48, 0x70), (0x56CC68, 3)]
elif rom.game == Game.ZM:
addr = rom.read_ptr(ReservedPointersZM.AREA_DOORS_PTR.value)
addr = rom.read_ptr(ReservedPointersZM.SAMUS_PALETTES_PTR.value)
return [(addr, 0xA3)]
raise ValueError(rom.game, rom.region)

Expand Down
21 changes: 13 additions & 8 deletions src/mars_patcher/palette.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
from mars_patcher.convert_array import u16_to_u8
from mars_patcher.rom import Rom

PAL_ROW_COUNT = 16
"""The number of colors in a palette row."""
PAL_ROW_SIZE = PAL_ROW_COUNT * 2
"""The byte size of a palette row."""

HUE_VARIATION_RANGE = 180.0
"""The maximum range that hue can be additionally rotated."""


class SineWave:
STEP = (2 * math.pi) / 16
STEP = (2 * math.pi) / PAL_ROW_COUNT

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

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


Expand Down Expand Up @@ -69,7 +74,7 @@ class Palette:
def __init__(self, rows: int, rom: Rom, addr: int):
assert rows >= 1
self.colors: list[RgbColor] = []
for i in range(rows * 16):
for i in range(rows * PAL_ROW_COUNT):
rgb = rom.read_16(addr + i * 2)
color = RgbColor.from_rgb(rgb, RgbBitSize.Rgb5)
self.colors.append(color)
Expand All @@ -78,7 +83,7 @@ def __getitem__(self, key: int) -> RgbColor:
return self.colors[key]

def rows(self) -> int:
return len(self.colors) // 16
return len(self.colors) // PAL_ROW_COUNT

def byte_data(self) -> bytes:
values = [c.rgb_15() for c in self.colors]
Expand All @@ -95,8 +100,8 @@ def change_colors_hsv(self, change: ColorChange, excluded_rows: set[int]) -> Non
for row in range(self.rows()):
if row in excluded_rows:
continue
offset = row * 16
for i in range(16):
offset = row * PAL_ROW_COUNT
for i in range(PAL_ROW_COUNT):
# Skip black and white
rgb = self.colors[offset + i]
if rgb == black or rgb == white:
Expand All @@ -117,8 +122,8 @@ def change_colors_oklab(self, change: ColorChange, excluded_rows: set[int]) -> N
for row in range(self.rows()):
if row in excluded_rows:
continue
offset = row * 16
for i in range(16):
offset = row * PAL_ROW_COUNT
for i in range(PAL_ROW_COUNT):
rgb = self.colors[offset + i]
lab = change.change_oklab(rgb.oklab(), i)
self.colors[offset + i] = lab.rgb()
60 changes: 36 additions & 24 deletions src/mars_patcher/random_palettes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import random
from enum import Enum
from enum import Enum, auto
from typing import TypeAlias

from typing_extensions import Self
Expand All @@ -19,20 +19,30 @@
TILESET_ANIM_PALS,
)
from mars_patcher.mf.constants.sprites import SpriteIdMF
from mars_patcher.palette import ColorChange, Palette, SineWave
from mars_patcher.rom import Game, Rom
from mars_patcher.palette import PAL_ROW_SIZE, ColorChange, Palette, SineWave
from mars_patcher.rom import Rom
from mars_patcher.tileset import Tileset
from mars_patcher.zm.auto_generated_types import (
MarsschemazmPalettes,
MarsschemazmPalettesColorspace,
MarsschemazmPalettesRandomize,
)
from mars_patcher.zm.constants.game_data import statues_cutscene_palette_addr
from mars_patcher.zm.constants.palettes import ENEMY_GROUPS_ZM, EXCLUDED_ENEMIES_ZM
from mars_patcher.zm.constants.sprites import SpriteIdZM

SchemaPalettes = MarsschemamfPalettes | MarsschemazmPalettes
SchemaPalettesColorspace = MarsschemamfPalettesColorspace | MarsschemazmPalettesColorspace
SchemaPalettesRandomize = MarsschemamfPalettesRandomize | MarsschemazmPalettesRandomize

HueRange: TypeAlias = tuple[int, int]


class PaletteType(Enum):
TILESETS = 1
ENEMIES = 2
SAMUS = 3
BEAMS = 4
TILESETS = auto()
ENEMIES = auto()
SAMUS = auto()
BEAMS = auto()


class PaletteSettings:
Expand All @@ -47,18 +57,18 @@ def __init__(
self,
seed: int,
pal_types: dict[PaletteType, HueRange],
color_space: MarsschemamfPalettesColorspace,
color_space: SchemaPalettesColorspace,
symmetric: bool,
extra_variation: bool,
):
self.seed = seed
self.pal_types = pal_types
self.color_space: MarsschemamfPalettesColorspace = color_space
self.color_space: SchemaPalettesColorspace = color_space
self.symmetric = symmetric
self.extra_variation = extra_variation

@classmethod
def from_json(cls, data: MarsschemamfPalettes) -> Self:
def from_json(cls, data: SchemaPalettes) -> Self:
seed = data.get("Seed", random.randint(0, 2**31 - 1))
random.seed(seed)
pal_types = {}
Expand All @@ -72,7 +82,7 @@ def from_json(cls, data: MarsschemamfPalettes) -> Self:
return cls(seed, pal_types, color_space, symmetric, True)

@classmethod
def get_hue_range(cls, data: MarsschemamfPalettesRandomize) -> HueRange:
def get_hue_range(cls, data: SchemaPalettesRandomize) -> HueRange:
hue_min = data.get("HueMin")
hue_max = data.get("HueMax")
if hue_min is None or hue_max is None:
Expand Down Expand Up @@ -141,8 +151,8 @@ def randomize(self) -> None:
if PaletteType.BEAMS in pal_types:
self.randomize_beams(pal_types[PaletteType.BEAMS])
# Fix any sprite/tileset palettes that should be the same
# if self.rom.is_zm():
# self.fix_zm_palettes()
if self.rom.is_zm():
self.fix_zm_palettes()

def change_palettes(self, pals: list[tuple[int, int]], change: ColorChange) -> None:
for addr, rows in pals:
Expand All @@ -157,7 +167,8 @@ def randomize_samus(self, hue_range: HueRange) -> None:
change = self.generate_palette_change(hue_range)
self.change_palettes(gd.samus_palettes(self.rom), change)
self.change_palettes(gd.helmet_cursor_palettes(self.rom), change)
self.change_palettes(sax_palettes(self.rom), change)
if self.rom.is_mf():
self.change_palettes(sax_palettes(self.rom), change)

def randomize_beams(self, hue_range: HueRange) -> None:
change = self.generate_palette_change(hue_range)
Expand All @@ -179,7 +190,7 @@ def randomize_tilesets(self, hue_range: HueRange) -> None:
continue
# Get excluded palette rows
excluded_rows = set()
if rom.game == Game.MF:
if rom.is_mf():
row = MF_TILESET_ALT_PAL_ROWS.get(pal_addr)
if row is not None:
excluded_rows = {row}
Expand Down Expand Up @@ -223,6 +234,7 @@ def randomize_enemies(self, hue_range: HueRange) -> None:
raise ValueError(rom.game)
excluded = {en_id.value for en_id in _excluded}
sp_count = gd.sprite_count(rom)
# The first 0x10 sprites have no graphics
to_randomize = set(range(0x10, sp_count))
to_randomize -= excluded

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

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

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

if PaletteType.TILESETS in self.settings.pal_types:
# Fix kraid elevator statue
sp_addr = self.get_sprite_addr(SpriteIdZM.KRAID_ELEVATOR_STATUE)
ts_addr = self.get_tileset_addr(0x35)
self.rom.copy_bytes(ts_addr + 0x20, sp_addr, 0x20)
self.rom.copy_bytes(ts_addr + PAL_ROW_SIZE, sp_addr, PAL_ROW_SIZE)

# Fix ridley elevator statue
ts_addr = self.get_tileset_addr(7)
self.rom.copy_bytes(ts_addr + 0x20, sp_addr + 0x20, 0x20)
self.rom.copy_bytes(ts_addr + PAL_ROW_SIZE, sp_addr + PAL_ROW_SIZE, PAL_ROW_SIZE)

# Fix tourian statues
sp_addr = self.get_sprite_addr(SpriteIdZM.KRAID_STATUE)
ts_addr = self.get_tileset_addr(0x41)
self.rom.copy_bytes(ts_addr + 0x60, sp_addr, 0x20)
self.rom.copy_bytes(ts_addr + (3 * PAL_ROW_SIZE), sp_addr, PAL_ROW_SIZE)
# Fix cutscene
sp_addr = statues_cutscene_palette_addr(self.rom)
self.rom.copy_bytes(ts_addr, sp_addr, 0xC0)
self.rom.copy_bytes(ts_addr, sp_addr, 6 * PAL_ROW_SIZE)
26 changes: 13 additions & 13 deletions src/mars_patcher/zm/auto_generated_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,42 +403,42 @@ class MarsschemazmDoorLocksItem(typ.TypedDict):
"""The type of cover on the hatch."""

MarsschemazmPalettesRandomizeKey = typ.Literal[
'tilesets',
'enemies',
'samus',
'beams'
'Tilesets',
'Enemies',
'Samus',
'Beams'
]

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

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

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


MarsschemazmPalettesColorSpace = typ.Literal[
MarsschemazmPalettesColorspace = typ.Literal[
'HSV',
'OKLAB'
'Oklab'
]

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

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

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

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

symmetric: bool = True
Symmetric: bool = True
"""Randomly rotates hues in the positive or negative direction true."""


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

palettes: MarsschemazmPalettes = None
Palettes: MarsschemazmPalettes = None
"""Properties for randomized in-game palettes."""

MusicReplacement: Musicmapping
Expand Down
28 changes: 14 additions & 14 deletions src/mars_patcher/zm/data/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,37 +307,37 @@
]
}
},
"palettes": {
"Palettes": {
"type": "object",
"description": "Properties for randomized in-game palettes.",
"properties": {
"seed": {
"Seed": {
"$ref": "#/$defs/seed",
"description": "A number used to initialize the random number generator for palettes. If not specified, the patcher will randomly generate one.",
"default": null
},
"randomize": {
"Randomize": {
"type": "object",
"description": "What kind of palettes should be randomized.",
"propertyNames": {
"type": "string",
"enum": [
"tilesets",
"enemies",
"samus",
"beams"
"Tilesets",
"Enemies",
"Samus",
"Beams"
]
},
"additionalProperties": {
"type": "object",
"description": "The range to use for rotating palette hues.",
"properties": {
"hue_min": {
"HueMin": {
"$ref": "#/$defs/hue_rotation",
"description": "The minimum value to use for rotating palette hues. If not specified, the patcher will randomly generate one.",
"default": null
},
"hue_max": {
"HueMax": {
"$ref": "#/$defs/hue_rotation",
"description": "The maximum value to use for rotating palette hues. If not specified, the patcher will randomly generate one.",
"default": null
Expand All @@ -346,24 +346,24 @@
"additionalProperties": false
}
},
"color_space": {
"ColorSpace": {
"type": "string",
"description": "The color space to use for rotating palette hues.",
"enum": [
"HSV",
"OKLAB"
"Oklab"
],
"default": "OKLAB"
"default": "Oklab"
},
"symmetric": {
"Symmetric": {
"type": "boolean",
"description": "Randomly rotates hues in the positive or negative direction true.",
"default": true
}
},
"additionalProperties": false,
"required": [
"randomize"
"Randomize"
],
"default": null
},
Expand Down
Loading
Loading