Files
dockipelago/worlds/pmd_eos/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

263 lines
11 KiB
Python

import json
from typing import TYPE_CHECKING
from BaseClasses import Item, Location
from settings import get_settings
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
if TYPE_CHECKING:
from . import EOSWorld
def get_base_rom_as_bytes() -> bytes:
with open(get_settings().pmd_eos_options.rom_file, "rb") as infile:
base_rom_bytes = bytes(infile.read())
return base_rom_bytes
class EOSPathExtension(APPatchExtension):
game = "Pokemon Mystery Dungeon Explorers of Sky"
class EOSProcedurePatch(APProcedurePatch, APTokenMixin):
# settings for what the end file is going to look like
game = "Pokemon Mystery Dungeon Explorers of Sky"
hash = "6735749e060e002efd88e61560e45567"
patch_file_ending = ".apeos"
result_file_ending = ".nds"
# different procedures to apply. Can always add more but have only needed these two so far
procedure = [
("apply_bsdiff4", ["base_patch.bsdiff4"]),
("apply_tokens", ["token_data.bin"]),
]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_as_bytes()
def write_tokens(
world: "EOSWorld",
patch: EOSProcedurePatch,
hint_items: list[Location],
dimensional_screams: list[Location],
starting_se: int,
) -> None:
ov36_mem_loc = 2724864 # find_ov36_mem_location()
seed_offset = 0x36FA0
player_name_offset = 0x36F80
player_name_changed_offset = 0x36F90
ap_settings_offset = 0x36FA8
# mission_max_offset = 0x36F9A
# macguffin_max_offset = 0x36F9E
# spinda_drinks_offset = 0x37146
hintable_items_offset = 3303424 # number from Heckas makefile code
custom_save_area_offset = ov36_mem_loc + 0x8F80
# main_game_unlocked_offset = ov36_mem_loc + 0x37148 # custom_save_area_offset + 0x2A7
dimensional_scream_who_offset = hintable_items_offset + 0x4
dimensional_scream_what_offset = hintable_items_offset + 0x202
dimensional_scream_where_offset = hintable_items_offset + 0x5E0
# dimensional_scream_hints = get_dimensional_hints(world)
dimensional_scream_hints = dimensional_screams
# writable_screams = [k.address for k in dimensional_scream_hints]
# recruitment_offset = 0x3702C
# recruitment_evo_offset = 0x37030
# team_formation_offset = 0x37034
# level_scaling_offset = 0x37038
options_dict = {
"seed": world.multiworld.seed,
"player": world.player,
"goal": world.options.goal.value,
"bag_start": world.options.bag_on_start.value,
"level_scaling": world.options.level_scale.value,
"recruiting": world.options.recruit.value,
"recruits_evolution": world.options.recruit_evo.value,
"team_formation": world.options.team_form.value,
"dojo_dungeons_rando": world.options.dojo_dungeons.value,
"relic_shard_fragments": world.options.required_fragments.value,
"extra_shards": world.options.total_shards.value,
"type_sanity": world.options.type_sanity.value,
"starter_option": world.options.starter_option.value,
"iq_scaling": world.options.iq_scaling.value,
"xp_scaling": world.options.xp_scaling.value,
"instruments_required": world.options.req_instruments.value,
"extra_instruments": world.options.total_instruments.value,
"hero_evolution": world.options.hero_evolution.value,
"deathlink": world.options.deathlink.value,
"deathlink_type": world.options.deathlink_type.value,
"legendaries": world.options.legendaries.value,
"sky_peak_type": world.options.sky_peak_type.value,
"special_sanity": world.options.special_episode_sanity.value,
"traps_allowed": world.options.allow_traps.value,
"invisible_traps": world.options.invisible_traps.value,
"trap_percentage": world.options.trap_percent.value,
"long_locations": world.options.long_location.value,
"cursed_aegis_cave": world.options.cursed_aegis_cave.value,
"drink_events": world.options.drink_events.value,
"spinda_drinks": world.options.spinda_drinks.value,
"exclude_special": world.options.exclude_special.value,
# "dimensional_screams": writable_screams,
}
seed = world.multiworld.seed_name.encode("UTF-8")[0:7]
patch.write_file("options.json", json.dumps(options_dict).encode("UTF-8"))
trans_table = {"[": "", "]": "", "~": "", "\\": ""}
trans_table = str.maketrans(trans_table)
# Change the player name so that PMD_EOS can read it correctly and then make it latin
player_name = world.multiworld.player_name[world.player]
player_name_changed = player_name.translate(trans_table)
player_name_encoded = player_name.encode("cp1252", "xmlcharrefreplace")
player_name_changed_encoded = player_name_changed.encode("cp1252", "xmlcharrefreplace")
# Bake player name into ROM
patch.write_token(APTokenTypes.WRITE, ov36_mem_loc + player_name_changed_offset, player_name_changed_encoded)
patch.write_token(APTokenTypes.WRITE, ov36_mem_loc + player_name_offset, player_name_encoded)
# Bake names of previewable items into ROM
for i in range(len(hint_items)):
if hint_items[i].name.__contains__(""):
hint_loc_name1 = hint_items[i].name.translate(trans_table)
hint_loc_name = hint_loc_name1.replace("", "[M:S3]")
else:
hint_loc_name = hint_items[i].name.translate(trans_table)
hint_player = world.multiworld.player_name[hint_items[i].item.player].translate(trans_table)
patch.write_token(
APTokenTypes.WRITE,
dimensional_scream_who_offset + 17 * i,
hint_player[0:15].encode("cp1252", "xmlcharrefreplace"),
)
patch.write_token(
APTokenTypes.WRITE,
dimensional_scream_where_offset + 33 * i,
hint_loc_name[0:31].encode("cp1252", "xmlcharrefreplace"),
)
hint_item = hint_items[i].item.name.translate(trans_table)
patch.write_token(
APTokenTypes.WRITE,
dimensional_scream_what_offset + 33 * i,
hint_item[0:31].encode("cp1252", "xmlcharrefreplace"),
)
# Bake the dimensional Scream Hints into the ROM
for i in range(len(dimensional_scream_hints)):
hint_player = world.multiworld.player_name[dimensional_scream_hints[i].player].translate(trans_table)
patch.write_token(
APTokenTypes.WRITE,
dimensional_scream_who_offset + 17 * (i + 10),
hint_player[0:15].encode("cp1252", "xmlcharrefreplace"),
)
if dimensional_scream_hints[i].name.__contains__(""):
hint_loc_name1 = dimensional_scream_hints[i].name.translate(trans_table)
hint_loc_name = hint_loc_name1.replace("", "[M:S3]")
else:
hint_loc_name = dimensional_scream_hints[i].name.translate(trans_table)
# hint_loc_name = dimensional_scream_hints[i].name.translate(trans_table)
patch.write_token(
APTokenTypes.WRITE,
dimensional_scream_where_offset + 33 * (i + 10),
hint_loc_name[0:31].encode("cp1252", "xmlcharrefreplace"),
)
hint_item = dimensional_scream_hints[i].item.name.translate(trans_table)
patch.write_token(
APTokenTypes.WRITE,
dimensional_scream_what_offset + 33 * (i + 10),
hint_item[0:31].encode("cp1252", "xmlcharrefreplace"),
)
# Bake seed name into ROM
patch.write_token(APTokenTypes.WRITE, ov36_mem_loc + seed_offset, seed)
write_byte = 0
late_missions_count = 0
late_outlaws_count = 0
if world.options.goal.value == 1:
late_missions_count = world.options.late_mission_checks.value
late_outlaws_count = world.options.late_outlaw_checks.value
elif world.options.goal.value == 0:
write_byte = write_byte | (0x1 << 17)
instruments_required = world.options.req_instruments.value
macguffins_required = world.options.required_fragments.value
# Take the options and bake them into the rom, so they can be applied on runtime
write_byte = write_byte | world.options.iq_scaling.value
write_byte = write_byte | (world.options.xp_scaling.value << 12)
write_byte = write_byte | (instruments_required << 52)
write_byte = write_byte | (macguffins_required << 48)
write_byte = write_byte | (world.options.early_mission_checks.value << 19)
write_byte = write_byte | (world.options.early_outlaw_checks.value << 27)
write_byte = write_byte | (late_missions_count << 35)
write_byte = write_byte | (late_outlaws_count << 43)
write_byte = write_byte | (world.options.drink_events.value << 56)
write_byte = write_byte | (world.options.spinda_drinks.value << 64)
if world.options.long_location.value == 1:
write_byte = write_byte | (0x1 << 18)
if world.options.early_mission_floors.value:
write_byte = write_byte | (0x1 << 4)
if world.options.move_shortcuts.value:
write_byte = write_byte | (0x1 << 5)
if world.options.level_scale.value == 1:
write_byte = write_byte | (0x1 << 24)
if world.options.level_scale.value == 2:
write_byte = write_byte | (0x1 << 25)
if world.options.guest_scaling.value:
write_byte = write_byte | (0x1 << 26)
if world.options.type_sanity.value:
write_byte = write_byte | (0x1 << 7)
if world.options.starter_option.value == 1:
write_byte = write_byte | (0x1 << 8)
elif world.options.starter_option.value == 2:
write_byte = write_byte | (0x1 << 9)
elif world.options.starter_option.value == 3:
write_byte = write_byte | ((0x1 << 8) + (0x1 << 9))
if world.options.deathlink.value and world.options.deathlink_type.value == 0:
write_byte = write_byte | (0x1 << 11)
elif world.options.deathlink.value and world.options.deathlink.value == 1:
write_byte = write_byte | (0x1 << 10)
# if world.options.special_episode_sanity.value == 0:
# write_byte = write_byte | (0x1 << 16)
if world.options.special_episode_sanity.value == 1 and starting_se != 0:
write_byte = write_byte | (starting_se << 32)
# write the tokens that will be applied and write the token data into the bin for AP
patch.write_token(
APTokenTypes.WRITE, ov36_mem_loc + ap_settings_offset, int.to_bytes(write_byte, length=9, byteorder="little")
)
patch.write_file("token_data.bin", patch.get_token_binary())
# testnum = find_ov36_mem_location()
def find_ov36_mem_location() -> int:
# Not currently used. Was an attempt to search the entire rom for the identifier and return where it found it
# Would simplify having to change the start value of ov 36 every time the base patch changes
rom = get_base_rom_as_bytes()
test = range(0x296000, 0x300000)
for byte_i in range(0x297000, 0x300000):
# , byte in enumerate(rom)
intest = 0x297000 / 2
hex_search_value = 0xBAADF00D
hex_searched = int.from_bytes((rom[byte_i : (byte_i + 4)]))
test2 = rom[byte_i : (byte_i + 4)]
if hex_searched == hex_search_value:
return byte_i