654 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			654 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import logging
 | 
						|
 | 
						|
from BaseClasses import ItemClassification, Location, Item
 | 
						|
from .data import iname, rname
 | 
						|
from .options import CV64Options, BackgroundMusic, Countdown, IceTrapAppearance, InvisibleItems, CharacterStages
 | 
						|
from .stages import vanilla_stage_order, get_stage_info
 | 
						|
from .locations import get_location_info, base_id
 | 
						|
from .regions import get_region_info
 | 
						|
from .items import get_item_info, item_info
 | 
						|
 | 
						|
from typing import TYPE_CHECKING, Dict, List, Tuple, Union, Iterable
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from . import CV64World
 | 
						|
 | 
						|
rom_sub_weapon_offsets = {
 | 
						|
    0x10C6EB: (b"\x10", rname.forest_of_silence),  # Forest
 | 
						|
    0x10C6F3: (b"\x0F", rname.forest_of_silence),
 | 
						|
    0x10C6FB: (b"\x0E", rname.forest_of_silence),
 | 
						|
    0x10C703: (b"\x0D", rname.forest_of_silence),
 | 
						|
 | 
						|
    0x10C81F: (b"\x0F", rname.castle_wall),  # Castle Wall
 | 
						|
    0x10C827: (b"\x10", rname.castle_wall),
 | 
						|
    0x10C82F: (b"\x0E", rname.castle_wall),
 | 
						|
    0x7F9A0F: (b"\x0D", rname.castle_wall),
 | 
						|
 | 
						|
    0x83A5D9: (b"\x0E", rname.villa),  # Villa
 | 
						|
    0x83A5E5: (b"\x0D", rname.villa),
 | 
						|
    0x83A5F1: (b"\x0F", rname.villa),
 | 
						|
    0xBFC903: (b"\x10", rname.villa),
 | 
						|
    0x10C987: (b"\x10", rname.villa),
 | 
						|
    0x10C98F: (b"\x0D", rname.villa),
 | 
						|
    0x10C997: (b"\x0F", rname.villa),
 | 
						|
    0x10CF73: (b"\x10", rname.villa),
 | 
						|
 | 
						|
    0x10CA57: (b"\x0D", rname.tunnel),  # Tunnel
 | 
						|
    0x10CA5F: (b"\x0E", rname.tunnel),
 | 
						|
    0x10CA67: (b"\x10", rname.tunnel),
 | 
						|
    0x10CA6F: (b"\x0D", rname.tunnel),
 | 
						|
    0x10CA77: (b"\x0F", rname.tunnel),
 | 
						|
    0x10CA7F: (b"\x0E", rname.tunnel),
 | 
						|
 | 
						|
    0x10CBC7: (b"\x0E", rname.castle_center),  # Castle Center
 | 
						|
    0x10CC0F: (b"\x0D", rname.castle_center),
 | 
						|
    0x10CC5B: (b"\x0F", rname.castle_center),
 | 
						|
 | 
						|
    0x10CD3F: (b"\x0E", rname.tower_of_execution),  # Character towers
 | 
						|
    0x10CD65: (b"\x0D", rname.tower_of_execution),
 | 
						|
    0x10CE2B: (b"\x0E", rname.tower_of_science),
 | 
						|
    0x10CE83: (b"\x10", rname.duel_tower),
 | 
						|
 | 
						|
    0x10CF8B: (b"\x0F", rname.room_of_clocks),  # Room of Clocks
 | 
						|
    0x10CF93: (b"\x0D", rname.room_of_clocks),
 | 
						|
 | 
						|
    0x99BC5A: (b"\x0D", rname.clock_tower),  # Clock Tower
 | 
						|
    0x10CECB: (b"\x10", rname.clock_tower),
 | 
						|
    0x10CED3: (b"\x0F", rname.clock_tower),
 | 
						|
    0x10CEDB: (b"\x0E", rname.clock_tower),
 | 
						|
    0x10CEE3: (b"\x0D", rname.clock_tower),
 | 
						|
}
 | 
						|
 | 
						|
rom_sub_weapon_flags = {
 | 
						|
    0x10C6EC: b"\x02\x00\xFF\x04",  # Forest of Silence
 | 
						|
    0x10C6FC: b"\x04\x00\xFF\x04",
 | 
						|
    0x10C6F4: b"\x08\x00\xFF\x04",
 | 
						|
    0x10C704: b"\x40\x00\xFF\x04",
 | 
						|
 | 
						|
    0x10C831: b"\x08",  # Castle Wall
 | 
						|
    0x10C829: b"\x10",
 | 
						|
    0x10C821: b"\x20",
 | 
						|
    0xBFCA97: b"\x04",
 | 
						|
 | 
						|
    # Villa
 | 
						|
    0xBFC926: b"\xFF\x04",
 | 
						|
    0xBFC93A: b"\x80",
 | 
						|
    0xBFC93F: b"\x01",
 | 
						|
    0xBFC943: b"\x40",
 | 
						|
    0xBFC947: b"\x80",
 | 
						|
    0x10C989: b"\x10",
 | 
						|
    0x10C991: b"\x20",
 | 
						|
    0x10C999: b"\x40",
 | 
						|
    0x10CF77: b"\x80",
 | 
						|
 | 
						|
    0x10CA58: b"\x40\x00\xFF\x0E",  # Tunnel
 | 
						|
    0x10CA6B: b"\x80",
 | 
						|
    0x10CA60: b"\x10\x00\xFF\x05",
 | 
						|
    0x10CA70: b"\x20\x00\xFF\x05",
 | 
						|
    0x10CA78: b"\x40\x00\xFF\x05",
 | 
						|
    0x10CA80: b"\x80\x00\xFF\x05",
 | 
						|
 | 
						|
    0x10CBCA: b"\x02",  # Castle Center
 | 
						|
    0x10CC10: b"\x80",
 | 
						|
    0x10CC5C: b"\x40",
 | 
						|
 | 
						|
    0x10CE86: b"\x01",  # Duel Tower
 | 
						|
    0x10CD43: b"\x02",  # Tower of Execution
 | 
						|
    0x10CE2E: b"\x20",  # Tower of Science
 | 
						|
 | 
						|
    0x10CF8E: b"\x04",  # Room of Clocks
 | 
						|
    0x10CF96: b"\x08",
 | 
						|
 | 
						|
    0x10CECE: b"\x08",  # Clock Tower
 | 
						|
    0x10CED6: b"\x10",
 | 
						|
    0x10CEE6: b"\x20",
 | 
						|
    0x10CEDE: b"\x80",
 | 
						|
}
 | 
						|
 | 
						|
rom_empty_breakables_flags = {
 | 
						|
    0x10C74D: b"\x40\xFF\x05",  # Forest of Silence
 | 
						|
    0x10C765: b"\x20\xFF\x0E",
 | 
						|
    0x10C774: b"\x08\x00\xFF\x0E",
 | 
						|
    0x10C755: b"\x80\xFF\x05",
 | 
						|
    0x10C784: b"\x01\x00\xFF\x0E",
 | 
						|
    0x10C73C: b"\x02\x00\xFF\x0E",
 | 
						|
 | 
						|
    0x10C8D0: b"\x04\x00\xFF\x0E",  # Villa foyer
 | 
						|
 | 
						|
    0x10CF9F: b"\x08",  # Room of Clocks flags
 | 
						|
    0x10CFA7: b"\x01",
 | 
						|
    0xBFCB6F: b"\x04",  # Room of Clocks candle property IDs
 | 
						|
    0xBFCB73: b"\x05",
 | 
						|
}
 | 
						|
 | 
						|
rom_axe_cross_lower_values = {
 | 
						|
    0x6: [0x7C7F97, 0x07],  # Forest
 | 
						|
    0x8: [0x7C7FA6, 0xF9],
 | 
						|
 | 
						|
    0x30: [0x83A60A, 0x71],  # Villa hallway
 | 
						|
    0x27: [0x83A617, 0x26],
 | 
						|
    0x2C: [0x83A624, 0x6E],
 | 
						|
 | 
						|
    0x16C: [0x850FE6, 0x07],  # Villa maze
 | 
						|
 | 
						|
    0x10A: [0x8C44D3, 0x08],  # CC factory floor
 | 
						|
    0x109: [0x8C44E1, 0x08],
 | 
						|
 | 
						|
    0x74: [0x8DF77C, 0x07],  # CC invention area
 | 
						|
    0x60: [0x90FD37, 0x43],
 | 
						|
    0x55: [0xBFCC2B, 0x43],
 | 
						|
    0x65: [0x90FBA1, 0x51],
 | 
						|
    0x64: [0x90FBAD, 0x50],
 | 
						|
    0x61: [0x90FE56, 0x43]
 | 
						|
}
 | 
						|
 | 
						|
rom_looping_music_fade_ins = {
 | 
						|
    0x10: None,
 | 
						|
    0x11: None,
 | 
						|
    0x12: None,
 | 
						|
    0x13: None,
 | 
						|
    0x14: None,
 | 
						|
    0x15: None,
 | 
						|
    0x16: 0x17,
 | 
						|
    0x18: 0x19,
 | 
						|
    0x1A: 0x1B,
 | 
						|
    0x21: 0x75,
 | 
						|
    0x27: None,
 | 
						|
    0x2E: 0x23,
 | 
						|
    0x39: None,
 | 
						|
    0x45: 0x63,
 | 
						|
    0x56: None,
 | 
						|
    0x57: 0x58,
 | 
						|
    0x59: None,
 | 
						|
    0x5A: None,
 | 
						|
    0x5B: 0x5C,
 | 
						|
    0x5D: None,
 | 
						|
    0x5E: None,
 | 
						|
    0x5F: None,
 | 
						|
    0x60: 0x61,
 | 
						|
    0x62: None,
 | 
						|
    0x64: None,
 | 
						|
    0x65: None,
 | 
						|
    0x66: None,
 | 
						|
    0x68: None,
 | 
						|
    0x69: None,
 | 
						|
    0x6D: 0x78,
 | 
						|
    0x6E: None,
 | 
						|
    0x6F: None,
 | 
						|
    0x73: None,
 | 
						|
    0x74: None,
 | 
						|
    0x77: None,
 | 
						|
    0x79: None
 | 
						|
}
 | 
						|
 | 
						|
music_sfx_ids = [0x1C, 0x4B, 0x4C, 0x4D, 0x4E, 0x55, 0x6C, 0x76]
 | 
						|
 | 
						|
renon_item_dialogue = {
 | 
						|
    0x02: "More Sub-weapon uses!\n"
 | 
						|
          "Just what you need!",
 | 
						|
    0x03: "Galamoth told me it's\n"
 | 
						|
          "a heart in other times.",
 | 
						|
    0x04: "Who needs Warp Rooms\n"
 | 
						|
          "when you have these?",
 | 
						|
    0x05: "I was told to safeguard\n"
 | 
						|
          "this, but I dunno why.",
 | 
						|
    0x06: "Fresh off a Behemoth!\n"
 | 
						|
          "Those cows are weird.",
 | 
						|
    0x07: "Preserved with special\n"
 | 
						|
          " wall-based methods.",
 | 
						|
    0x08: "Don't tell Geneva\n"
 | 
						|
          "about this...",
 | 
						|
    0x09: "If this existed in 1094,\n"
 | 
						|
          "that whip wouldn't...",
 | 
						|
    0x0A: "For when some lizard\n"
 | 
						|
          "brain spits on your ego.",
 | 
						|
    0x0C: "It'd be a shame if you\n"
 | 
						|
          "lost it immediately...",
 | 
						|
    0x10C: "No consequences should\n"
 | 
						|
           "you perish with this!",
 | 
						|
    0x0D: "Arthur was far better\n"
 | 
						|
          "with it than you!",
 | 
						|
    0x0E: "Night Creatures handle\n"
 | 
						|
          "with care!",
 | 
						|
    0x0F: "Some may call it a\n"
 | 
						|
          "\"Banshee Boomerang.\"",
 | 
						|
    0x10: "No weapon triangle\n"
 | 
						|
          "advantages with this.",
 | 
						|
    0x12: "It looks sus? Trust me,"
 | 
						|
          "my wares are genuine.",
 | 
						|
    0x15: "This non-volatile kind\n"
 | 
						|
          "is safe to handle.",
 | 
						|
    0x16: "If you can soul-wield,\n"
 | 
						|
          "they have a good one!",
 | 
						|
    0x17: "Calls the morning sun\n"
 | 
						|
          "to vanquish the night.",
 | 
						|
    0x18: "1 on-demand horrible\n"
 | 
						|
          "night. Devils love it!",
 | 
						|
    0x1A: "Want to study here?\n"
 | 
						|
          "It will cost you.",
 | 
						|
    0x1B: "\"Let them eat cake!\"\n"
 | 
						|
          "Said no princess ever.",
 | 
						|
    0x1C: "Why do I suspect this\n"
 | 
						|
          "was a toilet room?",
 | 
						|
    0x1D: "When you see Coller,\n"
 | 
						|
          "tell him I said hi!",
 | 
						|
    0x1E: "Atomic number is 29\n"
 | 
						|
          "and weight is 63.546.",
 | 
						|
    0x1F: "One torture per pay!\n"
 | 
						|
          "Who will it be?",
 | 
						|
    0x20: "Being here feels like\n"
 | 
						|
          "time is slowing down.",
 | 
						|
    0x21: "Only one thing beind\n"
 | 
						|
          "this. Do you dare?",
 | 
						|
    0x22: "The key 2 Science!\n"
 | 
						|
          "Both halves of it!",
 | 
						|
    0x23: "This warehouse can\n"
 | 
						|
          "be yours for a fee.",
 | 
						|
    0x24: "Long road ahead if you\n"
 | 
						|
          "don't have the others.",
 | 
						|
    0x25: "Will you get the curse\n"
 | 
						|
          "of eternal burning?",
 | 
						|
    0x26: "What's beyond time?\n"
 | 
						|
          "Find out your",
 | 
						|
    0x27: "Want to take out a\n"
 | 
						|
          "loan? By all means!",
 | 
						|
    0x28: "The bag is green,\n"
 | 
						|
          "so it must be lucky!",
 | 
						|
    0x29: "(Does this fool realize?)\n"
 | 
						|
          "Oh, sorry.",
 | 
						|
    "prog": "They will absolutely\n"
 | 
						|
            "need it in time!",
 | 
						|
    "useful": "Now, this would be\n"
 | 
						|
              "useful to send...",
 | 
						|
    "common": "Every last little bit\n"
 | 
						|
              "helps, right?",
 | 
						|
    "trap": "I'll teach this fool\n"
 | 
						|
            " a lesson for a price!",
 | 
						|
    "dlc coin": "1 coin out of... wha!?\n"
 | 
						|
                "You imp, why I oughta!"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
def randomize_lighting(world: "CV64World") -> Dict[int, bytes]:
 | 
						|
    """Generates randomized data for the map lighting table."""
 | 
						|
    randomized_lighting = {}
 | 
						|
    for entry in range(67):
 | 
						|
        for sub_entry in range(19):
 | 
						|
            if sub_entry not in [3, 7, 11, 15] and entry != 4:
 | 
						|
                # The fourth entry in the lighting table affects the lighting on some item pickups; skip it
 | 
						|
                randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = bytes([world.random.randint(0, 255)])
 | 
						|
    return randomized_lighting
 | 
						|
 | 
						|
 | 
						|
def shuffle_sub_weapons(world: "CV64World") -> Dict[int, bytes]:
 | 
						|
    """Shuffles the sub-weapons amongst themselves."""
 | 
						|
    sub_weapon_dict = {offset: rom_sub_weapon_offsets[offset][0] for offset in rom_sub_weapon_offsets if
 | 
						|
                       rom_sub_weapon_offsets[offset][1] in world.active_stage_exits}
 | 
						|
 | 
						|
    # Remove the one 3HB sub-weapon in Tower of Execution if 3HBs are not shuffled.
 | 
						|
    if not world.options.multi_hit_breakables and 0x10CD65 in sub_weapon_dict:
 | 
						|
        del (sub_weapon_dict[0x10CD65])
 | 
						|
 | 
						|
    sub_bytes = list(sub_weapon_dict.values())
 | 
						|
    world.random.shuffle(sub_bytes)
 | 
						|
    return dict(zip(sub_weapon_dict, sub_bytes))
 | 
						|
 | 
						|
 | 
						|
def randomize_music(world: "CV64World") -> Dict[int, bytes]:
 | 
						|
    """Generates randomized or disabled data for all the music in the game."""
 | 
						|
    music_array = bytearray(0x7A)
 | 
						|
    for number in music_sfx_ids:
 | 
						|
        music_array[number] = number
 | 
						|
    if world.options.background_music == BackgroundMusic.option_randomized:
 | 
						|
        looping_songs = []
 | 
						|
        non_looping_songs = []
 | 
						|
        fade_in_songs = {}
 | 
						|
        # Create shuffle-able lists of all the looping, non-looping, and fade-in track IDs
 | 
						|
        for i in range(0x10, len(music_array)):
 | 
						|
            if i not in rom_looping_music_fade_ins.keys() and i not in rom_looping_music_fade_ins.values() and \
 | 
						|
                    i != 0x72:  # Credits song is blacklisted
 | 
						|
                non_looping_songs.append(i)
 | 
						|
            elif i in rom_looping_music_fade_ins.keys():
 | 
						|
                looping_songs.append(i)
 | 
						|
            elif i in rom_looping_music_fade_ins.values():
 | 
						|
                fade_in_songs[i] = i
 | 
						|
        # Shuffle the looping songs
 | 
						|
        rando_looping_songs = looping_songs.copy()
 | 
						|
        world.random.shuffle(rando_looping_songs)
 | 
						|
        looping_songs = dict(zip(looping_songs, rando_looping_songs))
 | 
						|
        # Shuffle the non-looping songs
 | 
						|
        rando_non_looping_songs = non_looping_songs.copy()
 | 
						|
        world.random.shuffle(rando_non_looping_songs)
 | 
						|
        non_looping_songs = dict(zip(non_looping_songs, rando_non_looping_songs))
 | 
						|
        non_looping_songs[0x72] = 0x72
 | 
						|
        # Figure out the new fade-in songs if applicable
 | 
						|
        for vanilla_song in looping_songs:
 | 
						|
            if rom_looping_music_fade_ins[vanilla_song]:
 | 
						|
                if rom_looping_music_fade_ins[looping_songs[vanilla_song]]:
 | 
						|
                    fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = rom_looping_music_fade_ins[
 | 
						|
                        looping_songs[vanilla_song]]
 | 
						|
                else:
 | 
						|
                    fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = looping_songs[vanilla_song]
 | 
						|
        # Build the new music array
 | 
						|
        for i in range(0x10, len(music_array)):
 | 
						|
            if i in looping_songs.keys():
 | 
						|
                music_array[i] = looping_songs[i]
 | 
						|
            elif i in non_looping_songs.keys():
 | 
						|
                music_array[i] = non_looping_songs[i]
 | 
						|
            else:
 | 
						|
                music_array[i] = fade_in_songs[i]
 | 
						|
    del (music_array[0x00: 0x10])
 | 
						|
 | 
						|
    return {0xBFCD30: bytes(music_array)}
 | 
						|
 | 
						|
 | 
						|
def randomize_shop_prices(world: "CV64World") -> Dict[int, bytes]:
 | 
						|
    """Randomize the shop prices based on the minimum and maximum values chosen.
 | 
						|
    The minimum price will adjust if it's higher than the max."""
 | 
						|
    min_price = world.options.minimum_gold_price.value
 | 
						|
    max_price = world.options.maximum_gold_price.value
 | 
						|
 | 
						|
    if min_price > max_price:
 | 
						|
        min_price = world.random.randint(0, max_price)
 | 
						|
        logging.warning(f"[{world.multiworld.player_name[world.player]}] The Minimum Gold Price "
 | 
						|
                        f"({world.options.minimum_gold_price.value * 100}) is higher than the "
 | 
						|
                        f"Maximum Gold Price ({max_price * 100}). Lowering the minimum to: {min_price * 100}")
 | 
						|
        world.options.minimum_gold_price.value = min_price
 | 
						|
 | 
						|
    shop_price_list = [world.random.randint(min_price * 100, max_price * 100) for _ in range(7)]
 | 
						|
 | 
						|
    # Convert the price list into a data dict.
 | 
						|
    price_dict = {}
 | 
						|
    for i in range(len(shop_price_list)):
 | 
						|
        price_dict[0x103D6C + (i * 12)] = int.to_bytes(shop_price_list[i], 4, "big")
 | 
						|
 | 
						|
    return price_dict
 | 
						|
 | 
						|
 | 
						|
def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, bytes]:
 | 
						|
    """Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
 | 
						|
    increase a number.
 | 
						|
 | 
						|
    First, check the location's info to see if it has a countdown number override.
 | 
						|
    If not, then figure it out based on the parent region's stage's position in the vanilla stage order.
 | 
						|
    If the parent region is not part of any stage (as is the case for Renon's shop), skip the location entirely."""
 | 
						|
    countdown_list = [0 for _ in range(15)]
 | 
						|
    for loc in active_locations:
 | 
						|
        if loc.address is not None and (options.countdown == Countdown.option_all_locations or
 | 
						|
                                        (options.countdown == Countdown.option_majors
 | 
						|
                                         and loc.item.advancement)):
 | 
						|
 | 
						|
            countdown_number = get_location_info(loc.name, "countdown")
 | 
						|
 | 
						|
            if countdown_number is None:
 | 
						|
                stage = get_region_info(loc.parent_region.name, "stage")
 | 
						|
                if stage is not None:
 | 
						|
                    countdown_number = vanilla_stage_order.index(stage)
 | 
						|
 | 
						|
            if countdown_number is not None:
 | 
						|
                countdown_list[countdown_number] += 1
 | 
						|
 | 
						|
    return {0xBFD818: bytes(countdown_list)}
 | 
						|
 | 
						|
 | 
						|
def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
 | 
						|
        -> Tuple[Dict[int, bytes], List[str], List[bytearray], List[List[Union[int, str, None]]]]:
 | 
						|
    """Gets ALL the item data to go into the ROM. Item data consists of two bytes: the first dictates the appearance of
 | 
						|
    the item, the second determines what the item actually is when picked up. All items from other worlds will be AP
 | 
						|
    items that do nothing when picked up other than set their flag, and their appearance will depend on whether it's
 | 
						|
    another CV64 player's item and, if so, what item it is in their game. Ice Traps can assume the form of any item that
 | 
						|
    is progression, non-progression, or either depending on the player's settings.
 | 
						|
 | 
						|
    Appearance does not matter if it's one of the two NPC-given items (from either Vincent or Heinrich Meyer). For
 | 
						|
    Renon's shop items, a list containing the shop item names, descriptions, and colors will be returned alongside the
 | 
						|
    regular data."""
 | 
						|
 | 
						|
    # Figure out the list of possible Ice Trap appearances to use based on the settings, first and foremost.
 | 
						|
    if world.options.ice_trap_appearance == IceTrapAppearance.option_major_only:
 | 
						|
        allowed_classifications = ["progression", "progression skip balancing"]
 | 
						|
    elif world.options.ice_trap_appearance == IceTrapAppearance.option_junk_only:
 | 
						|
        allowed_classifications = ["filler", "useful"]
 | 
						|
    else:
 | 
						|
        allowed_classifications = ["progression", "progression skip balancing", "filler", "useful"]
 | 
						|
 | 
						|
    trap_appearances = []
 | 
						|
    for item in item_info:
 | 
						|
        if item_info[item]["default classification"] in allowed_classifications and item != "Ice Trap" and \
 | 
						|
                get_item_info(item, "code") is not None:
 | 
						|
            trap_appearances.append(item)
 | 
						|
 | 
						|
    shop_name_list = []
 | 
						|
    shop_desc_list = []
 | 
						|
    shop_colors_list = []
 | 
						|
 | 
						|
    location_bytes = {}
 | 
						|
 | 
						|
    for loc in active_locations:
 | 
						|
        # If the Location is an event, skip it.
 | 
						|
        if loc.address is None:
 | 
						|
            continue
 | 
						|
 | 
						|
        loc_type = get_location_info(loc.name, "type")
 | 
						|
 | 
						|
        # Figure out the item ID bytes to put in each Location here. Write the item itself if either it's the player's
 | 
						|
        # very own, or it belongs to an Item Link that the player is a part of.
 | 
						|
        if loc.item.player == world.player:
 | 
						|
            if loc_type not in ["npc", "shop"] and get_item_info(loc.item.name, "pickup actor id") is not None:
 | 
						|
                location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "pickup actor id")
 | 
						|
            else:
 | 
						|
                location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code") & 0xFF
 | 
						|
        else:
 | 
						|
            # Make the item the unused Wooden Stake - our multiworld item.
 | 
						|
            location_bytes[get_location_info(loc.name, "offset")] = 0x11
 | 
						|
 | 
						|
        # Figure out the item's appearance. If it's a CV64 player's item, change the multiworld item's model to
 | 
						|
        # match what it is. Otherwise, change it to an Archipelago progress or not progress icon. The model "change"
 | 
						|
        # has to be applied to even local items because this is how the game knows to count it on the Countdown.
 | 
						|
        if loc.item.game == "Castlevania 64":
 | 
						|
            location_bytes[get_location_info(loc.name, "offset") - 1] = get_item_info(loc.item.name, "code")
 | 
						|
        elif loc.item.advancement:
 | 
						|
            location_bytes[get_location_info(loc.name, "offset") - 1] = 0x11  # Wooden Stakes are majors
 | 
						|
        else:
 | 
						|
            location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12  # Roses are minors
 | 
						|
 | 
						|
        # If it's a PermaUp, change the item's model to a big PowerUp no matter what.
 | 
						|
        if loc.item.game == "Castlevania 64" and loc.item.code == 0x10C + base_id:
 | 
						|
            location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
 | 
						|
 | 
						|
        # If it's an Ice Trap, change its model to one of the appearances we determined before.
 | 
						|
        # Unless it's an NPC item, in which case use the Ice Trap's regular ID so that it won't decrement the majors
 | 
						|
        # Countdown due to how I set up the NPC items to work.
 | 
						|
        if loc.item.game == "Castlevania 64" and loc.item.code == 0x12 + base_id:
 | 
						|
            if loc_type == "npc":
 | 
						|
                location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12
 | 
						|
            else:
 | 
						|
                location_bytes[get_location_info(loc.name, "offset") - 1] = \
 | 
						|
                    get_item_info(world.random.choice(trap_appearances), "code")
 | 
						|
                # If we chose a PermaUp as our trap appearance, change it to its actual in-game ID of 0x0B.
 | 
						|
                if location_bytes[get_location_info(loc.name, "offset") - 1] == 0x10C:
 | 
						|
                    location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
 | 
						|
 | 
						|
        # Apply the invisibility variable depending on the "invisible items" setting.
 | 
						|
        if (world.options.invisible_items == InvisibleItems.option_vanilla and loc_type == "inv") or \
 | 
						|
                (world.options.invisible_items == InvisibleItems.option_hide_all and loc_type not in ["npc", "shop"]):
 | 
						|
            location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
 | 
						|
        elif world.options.invisible_items == InvisibleItems.option_chance and loc_type not in ["npc", "shop"]:
 | 
						|
            invisible = world.random.randint(0, 1)
 | 
						|
            if invisible:
 | 
						|
                location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
 | 
						|
 | 
						|
        # If it's an Axe or Cross in a higher freestanding location, lower it into grab range.
 | 
						|
        # KCEK made these spawn 3.2 units higher for some reason.
 | 
						|
        if loc.address & 0xFFF in rom_axe_cross_lower_values and loc.item.code & 0xFF in [0x0F, 0x10]:
 | 
						|
            location_bytes[rom_axe_cross_lower_values[loc.address & 0xFFF][0]] = \
 | 
						|
                rom_axe_cross_lower_values[loc.address & 0xFFF][1]
 | 
						|
 | 
						|
        # Figure out the list of shop names, descriptions, and text colors here.
 | 
						|
        if loc.parent_region.name != rname.renon:
 | 
						|
            continue
 | 
						|
 | 
						|
        shop_name = loc.item.name
 | 
						|
        if len(shop_name) > 18:
 | 
						|
            shop_name = shop_name[0:18]
 | 
						|
        shop_name_list.append(shop_name)
 | 
						|
 | 
						|
        if loc.item.player == world.player:
 | 
						|
            shop_desc_list.append([get_item_info(loc.item.name, "code"), None])
 | 
						|
        elif loc.item.game == "Castlevania 64":
 | 
						|
            shop_desc_list.append([get_item_info(loc.item.name, "code"),
 | 
						|
                                   world.multiworld.get_player_name(loc.item.player)])
 | 
						|
        else:
 | 
						|
            if loc.item.game == "DLCQuest" and loc.item.name in ["DLC Quest: Coin Bundle",
 | 
						|
                                                                 "Live Freemium or Die: Coin Bundle"]:
 | 
						|
                if getattr(world.multiworld.worlds[loc.item.player].options, "coinbundlequantity") == 1:
 | 
						|
                    shop_desc_list.append(["dlc coin", world.multiworld.get_player_name(loc.item.player)])
 | 
						|
                    shop_colors_list.append(get_item_text_color(loc))
 | 
						|
                    continue
 | 
						|
 | 
						|
            if loc.item.advancement:
 | 
						|
                shop_desc_list.append(["prog", world.multiworld.get_player_name(loc.item.player)])
 | 
						|
            elif loc.item.classification == ItemClassification.useful:
 | 
						|
                shop_desc_list.append(["useful", world.multiworld.get_player_name(loc.item.player)])
 | 
						|
            elif loc.item.classification == ItemClassification.trap:
 | 
						|
                shop_desc_list.append(["trap", world.multiworld.get_player_name(loc.item.player)])
 | 
						|
            else:
 | 
						|
                shop_desc_list.append(["common", world.multiworld.get_player_name(loc.item.player)])
 | 
						|
 | 
						|
        shop_colors_list.append(get_item_text_color(loc))
 | 
						|
 | 
						|
    return {offset: int.to_bytes(byte, 1, "big") for offset, byte in location_bytes.items()}, shop_name_list,\
 | 
						|
        shop_colors_list, shop_desc_list
 | 
						|
 | 
						|
 | 
						|
def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
 | 
						|
                           active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, bytes]:
 | 
						|
    """Figure out all the bytes for loading zones and map transitions based on which stages are where in the exit data.
 | 
						|
    The same data was used earlier in figuring out the logic. Map transitions consist of two major components: which map
 | 
						|
    to send the player to, and which spot within the map to spawn the player at."""
 | 
						|
 | 
						|
    # Write the byte for the starting stage to send the player to after the intro narration.
 | 
						|
    loading_zone_bytes = {0xB73308: get_stage_info(starting_stage, "start map id")}
 | 
						|
 | 
						|
    for stage in active_stage_exits:
 | 
						|
 | 
						|
        # Start loading zones
 | 
						|
        # If the start zone is the start of the line, have it simply refresh the map.
 | 
						|
        if active_stage_exits[stage]["prev"] == "Menu":
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = b"\xFF"
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x00"
 | 
						|
        elif active_stage_exits[stage]["prev"]:
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = \
 | 
						|
                get_stage_info(active_stage_exits[stage]["prev"], "end map id")
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = \
 | 
						|
                get_stage_info(active_stage_exits[stage]["prev"], "end spawn id")
 | 
						|
 | 
						|
            # Change CC's end-spawn ID to put you at Carrie's exit if appropriate
 | 
						|
            if active_stage_exits[stage]["prev"] == rname.castle_center:
 | 
						|
                if options.character_stages == CharacterStages.option_carrie_only or \
 | 
						|
                        active_stage_exits[rname.castle_center]["alt"] == stage:
 | 
						|
                    loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x03"
 | 
						|
 | 
						|
        # End loading zones
 | 
						|
        if active_stage_exits[stage]["next"]:
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "endzone map offset")] = \
 | 
						|
                get_stage_info(active_stage_exits[stage]["next"], "start map id")
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "endzone spawn offset")] = \
 | 
						|
                get_stage_info(active_stage_exits[stage]["next"], "start spawn id")
 | 
						|
 | 
						|
        # Alternate end loading zones
 | 
						|
        if active_stage_exits[stage]["alt"]:
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "altzone map offset")] = \
 | 
						|
                get_stage_info(active_stage_exits[stage]["alt"], "start map id")
 | 
						|
            loading_zone_bytes[get_stage_info(stage, "altzone spawn offset")] = \
 | 
						|
                get_stage_info(active_stage_exits[stage]["alt"], "start spawn id")
 | 
						|
 | 
						|
    return loading_zone_bytes
 | 
						|
 | 
						|
 | 
						|
def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, bytes]:
 | 
						|
    """Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything
 | 
						|
    has to be handled appropriately."""
 | 
						|
    start_inventory_data = {}
 | 
						|
 | 
						|
    inventory_items_array = [0 for _ in range(35)]
 | 
						|
    total_money = 0
 | 
						|
    total_jewels = 0
 | 
						|
    total_powerups = 0
 | 
						|
    total_ice_traps = 0
 | 
						|
 | 
						|
    items_max = 10
 | 
						|
 | 
						|
    # Raise the items max if Increase Item Limit is enabled.
 | 
						|
    if options.increase_item_limit:
 | 
						|
        items_max = 99
 | 
						|
 | 
						|
    for item in precollected_items:
 | 
						|
        if item.player != player:
 | 
						|
            continue
 | 
						|
 | 
						|
        inventory_offset = get_item_info(item.name, "inventory offset")
 | 
						|
        sub_equip_id = get_item_info(item.name, "sub equip id")
 | 
						|
        # Starting inventory items
 | 
						|
        if inventory_offset is not None:
 | 
						|
            inventory_items_array[inventory_offset] += 1
 | 
						|
            if inventory_items_array[inventory_offset] > items_max and "Special" not in item.name:
 | 
						|
                inventory_items_array[inventory_offset] = items_max
 | 
						|
            if item.name == iname.permaup:
 | 
						|
                if inventory_items_array[inventory_offset] > 2:
 | 
						|
                    inventory_items_array[inventory_offset] = 2
 | 
						|
        # Starting sub-weapon
 | 
						|
        elif sub_equip_id is not None:
 | 
						|
            start_inventory_data[0xBFD883] = bytes(sub_equip_id)
 | 
						|
        # Starting PowerUps
 | 
						|
        elif item.name == iname.powerup:
 | 
						|
            total_powerups += 1
 | 
						|
            # Can't have more than 2 PowerUps.
 | 
						|
            if total_powerups > 2:
 | 
						|
                total_powerups = 2
 | 
						|
        # Starting Gold
 | 
						|
        elif "GOLD" in item.name:
 | 
						|
            total_money += int(item.name[0:4])
 | 
						|
            # Money cannot be higher than 99999.
 | 
						|
            if total_money > 99999:
 | 
						|
                total_money = 99999
 | 
						|
        # Starting Jewels
 | 
						|
        elif "jewel" in item.name:
 | 
						|
            if "L" in item.name:
 | 
						|
                total_jewels += 10
 | 
						|
            else:
 | 
						|
                total_jewels += 5
 | 
						|
            # Jewels cannot be higher than 99.
 | 
						|
            if total_jewels > 99:
 | 
						|
                total_jewels = 99
 | 
						|
        # Starting Ice Traps
 | 
						|
        else:
 | 
						|
            total_ice_traps += 1
 | 
						|
            # Ice Traps cannot be higher than 255.
 | 
						|
            if total_ice_traps > 0xFF:
 | 
						|
                total_ice_traps = 0xFF
 | 
						|
 | 
						|
    # Convert the jewels into data.
 | 
						|
    start_inventory_data[0xBFD867] = bytes([total_jewels])
 | 
						|
 | 
						|
    # Convert the Ice Traps into data.
 | 
						|
    start_inventory_data[0xBFD88B] = bytes([total_ice_traps])
 | 
						|
 | 
						|
    # Convert the inventory items into data.
 | 
						|
    start_inventory_data[0xBFE518] = bytes(inventory_items_array)
 | 
						|
 | 
						|
    # Convert the starting money into data.
 | 
						|
    start_inventory_data[0xBFE514] = int.to_bytes(total_money, 4, "big")
 | 
						|
 | 
						|
    return start_inventory_data
 | 
						|
 | 
						|
 | 
						|
def get_item_text_color(loc: Location) -> bytearray:
 | 
						|
    if loc.item.advancement:
 | 
						|
        return bytearray([0xA2, 0x0C])
 | 
						|
    elif loc.item.classification == ItemClassification.useful:
 | 
						|
        return bytearray([0xA2, 0x0A])
 | 
						|
    elif loc.item.classification == ItemClassification.trap:
 | 
						|
        return bytearray([0xA2, 0x0B])
 | 
						|
    else:
 | 
						|
        return bytearray([0xA2, 0x02])
 |