forked from mirror/Archipelago
* initial (broken) commit * small work on init * Update Items.py * beginning work, some rom patches * commit progress from bh branch * deathlink, fix soft-reset kill, e-tank loss * begin work on targeting new bhclient * write font * definitely didn't forget to add the other two hashes no * update to modern options, begin colors * fix 6th letter bug * palette shuffle + logic rewrite * fix a bunch of pointers * fix color changes, deathlink, and add wily 5 req * adjust weapon weakness generation * Update Rules.py * attempt wily 5 softlock fix * add explicit test for rbm weaknesses * fix difficulty and hard reset * fix connect deathlink and off by one item color * fix atomic fire again * de-jank deathlink * rewrite wily5 rule * fix rare solo-gen fill issue, hopefully * Update Client.py * fix wily 5 requirements * undo fill hook * fix picopico-kun rules * for real this time * update minimum damage requirement * begin move to procedure patch * finish move to APPP, allow rando boobeam, color updates * fix color bug, UT support? * what do you mean I forgot the procedure * fix UT? * plando weakness and fixes * sfx when item received, more time stopper edge cases * Update test_weakness.py * fix rules and color bug * fix color bug, support reduced flashing * major world overhaul * Update Locations.py * fix first found bugs * mypy cleanup * headerless roms * Update Rom.py * further cleanup * work on energylink * el fixes * update to energylink 2.0 packet * energylink balancing * potentially break other clients, more balancing * Update Items.py * remove startup change from basepatch we write that in patch, since we also need to clean the area before applying * el balancing and feedback * hopefully less test failures? * implement world version check * add weapon/health option * Update Rom.py * x/x2 * specials * Update Color.py * Update Options.py * finally apply location groups * bump minor version number instead * fix duplicate stage sends * validate wily 5, tests * see if renaming fixes * add shuffled weakness * remove passwords * refresh rbm select, fix wily 5 validation * forgot we can't check 0 * oops I broke the basepatch (remove failing test later) * fix solo gen fill error? * fix webhost patch recognition * fix imports, basepatch * move to flexibility metric for boss validation * special case boobeam trap * block strobe on stage select init * more energylink balancing * bump world version * wily HP inaccurate in validation * fix validation edge case * save last completed wily to data storage * mypy and pep8 cleanup * fix file browse validation * fix test failure, add enemy weakness * remove test seed * update enemy damage * inno setup * Update en_Mega Man 2.md * setup guide * Update en_Mega Man 2.md * finish plando weakness section * starting rbm edge case * remove * imports * properly wrap later weakness additions in regen playthrough * fix import * forgot readme * remove time stopper special casing since we moved to proper wily 5 validation, this special casing is no longer important * properly type added locations * Update CODEOWNERS * add animation reduction * deprioritize Time Stopper in rush checks * special case wily phase 1 * fix key error * forgot the test * music and general cleanup * the great rename * fix import * thanks pycharm * reorder palette shuffle * account for alien on shuffled weakness * apply suggestions * fix seedbleed * fix invalid buster passthrough * fix weakness landing beneath required amount * fix failsafe * finish music * fix Time Stopper on Flash/Alien * asar pls * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world helpers * init cleanup * apostrophes * clearer wording * mypy and cleanup * options doc cleanup * Update rom.py * rules cleanup * Update __init__.py * Update __init__.py * move to defaultdict * cleanup world helpers * Update __init__.py * remove unnecessary line from fill hook * forgot the other one * apply code review * remove collect * Update rules.py * forgot another --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
277 lines
9.1 KiB
Python
277 lines
9.1 KiB
Python
from typing import Dict, Tuple, List, TYPE_CHECKING, Union
|
|
from . import names
|
|
from zlib import crc32
|
|
import struct
|
|
import logging
|
|
|
|
if TYPE_CHECKING:
|
|
from . import MM2World
|
|
from .rom import MM2ProcedurePatch
|
|
|
|
HTML_TO_NES: Dict[str, int] = {
|
|
"SNOW": 0x20,
|
|
"LINEN": 0x36,
|
|
"SEASHELL": 0x36,
|
|
"AZURE": 0x3C,
|
|
"LAVENDER": 0x33,
|
|
"WHITE": 0x30,
|
|
"BLACK": 0x0F,
|
|
"GREY": 0x00,
|
|
"GRAY": 0x00,
|
|
"ROYALBLUE": 0x12,
|
|
"BLUE": 0x11,
|
|
"SKYBLUE": 0x21,
|
|
"LIGHTBLUE": 0x31,
|
|
"TURQUOISE": 0x2B,
|
|
"CYAN": 0x2C,
|
|
"AQUAMARINE": 0x3B,
|
|
"DARKGREEN": 0x0A,
|
|
"GREEN": 0x1A,
|
|
"YELLOW": 0x28,
|
|
"GOLD": 0x28,
|
|
"WHEAT": 0x37,
|
|
"TAN": 0x37,
|
|
"CHOCOLATE": 0x07,
|
|
"BROWN": 0x07,
|
|
"SALMON": 0x26,
|
|
"ORANGE": 0x27,
|
|
"CORAL": 0x36,
|
|
"TOMATO": 0x16,
|
|
"RED": 0x16,
|
|
"PINK": 0x25,
|
|
"MAROON": 0x06,
|
|
"MAGENTA": 0x24,
|
|
"FUSCHIA": 0x24,
|
|
"VIOLET": 0x24,
|
|
"PLUM": 0x33,
|
|
"PURPLE": 0x14,
|
|
"THISTLE": 0x34,
|
|
"DARKBLUE": 0x01,
|
|
"SILVER": 0x10,
|
|
"NAVY": 0x02,
|
|
"TEAL": 0x1C,
|
|
"OLIVE": 0x18,
|
|
"LIME": 0x2A,
|
|
"AQUA": 0x2C,
|
|
# can add more as needed
|
|
}
|
|
|
|
MM2_COLORS: Dict[str, Tuple[int, int]] = {
|
|
names.atomic_fire: (0x28, 0x15),
|
|
names.air_shooter: (0x20, 0x11),
|
|
names.leaf_shield: (0x20, 0x19),
|
|
names.bubble_lead: (0x20, 0x00),
|
|
names.time_stopper: (0x34, 0x25),
|
|
names.quick_boomerang: (0x34, 0x14),
|
|
names.metal_blade: (0x37, 0x18),
|
|
names.crash_bomber: (0x20, 0x26),
|
|
names.item_1: (0x20, 0x16),
|
|
names.item_2: (0x20, 0x16),
|
|
names.item_3: (0x20, 0x16),
|
|
names.heat_man_stage: (0x28, 0x15),
|
|
names.air_man_stage: (0x28, 0x11),
|
|
names.wood_man_stage: (0x36, 0x17),
|
|
names.bubble_man_stage: (0x30, 0x19),
|
|
names.quick_man_stage: (0x28, 0x15),
|
|
names.flash_man_stage: (0x30, 0x12),
|
|
names.metal_man_stage: (0x28, 0x15),
|
|
names.crash_man_stage: (0x30, 0x16)
|
|
}
|
|
|
|
MM2_KNOWN_COLORS: Dict[str, Tuple[int, int]] = {
|
|
**MM2_COLORS,
|
|
# Street Fighter, technically
|
|
"Hadouken": (0x3C, 0x11),
|
|
"Shoryuken": (0x38, 0x16),
|
|
# X Series
|
|
"Z-Saber": (0x20, 0x16),
|
|
# X1
|
|
"Homing Torpedo": (0x3D, 0x37),
|
|
"Chameleon Sting": (0x3B, 0x1A),
|
|
"Rolling Shield": (0x3A, 0x25),
|
|
"Fire Wave": (0x37, 0x26),
|
|
"Storm Tornado": (0x34, 0x14),
|
|
"Electric Spark": (0x3D, 0x28),
|
|
"Boomerang Cutter": (0x3B, 0x2D),
|
|
"Shotgun Ice": (0x28, 0x2C),
|
|
# X2
|
|
"Crystal Hunter": (0x33, 0x21),
|
|
"Bubble Splash": (0x35, 0x28),
|
|
"Spin Wheel": (0x34, 0x1B),
|
|
"Silk Shot": (0x3B, 0x27),
|
|
"Sonic Slicer": (0x27, 0x01),
|
|
"Strike Chain": (0x30, 0x23),
|
|
"Magnet Mine": (0x28, 0x2D),
|
|
"Speed Burner": (0x31, 0x16),
|
|
# X3
|
|
"Acid Burst": (0x28, 0x2A),
|
|
"Tornado Fang": (0x28, 0x2C),
|
|
"Triad Thunder": (0x2B, 0x23),
|
|
"Spinning Blade": (0x20, 0x16),
|
|
"Ray Splasher": (0x28, 0x17),
|
|
"Gravity Well": (0x38, 0x14),
|
|
"Parasitic Bomb": (0x31, 0x28),
|
|
"Frost Shield": (0x23, 0x2C),
|
|
}
|
|
|
|
palette_pointers: Dict[str, List[int]] = {
|
|
"Mega Buster": [0x3D314],
|
|
"Atomic Fire": [0x3D318],
|
|
"Air Shooter": [0x3D31C],
|
|
"Leaf Shield": [0x3D320],
|
|
"Bubble Lead": [0x3D324],
|
|
"Quick Boomerang": [0x3D328],
|
|
"Time Stopper": [0x3D32C],
|
|
"Metal Blade": [0x3D330],
|
|
"Crash Bomber": [0x3D334],
|
|
"Item 1": [0x3D338],
|
|
"Item 2": [0x3D33C],
|
|
"Item 3": [0x3D340],
|
|
"Heat Man": [0x34B6, 0x344F7],
|
|
"Air Man": [0x74B6, 0x344FF],
|
|
"Wood Man": [0xB4EC, 0x34507],
|
|
"Bubble Man": [0xF4B6, 0x3450F],
|
|
"Quick Man": [0x134C8, 0x34517],
|
|
"Flash Man": [0x174B6, 0x3451F],
|
|
"Metal Man": [0x1B4A4, 0x34527],
|
|
"Crash Man": [0x1F4EC, 0x3452F],
|
|
}
|
|
|
|
|
|
def add_color_to_mm2(name: str, color: Tuple[int, int]) -> None:
|
|
"""
|
|
Add a color combo for Mega Man 2 to recognize as the color to display for a given item.
|
|
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
|
|
"""
|
|
MM2_KNOWN_COLORS[name] = validate_colors(*color)
|
|
|
|
|
|
def extrapolate_color(color: int) -> Tuple[int, int]:
|
|
if color > 0x1F:
|
|
color_1 = color
|
|
color_2 = color_1 - 0x10
|
|
else:
|
|
color_2 = color
|
|
color_1 = color_2 + 0x10
|
|
return color_1, color_2
|
|
|
|
|
|
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> Tuple[int, int]:
|
|
# Black should be reserved for outlines, a gray should suffice
|
|
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
|
color_1 = 0x10
|
|
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
|
color_2 = 0x10
|
|
|
|
# one final check, make sure we don't have two matching
|
|
if not allow_match and color_1 == color_2:
|
|
color_1 = 0x30 # color 1 to white works with about any paired color
|
|
|
|
return color_1, color_2
|
|
|
|
|
|
def get_colors_for_item(name: str) -> Tuple[int, int]:
|
|
if name in MM2_KNOWN_COLORS:
|
|
return MM2_KNOWN_COLORS[name]
|
|
|
|
check_colors = {color: color in name.upper().replace(" ", "") for color in HTML_TO_NES}
|
|
colors = [color for color in check_colors if check_colors[color]]
|
|
if colors:
|
|
# we have at least one color pattern matched
|
|
if len(colors) > 1:
|
|
# we have at least 2
|
|
color_1 = HTML_TO_NES[colors[0]]
|
|
color_2 = HTML_TO_NES[colors[1]]
|
|
else:
|
|
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
|
|
else:
|
|
# generate hash
|
|
crc_hash = crc32(name.encode("utf-8"))
|
|
hash_color = struct.pack("I", crc_hash)
|
|
color_1 = hash_color[0] % 0x3F
|
|
color_2 = hash_color[1] % 0x3F
|
|
|
|
if color_1 < color_2:
|
|
temp = color_1
|
|
color_1 = color_2
|
|
color_2 = temp
|
|
|
|
color_1, color_2 = validate_colors(color_1, color_2)
|
|
|
|
return color_1, color_2
|
|
|
|
|
|
def parse_color(colors: List[str]) -> Tuple[int, int]:
|
|
color_a = colors[0]
|
|
if color_a.startswith("$"):
|
|
color_1 = int(color_a[1:], 16)
|
|
else:
|
|
# assume it's in our list of colors
|
|
color_1 = HTML_TO_NES[color_a.upper()]
|
|
|
|
if len(colors) == 1:
|
|
color_1, color_2 = extrapolate_color(color_1)
|
|
else:
|
|
color_b = colors[1]
|
|
if color_b.startswith("$"):
|
|
color_2 = int(color_b[1:], 16)
|
|
else:
|
|
color_2 = HTML_TO_NES[color_b.upper()]
|
|
return color_1, color_2
|
|
|
|
|
|
def write_palette_shuffle(world: "MM2World", rom: "MM2ProcedurePatch") -> None:
|
|
palette_shuffle: Union[int, str] = world.options.palette_shuffle.value
|
|
palettes_to_write: Dict[str, Tuple[int, int]] = {}
|
|
if isinstance(palette_shuffle, str):
|
|
color_sets = palette_shuffle.split(";")
|
|
if len(color_sets) == 1:
|
|
palette_shuffle = world.options.palette_shuffle.option_none
|
|
# singularity is more correct, but this is faster
|
|
else:
|
|
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
|
|
for color_set in color_sets:
|
|
if "-" in color_set:
|
|
character, color = color_set.split("-")
|
|
if character.title() not in palette_pointers:
|
|
logging.warning(f"Player {world.multiworld.get_player_name(world.player)} "
|
|
f"attempted to set color for unrecognized option {character}")
|
|
colors = color.split("|")
|
|
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
|
palettes_to_write[character.title()] = real_colors
|
|
else:
|
|
# If color is provided with no character, assume singularity
|
|
colors = color_set.split("|")
|
|
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
|
for character in palette_pointers:
|
|
palettes_to_write[character] = real_colors
|
|
# Now we handle the real values
|
|
if palette_shuffle == 1:
|
|
shuffled_colors = list(MM2_COLORS.values())
|
|
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
|
|
world.random.shuffle(shuffled_colors)
|
|
for character in palette_pointers:
|
|
if character not in palettes_to_write:
|
|
palettes_to_write[character] = shuffled_colors.pop()
|
|
elif palette_shuffle > 1:
|
|
if palette_shuffle == 2:
|
|
for character in palette_pointers:
|
|
if character not in palettes_to_write:
|
|
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
|
palettes_to_write[character] = real_colors
|
|
else:
|
|
# singularity
|
|
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
|
for character in palette_pointers:
|
|
if character not in palettes_to_write:
|
|
palettes_to_write[character] = real_colors
|
|
|
|
for character in palettes_to_write:
|
|
for pointer in palette_pointers[character]:
|
|
rom.write_bytes(pointer, bytes(palettes_to_write[character]))
|
|
|
|
if character == "Atomic Fire":
|
|
# special case, we need to update Atomic Fire's flashing routine
|
|
rom.write_byte(0x3DE4A, palettes_to_write[character][1])
|
|
rom.write_byte(0x3DE4C, palettes_to_write[character][1])
|