208 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			208 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from typing import TYPE_CHECKING, Set
 | |
| from .locations import base_id
 | |
| from .text import cv64_text_wrap, cv64_string_to_bytearray
 | |
| 
 | |
| from NetUtils import ClientStatus
 | |
| import worlds._bizhawk as bizhawk
 | |
| import base64
 | |
| from worlds._bizhawk.client import BizHawkClient
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from worlds._bizhawk.context import BizHawkClientContext
 | |
| 
 | |
| 
 | |
| class Castlevania64Client(BizHawkClient):
 | |
|     game = "Castlevania 64"
 | |
|     system = "N64"
 | |
|     patch_suffix = ".apcv64"
 | |
|     self_induced_death = False
 | |
|     received_deathlinks = 0
 | |
|     death_causes = []
 | |
|     currently_shopping = False
 | |
|     local_checked_locations: Set[int]
 | |
| 
 | |
|     def __init__(self) -> None:
 | |
|         super().__init__()
 | |
|         self.local_checked_locations = set()
 | |
| 
 | |
|     async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
 | |
|         from CommonClient import logger
 | |
| 
 | |
|         try:
 | |
|             # Check ROM name/patch version
 | |
|             game_names = await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 0x14, "ROM"), (0xBFBFD0, 12, "ROM")])
 | |
|             if game_names[0].decode("ascii") != "CASTLEVANIA         ":
 | |
|                 return False
 | |
|             if game_names[1] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00':
 | |
|                 logger.info("ERROR: You appear to be running an unpatched version of Castlevania 64. "
 | |
|                             "You need to generate a patch file and use it to create a patched ROM.")
 | |
|                 return False
 | |
|             if game_names[1].decode("ascii") != "ARCHIPELAGO1":
 | |
|                 logger.info("ERROR: The patch file used to create this ROM is not compatible with "
 | |
|                             "this client. Double check your client version against the version being "
 | |
|                             "used by the generator.")
 | |
|                 return False
 | |
|         except UnicodeDecodeError:
 | |
|             return False
 | |
|         except bizhawk.RequestFailedError:
 | |
|             return False  # Should verify on the next pass
 | |
| 
 | |
|         ctx.game = self.game
 | |
|         ctx.items_handling = 0b001
 | |
|         ctx.want_slot_data = False
 | |
|         ctx.watcher_timeout = 0.125
 | |
|         return True
 | |
| 
 | |
|     async def set_auth(self, ctx: "BizHawkClientContext") -> None:
 | |
|         auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(0xBFBFE0, 16, "ROM")]))[0]
 | |
|         ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
 | |
| 
 | |
|     def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
 | |
|         if cmd != "Bounced":
 | |
|             return
 | |
|         if "tags" not in args:
 | |
|             return
 | |
|         if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
 | |
|             self.received_deathlinks += 1
 | |
|             if "cause" in args["data"]:
 | |
|                 cause = args["data"]["cause"]
 | |
|                 if len(cause) > 88:
 | |
|                     cause = cause[0x00:0x89]
 | |
|             else:
 | |
|                 cause = f"{args['data']['source']} killed you!"
 | |
|             self.death_causes.append(cause)
 | |
| 
 | |
|     async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
 | |
| 
 | |
|         try:
 | |
|             read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x342084, 4, "RDRAM"),
 | |
|                                                               (0x389BDE, 6, "RDRAM"),
 | |
|                                                               (0x389BE4, 224, "RDRAM"),
 | |
|                                                               (0x389EFB, 1, "RDRAM"),
 | |
|                                                               (0x389EEF, 1, "RDRAM"),
 | |
|                                                               (0xBFBFDE, 2, "ROM")])
 | |
| 
 | |
|             game_state = int.from_bytes(read_state[0], "big")
 | |
|             save_struct = read_state[2]
 | |
|             written_deathlinks = int.from_bytes(bytearray(read_state[1][4:6]), "big")
 | |
|             deathlink_induced_death = int.from_bytes(bytearray(read_state[1][0:1]), "big")
 | |
|             cutscene_value = int.from_bytes(read_state[3], "big")
 | |
|             current_menu = int.from_bytes(read_state[4], "big")
 | |
|             num_received_items = int.from_bytes(bytearray(save_struct[0xDA:0xDC]), "big")
 | |
|             rom_flags = int.from_bytes(read_state[5], "big")
 | |
| 
 | |
|             # Make sure we are in the Gameplay or Credits states before detecting sent locations and/or DeathLinks.
 | |
|             # If we are in any other state, such as the Game Over state, set self_induced_death to false, so we can once
 | |
|             # again send a DeathLink once we are back in the Gameplay state.
 | |
|             if game_state not in [0x00000002, 0x0000000B]:
 | |
|                 self.self_induced_death = False
 | |
|                 return
 | |
| 
 | |
|             # Enable DeathLink if the bit for it is set in our ROM flags.
 | |
|             if "DeathLink" not in ctx.tags and rom_flags & 0x0100:
 | |
|                 await ctx.update_death_link(True)
 | |
| 
 | |
|             # Scout the Renon shop locations if the shopsanity flag is written in the ROM.
 | |
|             if rom_flags & 0x0001 and ctx.locations_info == {}:
 | |
|                 await ctx.send_msgs([{
 | |
|                         "cmd": "LocationScouts",
 | |
|                         "locations": [base_id + i for i in range(0x1C8, 0x1CF)],
 | |
|                         "create_as_hint": 0
 | |
|                     }])
 | |
| 
 | |
|             # Send a DeathLink if we died on our own independently of receiving another one.
 | |
|             if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
 | |
|                     deathlink_induced_death:
 | |
|                 self.self_induced_death = True
 | |
|                 if save_struct[0xA4] & 0x08:
 | |
|                     # Special death message for dying while having the Vamp status.
 | |
|                     await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!")
 | |
|                 else:
 | |
|                     await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!")
 | |
| 
 | |
|             # Write any DeathLinks received along with the corresponding death cause starting with the oldest.
 | |
|             # To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
 | |
|             if self.received_deathlinks and not self.self_induced_death and not written_deathlinks:
 | |
|                 death_text, num_lines = cv64_text_wrap(self.death_causes[0], 96)
 | |
|                 await bizhawk.write(ctx.bizhawk_ctx, [(0x389BE3, [0x01], "RDRAM"),
 | |
|                                                       (0x389BDF, [0x11], "RDRAM"),
 | |
|                                                       (0x18BF98, bytearray([0xA2, 0x0B]) +
 | |
|                                                        cv64_string_to_bytearray(death_text, False), "RDRAM"),
 | |
|                                                       (0x18C097, [num_lines], "RDRAM")])
 | |
|                 self.received_deathlinks -= 1
 | |
|                 del self.death_causes[0]
 | |
|             else:
 | |
|                 # If the game hasn't received all items yet, the received item struct doesn't contain an item, the
 | |
|                 # current number of received items still matches what we read before, and there are no open text boxes,
 | |
|                 # then fill it with the next item and write the "item from player" text in its buffer. The game will
 | |
|                 # increment the number of received items on its own.
 | |
|                 if num_received_items < len(ctx.items_received):
 | |
|                     next_item = ctx.items_received[num_received_items]
 | |
|                     if next_item.flags & 0b001:
 | |
|                         text_color = bytearray([0xA2, 0x0C])
 | |
|                     elif next_item.flags & 0b010:
 | |
|                         text_color = bytearray([0xA2, 0x0A])
 | |
|                     elif next_item.flags & 0b100:
 | |
|                         text_color = bytearray([0xA2, 0x0B])
 | |
|                     else:
 | |
|                         text_color = bytearray([0xA2, 0x02])
 | |
|                     received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n"
 | |
|                                                               f"from {ctx.player_names[next_item.player]}", 96)
 | |
|                     await bizhawk.guarded_write(ctx.bizhawk_ctx,
 | |
|                                                 [(0x389BE1, [next_item.item & 0xFF], "RDRAM"),
 | |
|                                                  (0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False),
 | |
|                                                   "RDRAM"),
 | |
|                                                  (0x18C1A7, [num_lines], "RDRAM")],
 | |
|                                                 [(0x389BE1, [0x00], "RDRAM"),   # Remote item reward buffer
 | |
|                                                  (0x389CBE, save_struct[0xDA:0xDC], "RDRAM"),  # Received items
 | |
|                                                  (0x342891, [0x02], "RDRAM")])   # Textbox state
 | |
| 
 | |
|             flag_bytes = bytearray(save_struct[0x00:0x44]) + bytearray(save_struct[0x90:0x9F])
 | |
|             locs_to_send = set()
 | |
| 
 | |
|             # Check for set location flags.
 | |
|             for byte_i, byte in enumerate(flag_bytes):
 | |
|                 for i in range(8):
 | |
|                     and_value = 0x80 >> i
 | |
|                     if byte & and_value != 0:
 | |
|                         flag_id = byte_i * 8 + i
 | |
| 
 | |
|                         location_id = flag_id + base_id
 | |
|                         if location_id in ctx.server_locations:
 | |
|                             locs_to_send.add(location_id)
 | |
| 
 | |
|             # Send locations if there are any to send.
 | |
|             if locs_to_send != self.local_checked_locations:
 | |
|                 self.local_checked_locations = locs_to_send
 | |
| 
 | |
|                 if locs_to_send is not None:
 | |
|                     await ctx.send_msgs([{
 | |
|                         "cmd": "LocationChecks",
 | |
|                         "locations": list(locs_to_send)
 | |
|                     }])
 | |
| 
 | |
|             # Check the menu value to see if we are in Renon's shop, and set currently_shopping to True if we are.
 | |
|             if current_menu == 0xA:
 | |
|                 self.currently_shopping = True
 | |
| 
 | |
|             # If we are currently shopping, and the current menu value is 0 (meaning we just left the shop), hint the
 | |
|             # un-bought shop locations that have progression.
 | |
|             if current_menu == 0 and self.currently_shopping:
 | |
|                 await ctx.send_msgs([{
 | |
|                     "cmd": "LocationScouts",
 | |
|                     "locations": [loc for loc, n_item in ctx.locations_info.items() if n_item.flags & 0b001],
 | |
|                     "create_as_hint": 2
 | |
|                 }])
 | |
|                 self.currently_shopping = False
 | |
| 
 | |
|             # Send game clear if we're in either any ending cutscene or the credits state.
 | |
|             if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
 | |
|                 await ctx.send_msgs([{
 | |
|                     "cmd": "StatusUpdate",
 | |
|                     "status": ClientStatus.CLIENT_GOAL
 | |
|                 }])
 | |
| 
 | |
|         except bizhawk.RequestFailedError:
 | |
|             # Exit handler and return to main loop to reconnect.
 | |
|             pass
 | 
