forked from mirror/Archipelago
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
149 lines
6.7 KiB
Python
149 lines
6.7 KiB
Python
import json
|
|
import pkgutil
|
|
|
|
import yaml
|
|
|
|
from worlds.Files import APProcedurePatch, APTokenMixin, APPatchExtension
|
|
from .Functions import *
|
|
from .data_manager.text import apply_ages_edits, get_modded_seasons_text_data
|
|
from .puzzle_rando import randomize_puzzles
|
|
from .room_edits import apply_room_edits
|
|
from ..common.patching.rooms.encoding import write_room_data
|
|
from ..common.patching.text.encoding import write_text_data
|
|
from ..common.patching.z80asm.Assembler import Z80Assembler, Z80Block, GameboyAddress
|
|
|
|
|
|
class OoSPatchExtensions(APPatchExtension):
|
|
game = "The Legend of Zelda - Oracle of Seasons"
|
|
|
|
@staticmethod
|
|
def apply_patches(caller: APProcedurePatch, rom: bytes, patch_file: str) -> bytes:
|
|
from .. import OracleOfSeasonsWorld
|
|
rom_data = RomData(rom)
|
|
patch_data = json.loads(caller.get_file(patch_file).decode("utf-8"))
|
|
|
|
version = patch_data["version"].split(".")
|
|
world_version = OracleOfSeasonsWorld.world_version
|
|
if int(version[0]) != world_version.major or int(version[1]) > world_version.minor:
|
|
raise Exception(f"Invalid version: this patch was generated on v{patch_data['version']}, "
|
|
f"you are currently using v{world_version.as_simple_string()}")
|
|
|
|
if patch_data["options"]["cross_items"]:
|
|
file_name = get_settings().tloz_oos_options.ages_rom_file
|
|
file_path = Utils.user_path(file_name)
|
|
rom_file = open(file_path, "rb")
|
|
ages_rom = bytes(rom_file.read())
|
|
rom_file.close()
|
|
|
|
for bank in range(0x40, 0x80):
|
|
bank = 0xdd # TODO: this is an invalid instruction that hangs the game, it's easier to debug but looks worse, remove/comment out once stable
|
|
rom_data.add_bank(bank)
|
|
rom_data.update_rom_size()
|
|
else:
|
|
ages_rom = bytes()
|
|
|
|
# Initialize random seed with the one used for generation + the player ID, so that cosmetic stuff set
|
|
# to "random" always generate the same for successive patchings for a given slot
|
|
random.seed(patch_data["seed"] + caller.player)
|
|
|
|
assembler = Z80Assembler(CAVE_DATA, DEFINES, rom, ages_rom)
|
|
dictionary, texts = get_modded_seasons_text_data(rom_data)
|
|
if patch_data["options"]["cross_items"]:
|
|
if texts["TX_0053"] == "": # Check if cane text exists
|
|
# If not, add the Ages texts
|
|
apply_ages_edits(texts, RomData(ages_rom))
|
|
|
|
# Generate dungeon entrance/exit data
|
|
dungeon_entrances = dict(DUNGEON_ENTRANCES)
|
|
dungeon_exits = dict(DUNGEON_EXITS)
|
|
if patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.samasa:
|
|
dungeon_entrances["d11"] = {
|
|
"map_tile": 0xcf,
|
|
"room": 0xcf,
|
|
"group": 0x00,
|
|
"position": 0x55
|
|
}
|
|
if patch_data["options"]["linked_heros_cave"]:
|
|
dungeon_exits["d11"] = GameboyAddress(0x04, 0x7b35).address_in_rom()
|
|
room_data = apply_room_edits(rom_data, patch_data)
|
|
|
|
# Define assembly constants & floating chunks
|
|
item_data = define_foreign_item_data(assembler, texts, patch_data)
|
|
define_location_constants(assembler, patch_data, item_data)
|
|
define_option_constants(assembler, patch_data)
|
|
define_season_constants(assembler, patch_data)
|
|
make_text_data(assembler, texts, patch_data)
|
|
define_compass_rooms_table(assembler, patch_data, item_data)
|
|
define_collect_properties_table(assembler, patch_data, item_data)
|
|
define_additional_tile_replacements(assembler, patch_data)
|
|
define_samasa_combination(assembler, patch_data)
|
|
define_dungeon_items_text_constants(texts, patch_data)
|
|
define_essence_sparkle_constants(assembler, patch_data, dungeon_entrances)
|
|
define_lost_woods_sequences(assembler, texts, patch_data)
|
|
define_tree_sprites(assembler, patch_data, item_data)
|
|
set_file_select_text(assembler, caller.player_name)
|
|
set_player_start_inventory(assembler, patch_data)
|
|
randomize_puzzles(rom_data, assembler, room_data, patch_data)
|
|
if not hasattr(get_settings().tloz_oos_options, "beat_tutorial"):
|
|
set_faq_trap(assembler)
|
|
|
|
# Parse assembler files, compile them and write the result in the ROM
|
|
print("Compiling ASM files...")
|
|
write_text_data(rom_data, dictionary, texts, True)
|
|
write_room_data(rom_data, room_data, True)
|
|
for file_path in get_asm_files(patch_data):
|
|
data_loaded = yaml.safe_load(pkgutil.get_data(__name__, file_path))
|
|
for metalabel, contents in data_loaded.items():
|
|
assembler.add_block(Z80Block(metalabel, contents))
|
|
assembler.compile_all()
|
|
for block in assembler.blocks:
|
|
rom_data.write_bytes(block.addr.address_in_rom(), block.byte_array)
|
|
|
|
if patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.samasa:
|
|
dungeon_entrances["d11"]["addr"] = assembler.global_labels["warpSourceDesert"].address_in_rom() + 2
|
|
|
|
# Perform direct edits on the ROM
|
|
alter_treasure_types(rom_data, item_data)
|
|
write_chest_contents(rom_data, patch_data, item_data)
|
|
set_old_men_rupee_values(rom_data, patch_data)
|
|
set_dungeon_warps(rom_data, patch_data, dungeon_entrances, dungeon_exits)
|
|
set_portal_warps(rom_data, patch_data)
|
|
apply_miscellaneous_options(rom_data, patch_data)
|
|
set_fixed_subrosia_seaside_location(rom_data, patch_data)
|
|
if patch_data["options"]["randomize_ai"]:
|
|
randomize_ai_for_april_fools(rom_data, patch_data["seed"] + caller.player)
|
|
|
|
# Apply cosmetic settings
|
|
set_heart_beep_interval_from_settings(rom_data)
|
|
set_character_sprite_from_settings(rom_data)
|
|
inject_slot_name(rom_data, caller.player_name)
|
|
|
|
rom_data.update_header_checksum()
|
|
rom_data.update_checksum(0x14e)
|
|
return rom_data.output()
|
|
|
|
|
|
class OoSProcedurePatch(APProcedurePatch, APTokenMixin):
|
|
hash = [ROM_HASH]
|
|
patch_file_ending: str = ".apoos"
|
|
result_file_ending: str = ".gbc"
|
|
|
|
game = "The Legend of Zelda - Oracle of Seasons"
|
|
procedure = [
|
|
("apply_patches", ["patch.json"])
|
|
]
|
|
|
|
@classmethod
|
|
def get_source_data(cls) -> bytes:
|
|
base_rom_bytes = getattr(cls, "base_rom_bytes", None)
|
|
if not base_rom_bytes:
|
|
file_name = get_settings().tloz_oos_options.rom_file
|
|
file_name = Utils.user_path(file_name)
|
|
|
|
rom_file = open(file_name, "rb")
|
|
base_rom_bytes = bytes(rom_file.read())
|
|
rom_file.close()
|
|
|
|
setattr(cls, "base_rom_bytes", base_rom_bytes)
|
|
return base_rom_bytes
|