diff --git a/src/mars_patcher/constants/game_data.py b/src/mars_patcher/constants/game_data.py index 91d978c..eaf1d39 100644 --- a/src/mars_patcher/constants/game_data.py +++ b/src/mars_patcher/constants/game_data.py @@ -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) diff --git a/src/mars_patcher/palette.py b/src/mars_patcher/palette.py index 30fd798..21b1655 100644 --- a/src/mars_patcher/palette.py +++ b/src/mars_patcher/palette.py @@ -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 @@ -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) @@ -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) @@ -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] @@ -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: @@ -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() diff --git a/src/mars_patcher/random_palettes.py b/src/mars_patcher/random_palettes.py index da4a4f0..0dbc2bc 100644 --- a/src/mars_patcher/random_palettes.py +++ b/src/mars_patcher/random_palettes.py @@ -1,5 +1,5 @@ import random -from enum import Enum +from enum import Enum, auto from typing import TypeAlias from typing_extensions import Self @@ -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: @@ -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 = {} @@ -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: @@ -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: @@ -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) @@ -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} @@ -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 @@ -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.""" @@ -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) diff --git a/src/mars_patcher/zm/auto_generated_types.py b/src/mars_patcher/zm/auto_generated_types.py index 05ed395..8575698 100644 --- a/src/mars_patcher/zm/auto_generated_types.py +++ b/src/mars_patcher/zm/auto_generated_types.py @@ -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.""" @@ -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 diff --git a/src/mars_patcher/zm/data/schema.json b/src/mars_patcher/zm/data/schema.json index e4b008c..c143ffd 100644 --- a/src/mars_patcher/zm/data/schema.json +++ b/src/mars_patcher/zm/data/schema.json @@ -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 @@ -346,16 +346,16 @@ "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 @@ -363,7 +363,7 @@ }, "additionalProperties": false, "required": [ - "randomize" + "Randomize" ], "default": null }, diff --git a/src/mars_patcher/zm/patcher.py b/src/mars_patcher/zm/patcher.py index 88ddc37..e9cd2d5 100644 --- a/src/mars_patcher/zm/patcher.py +++ b/src/mars_patcher/zm/patcher.py @@ -1,6 +1,7 @@ from collections.abc import Callable from os import PathLike +from mars_patcher.random_palettes import PaletteRandomizer, PaletteSettings from mars_patcher.rom import Rom from mars_patcher.sounds import set_sounds from mars_patcher.zm.auto_generated_types import MarsSchemaZM @@ -35,11 +36,11 @@ def patch_zm( # Randomize palettes - palettes are randomized first since the item # patcher needs to copy tilesets - # if "Palettes" in patch_data: - # status_update("Randomizing palettes...", -1) - # pal_settings = PaletteSettings.from_json(patch_data["Palettes"]) - # pal_randomizer = PaletteRandomizer(rom, pal_settings) - # pal_randomizer.randomize() + if "Palettes" in patch_data: + status_update("Randomizing palettes...", -1) + pal_settings = PaletteSettings.from_json(patch_data["Palettes"]) + pal_randomizer = PaletteRandomizer(rom, pal_settings) + pal_randomizer.randomize() # Load locations and set assignments status_update("Writing item assignments...", -1)