Files
dockipelago/worlds/papermario/Rom.py
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

354 lines
14 KiB
Python

# from https://github.com/icebound777/PMR-SeedGenerator/blob/main/randomizer.py, pulled functions for rom-related stuff
import pkgutil
import bsdiff4
from . import PMItem
from .calculate_crc import recalculate_crcs
from .RomTable import RomTable
import os
from .data.itemlocation_replenish import replenishing_itemlocations
from .itemhints import get_itemhints
from .modules.random_actor_stats import get_shuffled_chapter_difficulty
from .modules.random_audio import get_randomized_audio
from .modules.random_formations import get_random_formations
from .modules.random_map_mirroring import get_mirrored_map_list
from .modules.random_movecosts import get_randomized_moves
from .modules.random_mystery import get_random_mystery
from .modules.random_palettes import get_randomized_palettes, get_randomized_coinpalette
from .data.MysteryOptions import MysteryOptions
from .modules.random_puzzles_minigames import get_puzzles_minigames
from .modules.random_quizzes import get_randomized_quizzes
from .options import EnemyDifficulty
from .data.ItemList import item_table, item_groups
from .data.node import Node
from .Locations import PMLocation
from .modules.random_shop_prices import get_shop_price
from .modules.random_stat_distribution import generate_random_stats
from .modules.modify_game_strings import multiworld_item_info_to_pmString
from worlds.Files import APDeltaPatch
from settings import get_settings
class PaperMarioDeltaPatch(APDeltaPatch):
game = "Paper Mario"
hash = "a722f8161ff489943191330bf8416496"
patch_file_ending = ".appm64"
result_file_ending = ".z64"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_as_bytes()
def get_base_rom_as_bytes() -> bytes:
with open(get_settings().paper_mario_settings.rom_file, "rb") as infile:
base_rom_bytes = bytes(infile.read())
return base_rom_bytes
def write_bytes(byte_array: bytearray, address: int, value: bytes) -> int:
byte_array[address: address + len(value) - 1] = value
return address + len(value)
def write_patch(
output_directory: str,
world,
placed_items: list,
entrance_list: list,
enemy_stats: list,
battle_formations: list,
move_costs: list,
itemhints: list,
coin_palette_data: list,
coin_palette_targets: list,
coin_palette_crcs: list,
palette_data: list,
quiz_data: list,
music_list: list,
mapmirror_list: list,
puzzle_list: list,
mystery_opts: MysteryOptions,
star_beam_area: int
):
base_rom = get_base_rom_as_bytes()
base_patch = pkgutil.get_data(__name__, "data/base_pmr_patch.bsdiff4")
patched_rom = bytearray(bsdiff4.patch(base_rom, base_patch))
seed_id = world.random.randint(0, 0xFFFFFFFF)
# Create the ROM table
rom_table = RomTable()
rom_table.create()
# Create a sorted list of key:value pairs to be written into the ROM
table_data = rom_table.generate_pairs(
options=world.options,
placed_items=placed_items,
entrances=entrance_list,
actor_attributes=enemy_stats,
move_costs=move_costs,
palettes=palette_data,
quizzes=quiz_data,
music_list=music_list,
mapmirror_list=mapmirror_list,
puzzle_list=puzzle_list,
mystery_opts=mystery_opts,
required_spirits=world.required_spirits,
battle_list=world.battle_list,
star_beam_area=star_beam_area,
trappable_item_names=world.trappable_item_names,
random=world.random
)
# Update table info with variable data
end_of_content_marker = 0x4 # end of table FFFFFFFF
end_padding = 0x10 # 4x FFFFFFFF
len_battle_formations = sum([len(formation) for formation in battle_formations])
len_itemhints = sum([len(itemhint_word) for itemhint_word in itemhints])
rom_table.info["db_size"] = (rom_table.info["header_size"]
+ (len(table_data) * 8)
+ (len_battle_formations * 4)
+ end_of_content_marker
+ (len_itemhints * 4)
+ end_of_content_marker
+ end_padding)
rom_table.info["seed"] = seed_id
rom_table.info["formations_offset"] = len(table_data) * 8
rom_table.info["itemhints_offset"] = (rom_table.info["formations_offset"]
+ end_of_content_marker
+ (len_battle_formations * 4))
# Modify the table data in the ROM
changed_coin_palette = False
out_file_name = world.multiworld.get_out_file_name_base(world.player)
output_path = os.path.join(output_directory, f"{out_file_name}.z64")
with open(output_path, "wb") as out_file:
out_file.write(patched_rom)
# Set slot auth
out_file.seek(rom_table.info["auth_address"])
out_file.write(world.auth)
# Write the db header
# file.seek(rom_table.info["address"]) # we're already here, but leaving this in case we move auth elsewhere
out_file.write(rom_table.info["magic_value"].to_bytes(4, byteorder="big"))
out_file.write(rom_table.info["header_size"].to_bytes(4, byteorder="big"))
out_file.write(rom_table.info["db_size"].to_bytes(4, byteorder="big"))
out_file.write(rom_table.info["seed"].to_bytes(4, byteorder="big"))
out_file.write(rom_table.info["formations_offset"].to_bytes(4, byteorder="big"))
out_file.write(rom_table.info["itemhints_offset"].to_bytes(4, byteorder="big"))
# Write table data and generate log file
out_file.seek(rom_table.info["address"] + rom_table.info["header_size"])
for _, pair in enumerate(table_data):
key_int = pair["key"].to_bytes(4, byteorder="big")
value_int = pair["value"].to_bytes(4, byteorder="big")
out_file.write(key_int)
out_file.write(value_int)
for formation in battle_formations:
for formation_hex_word in formation:
out_file.write(formation_hex_word.to_bytes(4, byteorder="big"))
# Write end of formations table
out_file.write(0xFFFFFFFF.to_bytes(4, byteorder="big"))
# Write itemhint table
for itemhint in itemhints:
for itemhint_hex in itemhint:
out_file.write(itemhint_hex.to_bytes(4, byteorder="big"))
# Write end of item hints table
out_file.write(0xFFFFFFFF.to_bytes(4, byteorder="big"))
# Write end of db padding
for _ in range(1, 5):
out_file.write(0xFFFFFFFF.to_bytes(4, byteorder="big"))
# Special solution for random coin palettes
if coin_palette_data and coin_palette_targets:
changed_coin_palette = True
for target_rom_location in coin_palette_targets:
out_file.seek(target_rom_location)
for palette_byte in coin_palette_data:
out_file.write(palette_byte.to_bytes(4, byteorder="big"))
# Write shop descriptions
for node in placed_items:
if node.shop_string_location != -1:
out_file.seek(node.shop_string_location)
out_file.write(bytes(node.shop_string))
if changed_coin_palette:
recalculate_crcs(output_path, coin_palette_crcs)
patch = PaperMarioDeltaPatch(os.path.splitext(output_path)[0] + ".appm64",
player=world.player,
player_name=world.multiworld.get_player_name(world.player),
patched_path=output_path)
patch.write()
os.unlink(output_path)
def generate_output(world, output_dir: str) -> None:
# mario stats
if world.options.random_start_stats.value:
world.options.starting_hp.value, world.options.starting_fp.value, world.options.starting_bp.value = (
generate_random_stats(world.options.random_start_stats_level.value, world.random))
# enemy stats
enemy_stats, chapter_changes = get_shuffled_chapter_difficulty(
world.options.enemy_difficulty.value, world.options.starting_map.value, world.random)
battle_formations = []
if (world.options.formation_shuffle.value
or world.options.enemy_difficulty.value == EnemyDifficulty.option_Progressive_Scaling):
battle_formations = get_random_formations(chapter_changes,
world.options.enemy_difficulty.value ==
EnemyDifficulty.option_Progressive_Scaling,
world.random)
# Coin palette values
coin_palette_data, coin_palette_targets, coin_palette_crcs = (
get_randomized_coinpalette(world.options.coin_palette.value))
# Quizzes are always randomized
quiz_data = get_randomized_quizzes(world.random)
# randomized puzzles
puzzle_list, world.spoilerlog_puzzles = get_puzzles_minigames(world.options.random_puzzles.value, world)
# Default mystery options for now
mystery_opts = get_random_mystery(world.options.mystery_shuffle.value, world.random)
# Non-coin palettes
palette_data = get_randomized_palettes(world)
# Move costs
move_costs = get_randomized_moves(world.options.badge_bp_shuffle.value,
world.options.badge_fp_shuffle.value,
world.options.partner_fp_shuffle.value,
world.options.sp_shuffle.value,
world.random)
# Randomized music
music_list = get_randomized_audio(world.options.shuffle_music.value,
world.options.shuffle_jingles.value,
world.random)
# mirror mode is always off at the moment
static_map_mirroring = get_mirrored_map_list()
placed_items = get_filled_node_list(world)
item_hints = get_itemhints(False, placed_items, world.options)
star_beam_area = get_star_beam_area(world)
write_patch(output_directory=output_dir,
world=world,
placed_items=placed_items,
entrance_list=world.entrance_list,
enemy_stats=enemy_stats,
battle_formations=battle_formations,
move_costs=move_costs,
itemhints=item_hints,
coin_palette_data=coin_palette_data,
coin_palette_targets=coin_palette_targets,
coin_palette_crcs=coin_palette_crcs,
palette_data=palette_data,
quiz_data=quiz_data,
music_list=music_list,
mapmirror_list=static_map_mirroring,
puzzle_list=puzzle_list,
mystery_opts=mystery_opts,
star_beam_area=star_beam_area)
# Paper Mario Rando operates off of a node list with item IDs and prices
def get_filled_node_list(world):
placed_items = []
mw_keys = 0
all_locations = [location for location in world.multiworld.get_locations(world.player)]
all_locations.extend(world.ch_excluded_locations)
for location in all_locations:
if location.keyname is None:
continue
if location.item is None:
continue
pm_loc: PMLocation = location
cur_node = Node()
cur_node.map_id = pm_loc.map_id
cur_node.area_id = pm_loc.area_id
cur_node.key_name_item = pm_loc.keyname
cur_node.item_source_type = pm_loc.source_type
cur_node.vanilla_price = pm_loc.vanilla_price
cur_node.item_index = pm_loc.index
cur_node.price_index = pm_loc.price_index
cur_node.identifier = pm_loc.identifier
cur_node.item_classification = pm_loc.item.classification
cur_node.item_player_name = world.multiworld.get_player_name(pm_loc.item.player)
cur_node.item_name = pm_loc.item.name
if pm_loc.price_keyname != "None":
cur_node.key_name_price = pm_loc.price_keyname
if pm_loc.item.player == world.player:
cur_node.current_item = pm_loc.item
else:
# Multiworld items in replenishable locations get IDs that cause them to stop spawning after being obtained
if cur_node.identifier in replenishing_itemlocations:
mw_key_name = "MultiWorldKey" + f"{mw_keys:02x}".upper()
cur_node.current_item = PMItem("MultiWorldItem", world.player, item_table[mw_key_name], False)
mw_keys += 1
# The rest can get the generic id
else:
cur_node.current_item = PMItem("MultiWorldItem", world.player, item_table["MultiWorldGeneric"], False)
# set prices, descriptions for items in shops
if "Shop" in cur_node.identifier:
if pm_loc.item.player != world.player:
cur_node.shop_string_location, cur_node.shop_string = (multiworld_item_info_to_pmString(
world.multiworld.get_player_name(pm_loc.item.player),
pm_loc.item.name,
pm_loc.item.classification,
cur_node.identifier))
cur_node.current_item.base_price = get_shop_price(pm_loc,
cur_node.current_item,
world.options.include_shops.value,
world.options.merlow_rewards_pricing.value,
world.options.total_power_stars.value,
world.random)
placed_items.append(cur_node)
return placed_items
def get_star_beam_area(world):
if world.options.shuffle_star_beam.value:
item_locations = world.multiworld.find_item_locations("Star Beam", world.player)
if item_locations:
location = item_locations[0]
# if the location is not in the player's game, the area must be an invalid one so fallback text is used
if location.player != world.player:
return 28
# local star beam gets its area hinted
else:
loc: PMLocation = location
return loc.area_id
return 28