Files
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

230 lines
8.3 KiB
Python

"""
Classes and functions related to creating a ROM patch
"""
from __future__ import annotations
from collections import Counter
import json
from pathlib import Path
from typing import TYPE_CHECKING, cast
from BaseClasses import Location
import Utils
from worlds.Files import APPatchExtension, APProcedurePatch
from .items import item_data_table, tank_data_table, major_item_data_table
from .locations import full_location_table as location_table
from .options import ChozodiaAccess, DisplayNonLocalItems, GameDifficulty, Goal, LayoutPatches
from .patcher import MD5_US, patch_rom
from .patcher.text import LINE_WIDTH, SPACE, Message, get_width_of_encoded_character
from .item_sprites import Sprite, get_zero_mission_sprite, unknown_item_alt_sprites
if TYPE_CHECKING:
from . import MZMWorld
class MZMPatchExtensions(APPatchExtension):
game = "Metroid: Zero Mission"
@staticmethod
def apply_json(caller: APProcedurePatch, rom: bytes, file_name: str) -> bytes:
return patch_rom(rom, json.loads(caller.get_file(file_name).decode()))
class MZMProcedurePatch(APProcedurePatch):
game = "Metroid: Zero Mission"
hash = MD5_US
patch_file_ending = ".apmzm"
result_file_ending = ".gba"
procedure = [("apply_json", ["patch.json"])]
@classmethod
def get_source_data(cls) -> bytes:
with open(get_base_rom_path(), "rb") as stream:
return stream.read()
def get_base_rom_path(file_name: str = "") -> Path:
from . import MZMWorld
if not file_name:
file_name = MZMWorld.settings.rom_file
file_path = Path(file_name)
if file_path.exists():
return file_path
else:
return Path(Utils.user_path(file_name))
goal_texts = {
Goal.option_mecha_ridley: "Infiltrate and destroy\nthe Space Pirates' mother ship.",
Goal.option_bosses: "Exterminate all Metroid\norganisms and defeat Mother Brain.",
Goal.option_metroid_dna: "Locate Metroid DNA\nsamples and destroy the mother ship.",
}
def get_item_sprite(location: Location, world: MZMWorld) -> str:
player = world.player
nonlocal_item_handling = world.options.display_nonlocal_items
item = location.item
if location.native_item and (nonlocal_item_handling != DisplayNonLocalItems.option_none or item.player == player):
other_world = cast("MZMWorld", world.multiworld.worlds[item.player])
sprite = item_data_table[item.name].game_data.sprite
if (item.name in unknown_item_alt_sprites and other_world.options.fully_powered_suit.use_alt_unknown_sprites()):
sprite = unknown_item_alt_sprites[item.name]
return sprite
if nonlocal_item_handling == DisplayNonLocalItems.option_match_series:
sprite = get_zero_mission_sprite(item)
if sprite is not None:
return sprite
if item.advancement or item.trap:
sprite = Sprite.APLogoProgression
elif item.useful:
sprite = Sprite.APLogoUseful
else:
sprite = Sprite.APLogo
return sprite
space_width = get_width_of_encoded_character(SPACE)
def split_text(text: str):
lines = [""]
i = 0
width = 0
while i < len(text):
next_space = text.find(" ", i)
if next_space == -1:
next_space = len(text)
next_word = text[i:next_space]
next_word_width = Message(next_word).display_width()
if width + space_width + next_word_width <= LINE_WIDTH:
lines[-1] = f"{lines[-1]}{next_word} "
width += space_width + next_word_width
else:
lines[-1] = lines[-1][:-1]
lines.append(text[i:next_space] + " ")
width = next_word_width
i = next_space + 1
lines[-1] = lines[-1][:-1]
return lines
GOAL_TO_CONFIG_NAME = {
Goal.option_mecha_ridley: "vanilla",
Goal.option_bosses: "bosses",
Goal.option_metroid_dna: "metroid_dna"
}
DIFFICULTY_TO_CONFIG_NAME = {
GameDifficulty.option_normal: "normal",
GameDifficulty.option_hard: "hard",
GameDifficulty.option_either: "either",
}
def write_json_data(world: MZMWorld, patch: MZMProcedurePatch):
multiworld = world.multiworld
player = world.player
data = {
"player_name": world.player_name,
"seed_name": multiworld.seed_name,
}
config = {
"goal": GOAL_TO_CONFIG_NAME[world.options.goal.value],
"difficulty": DIFFICULTY_TO_CONFIG_NAME[world.options.game_difficulty.value],
"remove_gravity_heat_resistance": True,
"power_bombs_without_bomb": True,
"buff_power_bomb_drops": bool(world.options.buff_pb_drops),
"separate_hijump_springball": bool(world.options.spring_ball),
"skip_chozodia_stealth": bool(world.options.skip_chozodia_stealth),
"chozodia_requires_mother_brain": world.options.chozodia_access.value == ChozodiaAccess.option_closed,
"start_with_maps": bool(world.options.start_with_maps),
"reveal_maps": bool(world.options.start_with_maps),
"reveal_hidden_blocks": bool(world.options.reveal_hidden_blocks),
"skip_tourian_opening_cutscenes": bool(world.options.skip_tourian_opening_cutscenes),
"elevator_speed": world.options.elevator_speed.value,
}
if world.options.goal.value == Goal.option_metroid_dna:
config["metroid_dna_required"] = world.options.metroid_dna_required.value
data["config"] = config
locations = []
for location in multiworld.get_locations(player):
item = location.item
if item.code is None:
continue
sprite = get_item_sprite(location, world)
if item.player == player:
item_name = item.name
message = None
else:
item_name = "Nothing"
message = f"{item.name}\nSent to {multiworld.player_name[item.player]}"
location_data = location_table[location.name]
assert location_data.id is not None
locations.append({
"id": location_data.id,
"item": item_name,
"sprite": sprite,
"message": message,
})
data["locations"] = locations
precollected_items = Counter(item.name for item in multiworld.precollected_items[player])
starting_inventory: dict[str, int | bool] = {}
for item, count in precollected_items.items():
if item == "Missile Tank":
starting_inventory[item] = min(count, 999)
elif item in tank_data_table:
starting_inventory[item] = min(count, 99)
elif item in major_item_data_table:
starting_inventory[item] = count > 0
data["start_inventory"] = starting_inventory
text = {"Story": {}}
ap_version = Utils.version_tuple.as_simple_string()
world_version = world.world_version.as_simple_string()
text["Story"]["Intro"] = (f"AP {multiworld.seed_name}\n"
f"P{player} - {world.player_name}\n"
f"AP {ap_version} / World version: {world_version}\n"
"\n"
f"YOUR MISSION: {goal_texts[world.options.goal.value]}")
plasma_beam = world.create_item("Plasma Beam")
if world.options.plasma_beam_hint.value and plasma_beam not in multiworld.precollected_items[player]:
zss_text = ("With Mother Brain taken down, I needed\n"
"to get my suit back in the ruins.\n")
location = multiworld.find_item(plasma_beam.name, player)
if location.native_item:
location_text = location.parent_region.hint_text
else:
location_text = f"at {location.name}"
if location.player != player:
player_text = f" in {multiworld.player_name[location.player]}'s world"
else:
player_text = ""
lines = split_text(f"Could I find the Plasma Beam {location_text}{player_text}?")
while len(lines) > 4:
location_text = location_text[:location_text.rfind(" ")]
lines = split_text(f"Could I find the Plasma Beam {location_text}{player_text}?")
if len(lines) < 4:
zss_text += "\n"
zss_text += "\n".join(lines)
text["Story"]["Escape 2"] = zss_text
data["text"] = text
if world.options.layout_patches.value == LayoutPatches.option_true:
data["layout_patches"] = "all"
else:
data["layout_patches"] = world.enabled_layout_patches
patch.write_file("patch.json", json.dumps(data).encode())