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
157 lines
5.0 KiB
Python
157 lines
5.0 KiB
Python
# The primary distinction between this file and `fe8py` is that this file is
|
|
# primarily concerned with interfacing the FE8 world with Archipelago, whereas
|
|
# `fe8py` makes semantic changes to the game itself (meaning the core
|
|
# randomization, stat tweaks, etc).
|
|
|
|
import json
|
|
from random import Random
|
|
from typing import TYPE_CHECKING
|
|
|
|
from worlds.Files import (
|
|
APTokenMixin,
|
|
APTokenTypes,
|
|
APProcedurePatch,
|
|
APPatchExtension,
|
|
)
|
|
from settings import get_settings
|
|
|
|
from .items import FE8Item
|
|
from .locations import FE8Location
|
|
from .constants import FE8_NAME, ROM_BASE_ADDRESS
|
|
from .options import FE8Options
|
|
from .connector_config import (
|
|
SLOT_NAME_ADDR,
|
|
SUPER_DEMON_KING_OFFS,
|
|
LOCKPICK_USABILITY_OFFS,
|
|
DEATH_LINK_KIND_OFFS,
|
|
LOCATION_INFO_OFFS,
|
|
LOCATION_INFO_SIZE,
|
|
)
|
|
from .fe8py import FE8Randomizer
|
|
|
|
if TYPE_CHECKING:
|
|
from . import FE8World
|
|
|
|
SLOT_NAME_OFFS = SLOT_NAME_ADDR - ROM_BASE_ADDRESS
|
|
|
|
BASE_PATCH = "data/base_patch.bsdiff4"
|
|
PATCH_FILE_EXT = ".apfe8"
|
|
|
|
AP_ITEM_KIND = 1
|
|
SELF_ITEM_KIND = 2
|
|
|
|
|
|
class FE8PatchExtension(APPatchExtension):
|
|
game = FE8_NAME
|
|
|
|
@staticmethod
|
|
def apply_gameplay_changes(caller: APProcedurePatch, rom: bytes) -> bytes:
|
|
config = json.loads(caller.get_file("config.json").decode("UTF-8"))
|
|
random = Random(config["seed"] + config["player"])
|
|
mut_rom = bytearray(rom)
|
|
randomizer = FE8Randomizer(rom=mut_rom, random=random, config=config)
|
|
randomizer.apply_base_changes()
|
|
|
|
if config["shuffle_skirmish_tables"]:
|
|
randomizer.randomize_monster_gen()
|
|
|
|
if config["easier_5x"]:
|
|
randomizer.apply_5x_buffs()
|
|
|
|
if config["unbreakable_regalia"]:
|
|
randomizer.apply_infinite_holy_weapons()
|
|
|
|
if config["normalize_genders"]:
|
|
randomizer.normalize_genders()
|
|
|
|
randomizer.randomize_growths(*config["growth_rando"])
|
|
randomizer.randomize_music(config["music_rando"])
|
|
|
|
return bytes(mut_rom)
|
|
|
|
|
|
class FE8ProcedurePatch(APProcedurePatch, APTokenMixin):
|
|
game = FE8_NAME
|
|
hash = "005531fef9efbb642095fb8f64645236"
|
|
patch_file_ending = PATCH_FILE_EXT
|
|
result_file_ending = ".gba"
|
|
|
|
procedure = [
|
|
("apply_bsdiff4", ["base_patch.bsdiff4"]),
|
|
("apply_tokens", ["token_data.bin"]),
|
|
("apply_gameplay_changes", []),
|
|
]
|
|
|
|
# CR-someday cam: Should we implement size checks?
|
|
def write_byte(self, offs: int, val: int):
|
|
self.write_token(APTokenTypes.WRITE, offs, bytes([val]))
|
|
|
|
def write_bytes_le(self, offs: int, val: int, size: int):
|
|
data = bytes((val >> 8 * i) & 0xFF for i in range(size))
|
|
self.write_token(APTokenTypes.WRITE, offs, data)
|
|
|
|
def write_short_le(self, addr: int, val: int):
|
|
self.write_bytes_le(addr, val, 2)
|
|
|
|
@classmethod
|
|
def get_source_data(cls):
|
|
return get_base_rom_as_bytes()
|
|
|
|
|
|
def get_base_rom_as_bytes() -> bytes:
|
|
with open(get_settings().fe8_settings.rom_file, "rb") as infile:
|
|
base_rom_bytes = bytes(infile.read())
|
|
|
|
return base_rom_bytes
|
|
|
|
|
|
def rom_location(loc: FE8Location):
|
|
return LOCATION_INFO_OFFS + loc.local_address * LOCATION_INFO_SIZE
|
|
|
|
|
|
def write_tokens(world: "FE8World", patch: FE8ProcedurePatch):
|
|
player = world.player
|
|
multiworld = world.multiworld
|
|
options: FE8Options = world.options
|
|
config_dict = {
|
|
"player_rando": bool(options.player_unit_rando),
|
|
"player_monster": bool(options.player_unit_monsters),
|
|
"easier_5x": bool(options.easier_5x),
|
|
"unbreakable_regalia": bool(options.unbreakable_regalia),
|
|
"shuffle_skirmish_tables": bool(options.shuffle_skirmish_tables),
|
|
"normalize_genders": bool(options.normalize_genders),
|
|
"growth_rando": (
|
|
int(options.growth_rando),
|
|
int(options.growth_rando_min),
|
|
int(options.growth_rando_max),
|
|
),
|
|
"music_rando": int(options.music_rando),
|
|
"seed": multiworld.seed,
|
|
"player": player,
|
|
}
|
|
patch.write_file("config.json", json.dumps(config_dict).encode("UTF-8"))
|
|
|
|
# Player name
|
|
player_name = multiworld.player_name[player]
|
|
name_bytes = player_name.encode("utf-8")
|
|
if len(name_bytes) > 63:
|
|
raise Exception(f"FE8: Player name {player_name} is too long (max 63 bytes)")
|
|
|
|
patch.write_token(APTokenTypes.WRITE, SLOT_NAME_OFFS, name_bytes)
|
|
|
|
for location in multiworld.get_locations(player):
|
|
assert isinstance(location, FE8Location)
|
|
rom_loc = rom_location(location)
|
|
if location.item and location.item.player == player:
|
|
assert isinstance(location.item, FE8Item)
|
|
patch.write_short_le(rom_loc, SELF_ITEM_KIND)
|
|
patch.write_short_le(rom_loc + 2, location.item.local_code)
|
|
else:
|
|
patch.write_short_le(rom_loc, AP_ITEM_KIND)
|
|
|
|
patch.write_byte(SUPER_DEMON_KING_OFFS, int(bool(options.super_demon_king)))
|
|
patch.write_byte(LOCKPICK_USABILITY_OFFS, int(options.lockpick_usability))
|
|
patch.write_byte(DEATH_LINK_KIND_OFFS, int(options.death_link))
|
|
|
|
patch.write_file("token_data.bin", patch.get_token_binary())
|