diff --git a/worlds/cvcotm/__init__.py b/worlds/cvcotm/__init__.py index 86a87404b023..30329b0f0f1b 100644 --- a/worlds/cvcotm/__init__.py +++ b/worlds/cvcotm/__init__.py @@ -16,7 +16,8 @@ from .presets import cvcotm_options_presets from worlds.AutoWorld import WebWorld, World -from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops +from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops,\ + get_start_inventory_data from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \ CVCotM_VC_US_HASH from .client import CastlevaniaCotMClient @@ -177,6 +178,8 @@ def generate_output(self, output_directory: str) -> None: # Countdown if self.options.countdown: offset_data.update(get_countdown_flags(self, active_locations)) + # Start Inventory + offset_data.update(get_start_inventory_data(self.player, self.multiworld.precollected_items[self.player])) patch = CVCotMProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch_rom(self, patch, offset_data) diff --git a/worlds/cvcotm/aesthetics.py b/worlds/cvcotm/aesthetics.py index e45927de5deb..fc5dbf63a0c6 100644 --- a/worlds/cvcotm/aesthetics.py +++ b/worlds/cvcotm/aesthetics.py @@ -1,13 +1,31 @@ -from BaseClasses import ItemClassification, Location +from BaseClasses import ItemClassification, Location, Item from .options import ItemDropRandomization, Countdown from .locations import cvcotm_location_info from .items import cvcotm_item_info +from .data import iname from typing import TYPE_CHECKING, Dict, List, Iterable if TYPE_CHECKING: from . import CVCotMWorld +# amount per = Amount this stat increases per Max Up the player starts with. +# max allowed = The most amount of this stat the player is allowed to start with. Problems arise if the stat exceeds +# 9999, so we must ensure it can't if the player raises any class to level 99 as well as collects 255 of +# that max up. The game caps hearts at 999 automatically, so it doesn't matter so much for that one. +# variable = The key variable in extra_stats that the stat max up affects. +extra_starting_stat_info = { + iname.hp_max: {"amount per": 10, + "max allowed": 5289, + "variable": "extra health"}, + iname.mp_max: {"amount per": 10, + "max allowed": 3129, + "variable": "extra magic"}, + iname.heart_max: {"amount per": 6, + "max allowed": 999, + "variable": "extra hearts"}, +} + # 0 = Holy water 22 # 1 = Axe 24 # 2 = Knife 32 @@ -115,7 +133,7 @@ } easy_items = [ - 1, # Leather Armor + 1, # Leather Armor 12, # Cotton Robe 17, # Cotton Clothes 34, # Wristband @@ -123,14 +141,14 @@ 46, # Antidote 47, # Cure Curse 48, # Mind Restore - 51 # Heart + 51 # Heart ] common_items = easy_items + [ - 2, # Bronze Armor - 3, # Gold Armor - 4, # Chainmail - 5, # Steel Armor + 2, # Bronze Armor + 3, # Gold Armor + 4, # Chainmail + 5, # Steel Armor 13, # Silk Robe 14, # Rainbow Robe @@ -160,10 +178,10 @@ ] rare_items = [ - 6, # Platinum Armor - 7, # Diamond Armor - 8, # Mirror Armor - 9, # Needle Armor + 6, # Platinum Armor + 7, # Diamond Armor + 8, # Mirror Armor + 9, # Needle Armor 10, # Dark Armor 15, # Magic Robe @@ -191,11 +209,11 @@ all_items = rare_items + common_items easily_farmable_enemies = [ - 0, # Medusa Head - 1, # Zombie - 2, # Ghoul - 3, # Wight - 7, # Skeleton Bomber + 0, # Medusa Head + 1, # Zombie + 2, # Ghoul + 3, # Wight + 7, # Skeleton Bomber 14, # Fleaman 16, # Bat 17, # Spirit @@ -213,42 +231,42 @@ ] below_150_hp_enemies = easily_farmable_enemies + [ - 4, # Clinking Man - 5, # Zombie Thief - 8, # Electric Skeleton - 9, # Skeleton Spear - 10, # Skeleton Boomerang - 11, # Skeleton Soldier - 12, # Skeleton Knight - 13, # Bone Tower - 15, # Poltergeist - 20, # Axe Armor - 26, # Earth Armor - 29, # Stone Armor - 35, # Bloody Sword - 41, # Skeleton Athlete - 42, # Harpy - 44, # Imp - 45, # Mudman - 47, # Slime - 48, # Frozen Shade - 49, # Heat Shade - 52, # Will-O-Wisp - 53, # Spearfish - 57, # Marionette - 60, # Evil Pillar - 63, # Bone Head - 64, # Fox Archer - 65, # Fox Hunter - 77, # Hyena - 78, # Fishhead - 79, # Dryad - 81, # Brain Float - 83, # Abiondarg - 86, # Witch - 93, # King Moth - 94, # Killer Bee - 96, # Lizard-man + 4, # Clinking Man + 5, # Zombie Thief + 8, # Electric Skeleton + 9, # Skeleton Spear + 10, # Skeleton Boomerang + 11, # Skeleton Soldier + 12, # Skeleton Knight + 13, # Bone Tower + 15, # Poltergeist + 20, # Axe Armor + 26, # Earth Armor + 29, # Stone Armor + 35, # Bloody Sword + 41, # Skeleton Athlete + 42, # Harpy + 44, # Imp + 45, # Mudman + 47, # Slime + 48, # Frozen Shade + 49, # Heat Shade + 52, # Will-O-Wisp + 53, # Spearfish + 57, # Marionette + 60, # Evil Pillar + 63, # Bone Head + 64, # Fox Archer + 65, # Fox Hunter + 77, # Hyena + 78, # Fishhead + 79, # Dryad + 81, # Brain Float + 83, # Abiondarg + 86, # Witch + 93, # King Moth + 94, # Killer Bee + 96, # Lizard-man 113, # Devil Tower (Battle Arena) 119, # Bone Tower (Battle Arena) 122, # Bloody Sword (Battle Arena) @@ -256,11 +274,11 @@ ] bosses = [ - 68, # Cerberus - 76, # Necromancer - 84, # Iron Golem - 89, # Adramelech - 95, # Zombie Dragon + 68, # Cerberus + 76, # Necromancer + 84, # Iron Golem + 89, # Adramelech + 95, # Zombie Dragon 100, # Death 101, # Camilla 102, # Hugh @@ -270,7 +288,7 @@ candles = [ 136, # Scary Candle 137, # Trick Candle - 80, # Mimic Candle + 80, # Mimic Candle ] NUMBER_ENEMIES = 141 @@ -301,7 +319,7 @@ def get_countdown_flags(world: "CVCotMWorld", active_locations: Iterable[Locatio # array of flags the Countdown will track. for loc in active_locations: if ((loc.item.advancement or loc.item.classification == ItemClassification.useful) - or world.options.countdown == Countdown.option_all_locations) and loc.address is not None: + or world.options.countdown == Countdown.option_all_locations) and loc.address is not None: countdown_index = cvcotm_location_info[loc.name].countdown # If we're looking at a locally-placed DSS Card, take the card's parameter value for the flag. if (loc.item.player == world.player or (loc.item.player in world.multiworld.groups and world.player in @@ -482,3 +500,79 @@ def select_drop(world: "CVCotMWorld", drop_list: List[int], drops_placed: List[i # Return the item ID return drop_list[eligible_items[random_result]] + + +def get_start_inventory_data(player: int, precollected_items: List[Item]) -> Dict[int, bytes]: + """Calculate and return the starting inventory arrays. Different items go into different arrays, so they all have + to be handled accordingly.""" + start_inventory_data = {} + + magic_items_array = [0 for _ in range(8)] + cards_array = [0 for _ in range(20)] + extra_stats = {"extra health": 0, + "extra magic": 0, + "extra hearts": 0} + + # Always start with the Dash Boots. + magic_items_array[0] = 1 + + for item in precollected_items: + if item.player != player: + continue + + array_offset = item.code & 0xFF + + # If it's a Max Up we're starting with, check if increasing the extra amount of that stat will put us over the + # max amount of the stat allowed. If it will, set the current extra amount to the max. Otherwise, increase it by + # the amount that it should. + if "Max Up" in item.name: + info = extra_starting_stat_info[item.name] + if extra_stats[info["variable"]] + info["amount per"] > info["max allowed"]: + extra_stats[info["variable"]] = info["max allowed"] + else: + extra_stats[info["variable"]] += info["amount per"] + # If it's a DSS card we're starting with, set that card's value in the cards array. + elif "Card" in item.name: + cards_array[array_offset] = 1 + # If it's none of the above, it has to be a Magic Item. + # Increase that Magic Item's value in the Magic Items array if it's not greater than 240. Last Keys are the only + # Magic Item wherein having more than one is relevant. + else: + # Decrease the Magic Item array offset by 1 if it's higher than the unused Map's item value. + if array_offset > 5: + array_offset -= 1 + if magic_items_array[array_offset] < 240: + magic_items_array[array_offset] += 1 + + # Add the start inventory arrays to the offset data in bytes form. + start_inventory_data[0x680080] = bytes(magic_items_array) + start_inventory_data[0x6800A0] = bytes(cards_array) + + # Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max + # possible Max Ups. + # Vampire Killer + start_inventory_data[0xE08C6] = int.to_bytes(100 + extra_stats["extra health"], 2, "little") + start_inventory_data[0xE08CE] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little") + start_inventory_data[0xE08D4] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") + + # Magician + start_inventory_data[0xE090E] = int.to_bytes(50 + extra_stats["extra health"], 2, "little") + start_inventory_data[0xE0916] = int.to_bytes(400 + extra_stats["extra magic"], 2, "little") + start_inventory_data[0xE091C] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") + + # Fighter + start_inventory_data[0xE0932] = int.to_bytes(200 + extra_stats["extra health"], 2, "little") + start_inventory_data[0xE093A] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little") + start_inventory_data[0xE0940] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") + + # Shooter + start_inventory_data[0xE0832] = int.to_bytes(50 + extra_stats["extra health"], 2, "little") + start_inventory_data[0xE08F2] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little") + start_inventory_data[0xE08F8] = int.to_bytes(250 + extra_stats["extra hearts"], 2, "little") + + # Thief + start_inventory_data[0xE0956] = int.to_bytes(50 + extra_stats["extra health"], 2, "little") + start_inventory_data[0xE095E] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little") + start_inventory_data[0xE0964] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") + + return start_inventory_data diff --git a/worlds/cvcotm/client.py b/worlds/cvcotm/client.py index 85981b4b9a8b..aa841114a65f 100644 --- a/worlds/cvcotm/client.py +++ b/worlds/cvcotm/client.py @@ -2,6 +2,7 @@ from .locations import base_id, get_location_names_to_ids from .text import cvcotm_string_to_bytearray from .options import CompletionGoal, DeathLink +from .data import iname from NetUtils import ClientStatus import worlds._bizhawk as bizhawk @@ -29,6 +30,11 @@ 0xBC: "FLAG_DEFEATED_DRACULA_II" } +DEATHLINK_AREA_NAMES = ["Sealed Room", "Catacomb", "Abyss Staircase", "Audience Room", "Triumph Hallway", + "Machine Tower", "Eternal Corridor", "Chapel Tower", "Underground Warehouse", + "Underground Gallery", "Underground Waterway", "Outer Wall", "Observation Tower", + "Ceremonial Room", "Battle Arena"] + class CastlevaniaCotMClient(BizHawkClient): game = "Castlevania - Circle of the Moon" @@ -73,7 +79,7 @@ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: return False # Should verify on the next pass ctx.game = self.game - ctx.items_handling = 0b101 + ctx.items_handling = 0b001 ctx.want_slot_data = True ctx.watcher_timeout = 0.125 return True @@ -90,10 +96,12 @@ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name: if "cause" in args["data"]: cause = args["data"]["cause"] + if cause == "": + cause = f"{args['data']['source']} killed you without a word!" if len(cause) > 300: - cause = cause[0x00:0x89] + cause = cause[:300] else: - cause = f"{args['data']['source']} killed you!" + cause = f"{args['data']['source']} killed you without a word!" # Highlight the player that killed us in the game's orange text. if args['data']['source'] in cause: @@ -117,8 +125,8 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: (0x25308, 2, "EWRAM"), (0x26000, 1, "EWRAM"), (0x50, 1, "EWRAM"), - (0x2562C, 4, "EWRAM"), - (0x253FC, 1, "EWRAM")]) + (0x2562E, 18, "EWRAM"), + (0x253FC, 2, "EWRAM")]) game_state = int.from_bytes(read_state[0], "little") flag_bytes = read_state[1] @@ -130,8 +138,17 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: delay_timer = int.from_bytes(bytearray(read_state[7]), "little") cutscene = int.from_bytes(bytearray(read_state[8]), "little") nathan_state = int.from_bytes(bytearray(read_state[9]), "little") - health = int.from_bytes(bytearray(read_state[10]), "little") - area = int.from_bytes(bytearray(read_state[11]), "little") + health_stats_array = bytearray(read_state[10]) + area = int.from_bytes(bytearray(read_state[11][0:1]), "little") + room = int.from_bytes(bytearray(read_state[11][1:]), "little") + + # Get out each of the individual health/magic/heart values. + hp = int.from_bytes(health_stats_array[0:2], "little") + max_hp = int.from_bytes(health_stats_array[4:6], "little") + # mp = int.from_bytes(health_stats_array[8:10], "little") Not used. Not currently, at least. + max_mp = int.from_bytes(health_stats_array[12:14], "little") + hearts = int.from_bytes(health_stats_array[14:16], "little") + max_hearts = int.from_bytes(health_stats_array[16:], "little") # If there's no textbox already queued, the delay timer is 0, we are not in a cutscene, and Nathan's current # state value is not 0x34 (using a save room), we can safely inject a textbox message. @@ -153,9 +170,18 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: await ctx.update_death_link(True) # Send a DeathLink if we died on our own independently of receiving another one. - if "DeathLink" in ctx.tags and health == 0 and not self.currently_dead: + if "DeathLink" in ctx.tags and hp == 0 and not self.currently_dead: self.currently_dead = True - await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!") + + # Check if we are in Dracula II's arena. The game considers this part of the Sealed Room area, + # which I don't think makes sense to be player-facing like this. + if area == 0 and room == 2: + area_of_death = "Dracula's realm" + # If we aren't in Dracula II's arena, then take the name of whatever area the player is currently in. + else: + area_of_death = DEATHLINK_AREA_NAMES[area] + + await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!") # Scout all Locations and get our Set events. if ctx.locations_info == {}: @@ -283,7 +309,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: mssg_sfx_id = 0x1B3 inv_array = max_ups_array - item_name = ctx.item_names[next_item.item] + item_name = ctx.item_names.lookup_in_slot(next_item.item) player_name = ctx.player_names[next_item.player] # Truncate the player name at 50 characters. if len(player_name) > 50: @@ -292,12 +318,33 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: received_text = cvcotm_string_to_bytearray(f"「{item_name}」 received from " f"「{player_name}」◊", "big middle", 0) + # Check if the player has 255 of the item being received. If they do, don't increment that counter + # further. + refill_write = [] + new_total = inv_array[item_index] + 1 + if new_total > 0xFF: + new_total -= 1 + # If it's a stat max up being received, manually give a refill of that item's stat. + # Normally, the game does this automatically by incrementing the number of that max up. + if item_name == iname.hp_max: + refill_write = [(0x2562E, int.to_bytes(max_hp, 2, "little"), "EWRAM")] + elif item_name == iname.mp_max: + refill_write = [(0x25636, int.to_bytes(max_mp, 2, "little"), "EWRAM")] + elif item_name == iname.heart_max: + # If adding +6 Hearts doesn't put us over the player's current max Hearts, do so. + # Otherwise, set the player's current Hearts to the current max. + if hearts + 6 > max_hearts: + new_hearts = max_hearts + else: + new_hearts = hearts + 6 + refill_write = [(0x2563C, int.to_bytes(new_hearts, 2, "little"), "EWRAM")] + await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x25300, [0x1D, 0x82], "EWRAM"), (0x25304, [1], "EWRAM"), (0x25306, bytearray(int.to_bytes(mssg_sfx_id, 2, "little")), "EWRAM"), - (inv_offset + item_index, [inv_array[item_index] + 1], "EWRAM"), - (0x7CEB00, received_text, "ROM")], + (inv_offset + item_index, [new_total], "EWRAM"), + (0x7CEB00, received_text, "ROM")] + refill_write, # Make sure the number of received items is still what we expected it to be. [(0x253D0, read_state[3], "EWRAM")]), diff --git a/worlds/cvcotm/data/ips/AutoDashBoots.ips b/worlds/cvcotm/data/ips/AutoDashBoots.ips deleted file mode 100644 index ff2d501dade9..000000000000 Binary files a/worlds/cvcotm/data/ips/AutoDashBoots.ips and /dev/null differ diff --git a/worlds/cvcotm/data/ips/SeedDisplayAPEdition.ips b/worlds/cvcotm/data/ips/SeedDisplay20Digits.ips similarity index 100% rename from worlds/cvcotm/data/ips/SeedDisplayAPEdition.ips rename to worlds/cvcotm/data/ips/SeedDisplay20Digits.ips diff --git a/worlds/cvcotm/data/patches.py b/worlds/cvcotm/data/patches.py index 935b299514ff..f4f0ca5255f6 100644 --- a/worlds/cvcotm/data/patches.py +++ b/worlds/cvcotm/data/patches.py @@ -109,7 +109,7 @@ 0x05, 0x21, # mov r1, #5 0x88, 0x42, # cmp r0, r1 0x06, 0xD0, # beq 0x87FFE68 - 0xDA, 0x20, # movs r0, #0xDA + 0xDA, 0x20, # mov r0, #0xDA 0x40, 0x00, # lsls r0, r0, #1 0x03, 0x4A, # ldr r2, =0x8005E80 0x7B, 0x46, # mov r3, r15 @@ -123,6 +123,117 @@ 0xEC, 0x5B, 0x09, 0x08, ] +start_inventory_giver = [ + # This replaces AutoDashBoots.ips from standalone CotMR by allowing the player to start with any set of items, not + # just the Dash Boots. If playing Magician Mode, they will be given all cards that were not put into the starting + # inventory right after this code runs. + + # Magic Items + 0x13, 0x48, # ldr r0, =0x202572F + 0x14, 0x49, # ldr r1, =0x8680080 + 0x00, 0x22, # mov r2, #0 + 0x8B, 0x5C, # ldrb r3, [r1, r2] + 0x83, 0x54, # strb r3, [r0, r2] + 0x01, 0x32, # adds r2, #1 + 0x08, 0x2A, # cmp r2, #8 + 0xFA, 0xDB, # blt 0x8680006 + # Max Ups + 0x11, 0x48, # ldr r0, =0x202572C + 0x12, 0x49, # ldr r1, =0x8680090 + 0x00, 0x22, # mov r2, #0 + 0x8B, 0x5C, # ldrb r3, [r1, r2] + 0x83, 0x54, # strb r3, [r0, r2] + 0x01, 0x32, # adds r2, #1 + 0x03, 0x2A, # cmp r2, #3 + 0xFA, 0xDB, # blt 0x8680016 + # Cards + 0x0F, 0x48, # ldr r0, =0x2025674 + 0x10, 0x49, # ldr r1, =0x86800A0 + 0x00, 0x22, # mov r2, #0 + 0x8B, 0x5C, # ldrb r3, [r1, r2] + 0x83, 0x54, # strb r3, [r0, r2] + 0x01, 0x32, # adds r2, #1 + 0x14, 0x2A, # cmp r2, #0x14 + 0xFA, 0xDB, # blt 0x8680026 + # Inventory Items (not currently supported) + 0x0D, 0x48, # ldr r0, =0x20256ED + 0x0E, 0x49, # ldr r1, =0x86800C0 + 0x00, 0x22, # mov r2, #0 + 0x8B, 0x5C, # ldrb r3, [r1, r2] + 0x83, 0x54, # strb r3, [r0, r2] + 0x01, 0x32, # adds r2, #1 + 0x36, 0x2A, # cmp r2, #36 + 0xFA, 0xDB, # blt 0x8680036 + # Return to the function that checks for Magician Mode. + 0xBA, 0x21, # movs r1, #0xBA + 0x89, 0x00, # lsls r1, r1, #2 + 0x70, 0x18, # adds r0, r6, r1 + 0x04, 0x70, # strb r4, [r0] + 0x00, 0x4A, # ldr r2, =0x8007F78 + 0x97, 0x46, # mov r15, r2 + # LDR number pool + 0x78, 0x7F, 0x00, 0x08, + 0x2F, 0x57, 0x02, 0x02, + 0x80, 0x00, 0x68, 0x08, + 0x2C, 0x57, 0x02, 0x02, + 0x90, 0x00, 0x68, 0x08, + 0x74, 0x56, 0x02, 0x02, + 0xA0, 0x00, 0x68, 0x08, + 0xED, 0x56, 0x02, 0x02, + 0xC0, 0x00, 0x68, 0x08, +] + +max_max_up_checker = [ + # Whenever the player picks up a Max Up, this will check to see if they currently have 255 of that particular Max Up + # and only increment the number further if they don't. This is necessary for extreme Item Link seeds, as going over + # 255 of any Max Up will reset the counter to 0. + 0x08, 0x78, # ldrb r0, [r1] + 0xFF, 0x28, # cmp r0, 0xFF + 0x17, 0xD1, # bne 0x86A0036 + # If it's an HP Max, refill our HP. + 0xFF, 0x23, # mov r3, #0xFF + 0x0B, 0x40, # and r3, r1 + 0x2D, 0x2B, # cmp r3, 0x2D + 0x03, 0xD1, # bne 0x86A0016 + 0x0D, 0x4A, # ldr r2, =0x202562E + 0x93, 0x88, # ldrh r3, [r2, #4] + 0x13, 0x80, # strh r3, [r2] + 0x11, 0xE0, # b 0x86A003A + # If it's an MP Max, refill our MP. + 0x2E, 0x2B, # cmp r3, 0x2E + 0x03, 0xD1, # bne 0x86A0022 + 0x0B, 0x4A, # ldr r2, =0x2025636 + 0x93, 0x88, # ldrh r3, [r2, #4] + 0x13, 0x80, # strh r3, [r2] + 0x0B, 0xE0, # b 0x86A003A + # Else, meaning it's a Hearts Max, add +6 Hearts. If adding +6 Hearts would put us over our current max, set our + # current amount to said current max instead. + 0x0A, 0x4A, # ldr r2, =0x202563C + 0x13, 0x88, # ldrh r3, [r2] + 0x06, 0x33, # add r3, #6 + 0x51, 0x88, # ldrh r1, [r2, #2] + 0x8B, 0x42, # cmp r3, r1 + 0x00, 0xDB, # blt 0x86A0030 + 0x0B, 0x1C, # add r3, r1, #0 + 0x13, 0x80, # strh r3, [r2] + 0x02, 0xE0, # b 0x86A003A + 0x00, 0x00, + # Increment the Max Up count like normal. Should only get here if the Max Up count was determined to be less than + # 255, branching past if not the case. + 0x01, 0x30, # adds r0, #1 + 0x08, 0x70, # strb r0, [r1] + # Return to the function that gives Max Ups normally. + 0x05, 0x48, # ldr r0, =0x1B3 + 0x00, 0x4A, # ldr r2, =0x805E170 + 0x97, 0x46, # mov r15, r2 + # LDR number pool + 0x78, 0xE1, 0x05, 0x08, + 0x2E, 0x56, 0x02, 0x02, + 0x36, 0x56, 0x02, 0x02, + 0x3C, 0x56, 0x02, 0x02, + 0xB3, 0x01, 0x00, 0x00, +] + missing_char_data = { # The data for all missing ASCII characters from the game's dialogue textbox font. diff --git a/worlds/cvcotm/rom.py b/worlds/cvcotm/rom.py index e6eadcbf6f45..b719b9fe81ed 100644 --- a/worlds/cvcotm/rom.py +++ b/worlds/cvcotm/rom.py @@ -99,11 +99,6 @@ def apply_ips_patches(caller: APProcedurePatch, rom: bytes, options_file: str) - rom_data = RomData(rom) options = json.loads(caller.get_file(options_file).decode("utf-8")) - # This patch grants Dash Boots on game initialization, effectively giving you Dash Boots from the beginning of - # the game without needing to pick them up. - # Created by DevAnj. - rom_data.apply_ips("AutoDashBoots.ips") - # This patch allows placing DSS cards on pedestals, prevents them from timing out, and removes them from enemy # drop tables. Created by DevAnj but drop and pedestal item replacements have been stripped out. rom_data.apply_ips("CardUp_v3_Custom.ips") @@ -157,9 +152,9 @@ def apply_ips_patches(caller: APProcedurePatch, rom: bytes, options_file: str) - # Created by Fusecavator. rom_data.apply_ips("AllowAlwaysDrop.ips") - # Displays the seed on the pause menu. Created by Fusecavator and modified by Liquid Cat to display a 20-digit - # seed for AP purposes. - rom_data.apply_ips("SeedDisplayAPEdition.ips") + # Displays the seed on the pause menu. Originally created by Fusecavator and modified by Liquid Cat to display a + # 20-digit seed (which AP seeds most commonly are). + rom_data.apply_ips("SeedDisplay20Digits.ips") # Write the seed. Upwards of 20 digits can be displayed for the seed number. curr_seed_addr = 0x672152 @@ -237,6 +232,14 @@ def apply_ips_patches(caller: APProcedurePatch, rom: bytes, options_file: str) - # Everything from this line and below was created and added by Liquid Cat for this Archipelago version. + # Give the player their Start Inventory upon entering their name on a new file. + rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x68, 0x08]) + rom_data.write_bytes(0x680000, patches.start_inventory_giver) + + # Prevent Max Ups from exceeding 255. + rom_data.write_bytes(0x5E170, [0x00, 0x4A, 0x97, 0x46, 0x00, 0x00, 0x6A, 0x08]) + rom_data.write_bytes(0x6A0000, patches.max_max_up_checker) + # Write the textbox messaging system code. rom_data.write_bytes(0x7D60, [0x00, 0x48, 0x87, 0x46, 0x20, 0xFF, 0x7F, 0x08]) rom_data.write_bytes(0x7FFF20, patches.remote_textbox_shower)