Grinch Refactor

This commit is contained in:
2025-10-31 21:24:07 -06:00
parent d0a8df25f6
commit 4e131cd5e4
7 changed files with 202 additions and 197 deletions

View File

@@ -66,34 +66,24 @@ class GrinchClient(BizHawkClient):
try: try:
bytes_actual: bytes = ( bytes_actual: bytes = (
await bizhawk.read( await bizhawk.read(ctx.bizhawk_ctx, [(grinch_identifier_ram_address, 11, "MainRAM")])
ctx.bizhawk_ctx, [(grinch_identifier_ram_address, 11, "MainRAM")]
)
)[0] )[0]
psx_rom_name = bytes_actual.decode("ascii") psx_rom_name = bytes_actual.decode("ascii")
if psx_rom_name != "SLUS_011.97": if psx_rom_name != "SLUS_011.97":
bios_bytes_check: bytes = ( bios_bytes_check: bytes = (
await bizhawk.read( await bizhawk.read(ctx.bizhawk_ctx, [(bios_identifier_ram_address, 24, "MainRAM")])
ctx.bizhawk_ctx, [(bios_identifier_ram_address, 24, "MainRAM")]
)
)[0] )[0]
if "System ROM Version" in bios_bytes_check.decode("ascii"): if "System ROM Version" in bios_bytes_check.decode("ascii"):
if not self.loading_bios_msg: if not self.loading_bios_msg:
self.loading_bios_msg = True self.loading_bios_msg = True
logger.error( logger.error("BIOS is currently loading. Will wait up to 5 seconds before retrying.")
"BIOS is currently loading. Will wait up to 5 seconds before retrying."
)
return False return False
logger.error( logger.error("Invalid rom detected. You are not playing Grinch USA Version.")
"Invalid rom detected. You are not playing Grinch USA Version." raise Exception("Invalid rom detected. You are not playing Grinch USA Version.")
)
raise Exception(
"Invalid rom detected. You are not playing Grinch USA Version."
)
ctx.command_processor.commands["ringlink"] = _cmd_ringlink ctx.command_processor.commands["ringlink"] = _cmd_ringlink
@@ -147,9 +137,7 @@ class GrinchClient(BizHawkClient):
and "RingLink" in args["tags"] and "RingLink" in args["tags"]
and args["data"]["source"] != self.unique_client_id and args["data"]["source"] != self.unique_client_id
): ):
Utils.async_start( Utils.async_start(self.ring_link_input(args["data"]["amount"], ctx), "SyncEggs")
self.ring_link_input(args["data"]["amount"], ctx), "SyncEggs"
)
async def set_auth(self, ctx: "BizHawkClientContext") -> None: async def set_auth(self, ctx: "BizHawkClientContext") -> None:
await ctx.get_username() await ctx.get_username()
@@ -165,9 +153,7 @@ class GrinchClient(BizHawkClient):
if not await self.ingame_checker(ctx): if not await self.ingame_checker(ctx):
return return
if not any( if not any(task.get_name() == "Grinch EggLink" for task in asyncio.all_tasks()):
task.get_name() == "Grinch EggLink" for task in asyncio.all_tasks()
):
print("EggLink") print("EggLink")
self.send_ring_link = True self.send_ring_link = True
Utils.async_start(self.ring_link_output(ctx), name="Grinch EggLink") Utils.async_start(self.ring_link_output(ctx), name="Grinch EggLink")
@@ -180,18 +166,12 @@ class GrinchClient(BizHawkClient):
except bizhawk.RequestFailedError as ex: except bizhawk.RequestFailedError as ex:
# The connector didn't respond. Exit handler and return to main loop to reconnect # The connector didn't respond. Exit handler and return to main loop to reconnect
logger.error( logger.error("Failure to connect / authenticate the grinch. Error details: " + str(ex))
"Failure to connect / authenticate the grinch. Error details: "
+ str(ex)
)
pass pass
except Exception as genericEx: except Exception as genericEx:
# For all other errors, catch this and let the client gracefully disconnect # For all other errors, catch this and let the client gracefully disconnect
logger.error( logger.error("Unknown error occurred while playing the grinch. Error details: " + str(genericEx))
"Unknown error occurred while playing the grinch. Error details: "
+ str(genericEx)
)
await ctx.disconnect(False) await ctx.disconnect(False)
pass pass
@@ -208,14 +188,12 @@ class GrinchClient(BizHawkClient):
grinch_loc_name = ctx.location_names.lookup_in_game(missing_location) grinch_loc_name = ctx.location_names.lookup_in_game(missing_location)
grinch_loc_ram_data = grinch_locations[grinch_loc_name] grinch_loc_ram_data = grinch_locations[grinch_loc_name]
missing_addr_list: list[tuple[int, int, str]] = [ missing_addr_list: list[tuple[int, int, str]] = [
(read_addr.ram_address, read_addr.byte_size, "MainRAM") (read_addr.ram_address, read_addr.byte_size, read_addr.ram_area)
for read_addr in grinch_loc_ram_data.update_ram_addr for read_addr in grinch_loc_ram_data.update_ram_addr
] ]
addr_list_to_read = [*addr_list_to_read, *missing_addr_list] addr_list_to_read = [*addr_list_to_read, *missing_addr_list]
returned_bytes: list[bytes] = await bizhawk.read( returned_bytes: list[bytes] = await bizhawk.read(ctx.bizhawk_ctx, addr_list_to_read)
ctx.bizhawk_ctx, addr_list_to_read
)
# Now loop through everything again and this time get the byte value from the above read, convert to int, # Now loop through everything again and this time get the byte value from the above read, convert to int,
# and check to see if that ram address has our expected value. # and check to see if that ram address has our expected value.
@@ -234,31 +212,20 @@ class GrinchClient(BizHawkClient):
orig_index: int = addr_list_to_read.index( orig_index: int = addr_list_to_read.index(
(addr_to_update.ram_address, addr_to_update.byte_size, "MainRAM") (addr_to_update.ram_address, addr_to_update.byte_size, "MainRAM")
) )
value_read_from_bizhawk: int = int.from_bytes( value_read_from_bizhawk: int = int.from_bytes(returned_bytes[orig_index], "little")
returned_bytes[orig_index], "little"
)
if is_binary: if is_binary:
ram_checked_list.append( ram_checked_list.append((value_read_from_bizhawk & (1 << addr_to_update.binary_bit_pos)) > 0)
(value_read_from_bizhawk & (1 << addr_to_update.binary_bit_pos))
> 0
)
else: else:
expected_int_value = addr_to_update.value expected_int_value = addr_to_update.value
ram_checked_list.append( ram_checked_list.append(expected_int_value == value_read_from_bizhawk)
expected_int_value == value_read_from_bizhawk
)
if all(ram_checked_list): if all(ram_checked_list):
local_locations_checked.append( local_locations_checked.append(GrinchLocation.get_apid(grinch_loc_ram_data.id))
GrinchLocation.get_apid(grinch_loc_ram_data.id)
)
# Update the AP server with the locally checked list of locations (In other words, locations I found in Grinch) # Update the AP server with the locally checked list of locations (In other words, locations I found in Grinch)
locations_sent_to_ap: set[int] = await ctx.check_locations( locations_sent_to_ap: set[int] = await ctx.check_locations(local_locations_checked)
local_locations_checked
)
if len(locations_sent_to_ap) > 0: if len(locations_sent_to_ap) > 0:
await self.remove_physical_items(ctx) await self.remove_physical_items(ctx)
@@ -271,11 +238,7 @@ class GrinchClient(BizHawkClient):
# Otherwise, we will get the new items and give them to the player. # Otherwise, we will get the new items and give them to the player.
self.last_received_index = int.from_bytes( self.last_received_index = int.from_bytes(
( (await bizhawk.read(ctx.bizhawk_ctx, [(RECV_ITEM_ADDR, RECV_ITEM_BITSIZE, "MainRAM")]))[0],
await bizhawk.read(
ctx.bizhawk_ctx, [(RECV_ITEM_ADDR, RECV_ITEM_BITSIZE, "MainRAM")]
)
)[0],
"little", "little",
) )
@@ -294,9 +257,7 @@ class GrinchClient(BizHawkClient):
is_binary = True if not addr_to_update.binary_bit_pos is None else False is_binary = True if not addr_to_update.binary_bit_pos is None else False
if addr_to_update.ram_address in ram_addr_dict.keys(): if addr_to_update.ram_address in ram_addr_dict.keys():
current_ram_address_value = ram_addr_dict[ current_ram_address_value = ram_addr_dict[addr_to_update.ram_address][0]
addr_to_update.ram_address
][0]
else: else:
current_ram_address_value = int.from_bytes( current_ram_address_value = int.from_bytes(
( (
@@ -306,25 +267,21 @@ class GrinchClient(BizHawkClient):
( (
addr_to_update.ram_address, addr_to_update.ram_address,
addr_to_update.byte_size, addr_to_update.byte_size,
"MainRAM", addr_to_update.ram_area,
) )
], ],
) )
)[0], )[0],
"little", addr_to_update.endian,
) )
if is_binary: if is_binary:
current_ram_address_value = current_ram_address_value | ( current_ram_address_value = current_ram_address_value | (1 << addr_to_update.binary_bit_pos)
1 << addr_to_update.binary_bit_pos
)
elif addr_to_update.update_existing_value: elif addr_to_update.update_existing_value:
# Grabs minimum value of a list of numbers and makes sure it does not go above max count possible # Grabs minimum value of a list of numbers and makes sure it does not go above max count possible
current_ram_address_value += addr_to_update.value current_ram_address_value += addr_to_update.value
current_ram_address_value = min( current_ram_address_value = min(current_ram_address_value, addr_to_update.max_count)
current_ram_address_value, addr_to_update.max_count
)
else: else:
current_ram_address_value = addr_to_update.value current_ram_address_value = addr_to_update.value
@@ -340,9 +297,7 @@ class GrinchClient(BizHawkClient):
# Update the latest received item index to ram as well. # Update the latest received item index to ram as well.
ram_addr_dict[RECV_ITEM_ADDR] = [self.last_received_index, RECV_ITEM_BITSIZE] ram_addr_dict[RECV_ITEM_ADDR] = [self.last_received_index, RECV_ITEM_BITSIZE]
await bizhawk.write( await bizhawk.write(ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict))
ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict)
)
async def goal_checker(self, ctx: "BizHawkClientContext"): async def goal_checker(self, ctx: "BizHawkClientContext"):
if not ctx.finished_game: if not ctx.finished_game:
@@ -356,12 +311,12 @@ class GrinchClient(BizHawkClient):
( (
goal_ram_address.ram_address, goal_ram_address.ram_address,
goal_ram_address.byte_size, goal_ram_address.byte_size,
"MainRAM", goal_ram_address.ram_area,
) )
], ],
) )
)[0], )[0],
"little", goal_ram_address.endian,
) )
if (current_ram_address_value & (1 << goal_ram_address.binary_bit_pos)) > 0: if (current_ram_address_value & (1 << goal_ram_address.binary_bit_pos)) > 0:
@@ -381,12 +336,8 @@ class GrinchClient(BizHawkClient):
ram_addr_dict: dict[int, list[int]] = {} ram_addr_dict: dict[int, list[int]] = {}
list_recv_itemids: list[int] = [netItem.item for netItem in ctx.items_received] list_recv_itemids: list[int] = [netItem.item for netItem in ctx.items_received]
items_to_check: dict[str, GrinchItemData] = { items_to_check: dict[str, GrinchItemData] = {**GADGETS_TABLE} # , **SLEIGH_PARTS_TABLE
**GADGETS_TABLE heart_count = len(list(item_id for item_id in list_recv_itemids if item_id == 42570))
} # , **SLEIGH_PARTS_TABLE
heart_count = len(
list(item_id for item_id in list_recv_itemids if item_id == 42570)
)
heart_item_data = ALL_ITEMS_TABLE["Heart of Stone"] heart_item_data = ALL_ITEMS_TABLE["Heart of Stone"]
ram_addr_dict[heart_item_data.update_ram_addr[0].ram_address] = [ ram_addr_dict[heart_item_data.update_ram_addr[0].ram_address] = [
min(heart_count, 4), min(heart_count, 4),
@@ -398,10 +349,7 @@ class GrinchClient(BizHawkClient):
for item_name, item_data in items_to_check.items(): for item_name, item_data in items_to_check.items():
# If item is an event or already been received, ignore. # If item is an event or already been received, ignore.
if ( if item_data.id is None or GrinchLocation.get_apid(item_data.id) in list_recv_itemids:
item_data.id is None
or GrinchLocation.get_apid(item_data.id) in list_recv_itemids
):
continue continue
# This assumes we don't have the item so we must set all the data to 0 # This assumes we don't have the item so we must set all the data to 0
@@ -421,12 +369,12 @@ class GrinchClient(BizHawkClient):
( (
addr_to_update.ram_address, addr_to_update.ram_address,
addr_to_update.byte_size, addr_to_update.byte_size,
"MainRAM", addr_to_update.ram_area,
) )
], ],
) )
)[0], )[0],
"little", addr_to_update.endian,
) )
current_bin_value &= ~(1 << addr_to_update.binary_bit_pos) current_bin_value &= ~(1 << addr_to_update.binary_bit_pos)
ram_addr_dict[addr_to_update.ram_address] = [current_bin_value, 1] ram_addr_dict[addr_to_update.ram_address] = [current_bin_value, 1]
@@ -437,19 +385,13 @@ class GrinchClient(BizHawkClient):
addr_to_update.byte_size, addr_to_update.byte_size,
] ]
await bizhawk.write( await bizhawk.write(ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict))
ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict)
)
def convert_dict_to_ram_list( def convert_dict_to_ram_list(self, addr_dict: dict[int, list[int]]) -> list[tuple[int, Sequence[int], str]]:
self, addr_dict: dict[int, list[int]]
) -> list[tuple[int, Sequence[int], str]]:
addr_list_to_update: list[tuple[int, Sequence[int], str]] = [] addr_list_to_update: list[tuple[int, Sequence[int], str]] = []
for key, val in addr_dict.items(): for key, val in addr_dict.items():
addr_list_to_update.append( addr_list_to_update.append((key, val[0].to_bytes(val[1], "little"), "MainRAM"))
(key, val[0].to_bytes(val[1], "little"), "MainRAM")
)
return addr_list_to_update return addr_list_to_update
@@ -465,9 +407,7 @@ class GrinchClient(BizHawkClient):
for item_name, item_data in items_to_check.items(): for item_name, item_data in items_to_check.items():
# If item is an event or already been received, ignore. # If item is an event or already been received, ignore.
if ( if item_data.id is None: # or GrinchLocation.get_apid(item_data.id) in list_recv_itemids:
item_data.id is None
): # or GrinchLocation.get_apid(item_data.id) in list_recv_itemids:
continue continue
# This will either constantly update the item to ensure you still have it or take it away if you don't deserve it # This will either constantly update the item to ensure you still have it or take it away if you don't deserve it
@@ -487,12 +427,12 @@ class GrinchClient(BizHawkClient):
( (
addr_to_update.ram_address, addr_to_update.ram_address,
addr_to_update.byte_size, addr_to_update.byte_size,
"MainRAM", addr_to_update.ram_area,
) )
], ],
) )
)[0], )[0],
"little", addr_to_update.endian,
) )
if GrinchLocation.get_apid(item_data.id) in list_recv_itemids: if GrinchLocation.get_apid(item_data.id) in list_recv_itemids:
@@ -516,9 +456,7 @@ class GrinchClient(BizHawkClient):
addr_to_update.byte_size, addr_to_update.byte_size,
] ]
await bizhawk.write( await bizhawk.write(ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict))
ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict)
)
async def ingame_checker(self, ctx: "BizHawkClientContext"): async def ingame_checker(self, ctx: "BizHawkClientContext"):
from CommonClient import logger from CommonClient import logger
@@ -575,14 +513,13 @@ class GrinchClient(BizHawkClient):
if self.loc_unlimited_eggs: if self.loc_unlimited_eggs:
await bizhawk.write( await bizhawk.write(
ctx.bizhawk_ctx, ctx.bizhawk_ctx,
[(EGG_COUNT_ADDR, MAX_EGGS.to_bytes(2, "little"), "MainRAM")], [(EGG_COUNT_ADDR, MAX_EGGS.to_bytes(EGG_ADDR_BYTESIZE, "little"), "MainRAM")],
) )
async def ring_link_output(self, ctx: "BizHawkClientContext"): async def ring_link_output(self, ctx: "BizHawkClientContext"):
from CommonClient import logger from CommonClient import logger
while self.send_ring_link and ctx.slot: while self.send_ring_link and ctx.slot:
try: try:
current_egg_count = int.from_bytes( current_egg_count = int.from_bytes(
( (
@@ -604,6 +541,7 @@ class GrinchClient(BizHawkClient):
}, },
"tags": ["RingLink"], "tags": ["RingLink"],
} }
await ctx.send_msgs([msg]) await ctx.send_msgs([msg])
self.previous_egg_count = current_egg_count self.previous_egg_count = current_egg_count
# logger.info(f"RingLink: You sent {str(current_egg_count - self.previous_egg_count)} rotten eggs.") # logger.info(f"RingLink: You sent {str(current_egg_count - self.previous_egg_count)} rotten eggs.")
@@ -611,31 +549,20 @@ class GrinchClient(BizHawkClient):
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
except Exception as ex: except Exception as ex:
logger.error( logger.error("While monitoring grinch's egg count ingame, an error occurred. Details:" + str(ex))
"While monitoring grinch's egg count ingame, an error occurred. Details:"
+ str(ex)
)
self.send_ring_link = False self.send_ring_link = False
if not ctx.slot: if not ctx.slot:
logger.info( logger.info("You must be connected to the multi-world in order for RingLink to work properly.")
"You must be connected to the multi-world in order for RingLink to work properly."
)
async def ring_link_input(self, egg_amount: int, ctx: "BizHawkClientContext"): async def ring_link_input(self, egg_amount: int, ctx: "BizHawkClientContext"):
from CommonClient import logger from CommonClient import logger
game_egg_count = int.from_bytes( game_egg_count = int.from_bytes(
( (await bizhawk.read(ctx.bizhawk_ctx, [(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")]))[0],
await bizhawk.read(
ctx.bizhawk_ctx, [(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")]
)
)[0],
"little", "little",
) )
non_neg_eggs = ( non_neg_eggs = game_egg_count + egg_amount if game_egg_count + egg_amount > 0 else 0
game_egg_count + egg_amount if game_egg_count + egg_amount > 0 else 0
)
current_egg_count = min(non_neg_eggs, MAX_EGGS) current_egg_count = min(non_neg_eggs, MAX_EGGS)
await bizhawk.write( await bizhawk.write(
@@ -657,6 +584,7 @@ class GrinchClient(BizHawkClient):
uid: int = 0 uid: int = 0
for char in string_id: for char in string_id:
uid += ord(char) uid += ord(char)
return uid return uid
@@ -677,7 +605,6 @@ async def _update_ring_link(ctx: "BizHawkClientContext", ring_link: bool):
if ring_link: if ring_link:
ctx.tags.add("RingLink") ctx.tags.add("RingLink")
else: else:
ctx.tags -= {"RingLink"} ctx.tags -= {"RingLink"}

View File

@@ -8,9 +8,7 @@ from BaseClasses import (
class GrinchItemData(NamedTuple): class GrinchItemData(NamedTuple):
item_group: list[ item_group: list[str] # arbituary that can be whatever it can be, basically the field/property for item groups
str
] # arbituary that can be whatever it can be, basically the field/property for item groups
id: Optional[int] id: Optional[int]
classification: IC classification: IC
update_ram_addr: list[GrinchRamData] update_ram_addr: list[GrinchRamData]
@@ -27,9 +25,7 @@ class GrinchItem(Item):
return base_id + id if id is not None else None return base_id + id if id is not None else None
def __init__(self, name: str, player: int, data: GrinchItemData): def __init__(self, name: str, player: int, data: GrinchItemData):
super(GrinchItem, self).__init__( super(GrinchItem, self).__init__(name, data.classification, GrinchItem.get_apid(data.id), player)
name, data.classification, GrinchItem.get_apid(data.id), player
)
self.type = data.item_group self.type = data.item_group
self.item_id = data.id self.item_id = data.id
@@ -75,13 +71,13 @@ class grinch_items:
WV_WHO_CLOAK: str = "Who Cloak" WV_WHO_CLOAK: str = "Who Cloak"
WV_PAINT_BUCKET: str = "Painting Bucket" WV_PAINT_BUCKET: str = "Painting Bucket"
WV_HAMMER: str = "Hammer" WV_HAMMER: str = "Hammer"
WV_SCULPTIN_TOOLS: str = "Sculpting Tools" WV_SCULPTING_TOOLS: str = "Sculpting Tools"
WF_GLUE_BUCKET: str = "Glue Bucket" WF_GLUE_BUCKET: str = "Glue Bucket"
WF_CABLE_CAR_ACCESS_CARD: str = "Cable Car Access Card" WF_CABLE_CAR_ACCESS_CARD: str = "Cable Car Access Card"
WD_SCISSORS: str = "Scissors" WD_SCISSORS: str = "Scissors"
WL_ROPE: str = "Rope" WL_ROPE: str = "Rope"
WL_HOOK: str = "Hook" WL_HOOK: str = "Hook"
WL_DRILLL: str = "Drill" WL_DRILL: str = "Drill"
WL_SCOUT_CLOTHES: str = "Scout Clothes" WL_SCOUT_CLOTHES: str = "Scout Clothes"
class useful_items: class useful_items:
@@ -288,7 +284,7 @@ MISSION_ITEMS_TABLE: dict[str, GrinchItemData] = {
IC.progression, IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=5)], [GrinchRamData(0x0101F9, binary_bit_pos=5)],
), ),
grinch_items.level_items.WL_DRILLL: GrinchItemData( grinch_items.level_items.WL_DRILL: GrinchItemData(
[ [
grinch_categories.MISSION_SPECIFIC_ITEMS, grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS, grinch_categories.USEFUL_ITEMS,
@@ -315,7 +311,7 @@ MISSION_ITEMS_TABLE: dict[str, GrinchItemData] = {
IC.progression_deprioritized, IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=0)], [GrinchRamData(0x0101FA, binary_bit_pos=0)],
), ),
grinch_items.level_items.WV_SCULPTIN_TOOLS: GrinchItemData( grinch_items.level_items.WV_SCULPTING_TOOLS: GrinchItemData(
[ [
grinch_categories.MISSION_SPECIFIC_ITEMS, grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS, grinch_categories.USEFUL_ITEMS,

View File

@@ -1,17 +1,41 @@
from typing import NamedTuple, Optional from typing import Optional
from .RamHandler import GrinchRamData from .RamHandler import GrinchRamData
from BaseClasses import Location, Region from BaseClasses import Location, Region
class GrinchLocationData(NamedTuple): class GrinchLocationData:
region: str region: str
location_group: Optional[list[str]] location_group: Optional[list[str]]
id: Optional[int] id: Optional[int]
update_ram_addr: list[GrinchRamData] update_ram_addr: list[GrinchRamData]
reset_addr: Optional[list[GrinchRamData]] = ( reset_addr: Optional[list[GrinchRamData]] = None # Addresses to update once we find the item
None # Addresses to update once we find the item
) def __init__(
self,
region: str,
location_group: Optional[list[str]] = None,
id: Optional[int] = None,
update_ram_addr: list[GrinchRamData] = [],
reset_addr: Optional[list[GrinchRamData]] = None,
):
self.region = region
if location_group:
self.location_group = location_group
if id:
self.id = id
else:
raise ValueError(f"id is required on GrinchLocationData")
if update_ram_addr:
self.update_ram_addr = update_ram_addr
else:
raise ValueError(f"udpate_ram_addr is required on GrinchLocationData")
if reset_addr:
self.reset_addr = reset_addr
class GrinchLocation(Location): class GrinchLocation(Location):
@@ -23,17 +47,18 @@ class GrinchLocation(Location):
return base_id + id if id is not None else None return base_id + id if id is not None else None
def __init__( def __init__(
self, player: int, name: str, parent: Region, data: GrinchLocationData self,
player: int,
name: str,
parent: Region,
data: GrinchLocationData,
): ):
address = None if data.id is None else GrinchLocation.get_apid(data.id) address = None if data.id is None else GrinchLocation.get_apid(data.id)
super(GrinchLocation, self).__init__( super(GrinchLocation, self).__init__(player, name, address=address, parent=parent)
player, name, address=address, parent=parent
)
self.code = data.id self.code = data.id
self.region = data.region self.region = data.region
self.type = data.location_group self.type = data.location_group
self.address = self.address
def get_location_names_per_category() -> dict[str, set[str]]: def get_location_names_per_category() -> dict[str, set[str]]:
@@ -1151,7 +1176,10 @@ grinch_locations = {
[GrinchRamData(0x0100BF, binary_bit_pos=6)], [GrinchRamData(0x0100BF, binary_bit_pos=6)],
), ),
"MC - Sleigh Ride - Neutralizing Santa": GrinchLocationData( "MC - Sleigh Ride - Neutralizing Santa": GrinchLocationData(
"Sleigh Room", None, None, [GrinchRamData(0x0100BF, binary_bit_pos=7)] "Sleigh Room",
["Sleigh Ride"],
1301,
[GrinchRamData(0x0100BF, binary_bit_pos=7)],
), # [GrinchRamData(0x010000, value=0x3E)]), ), # [GrinchRamData(0x010000, value=0x3E)]),
# Heart of Stones # Heart of Stones
"WV - Post Office - Heart of Stone": GrinchLocationData( "WV - Post Office - Heart of Stone": GrinchLocationData(
@@ -1224,19 +1252,44 @@ grinch_locations = {
), ),
# Mount Crumpit Locations # Mount Crumpit Locations
"MC - 1st Crate Squashed": GrinchLocationData( "MC - 1st Crate Squashed": GrinchLocationData(
"Mount Crumpit", ["Mount Crumpit"], 1700, [GrinchRamData(0x095343, value=1)] "Mount Crumpit",
["Mount Crumpit"],
1700,
[
GrinchRamData(0x095343, value=1),
],
), ),
"MC - 2nd Crate Squashed": GrinchLocationData( "MC - 2nd Crate Squashed": GrinchLocationData(
"Mount Crumpit", ["Mount Crumpit"], 1701, [GrinchRamData(0x095343, value=2)] "Mount Crumpit",
["Mount Crumpit"],
1701,
[
GrinchRamData(0x095343, value=2),
],
), ),
"MC - 3rd Crate Squashed": GrinchLocationData( "MC - 3rd Crate Squashed": GrinchLocationData(
"Mount Crumpit", ["Mount Crumpit"], 1702, [GrinchRamData(0x095343, value=3)] "Mount Crumpit",
["Mount Crumpit"],
1702,
[
GrinchRamData(0x095343, value=3),
],
), ),
"MC - 4th Crate Squashed": GrinchLocationData( "MC - 4th Crate Squashed": GrinchLocationData(
"Mount Crumpit", ["Mount Crumpit"], 1703, [GrinchRamData(0x095343, value=4)] "Mount Crumpit",
["Mount Crumpit"],
1703,
[
GrinchRamData(0x095343, value=4),
],
), ),
"MC - 5th Crate Squashed": GrinchLocationData( "MC - 5th Crate Squashed": GrinchLocationData(
"Mount Crumpit", ["Mount Crumpit"], 1704, [GrinchRamData(0x095343, value=5)] "Mount Crumpit",
["Mount Crumpit"],
1704,
[
GrinchRamData(0x095343, value=5),
],
), ),
} }
@@ -1244,7 +1297,5 @@ grinch_locations = {
def grinch_locations_to_id() -> dict[str, int]: def grinch_locations_to_id() -> dict[str, int]:
location_mappings: dict[str, int] = {} location_mappings: dict[str, int] = {}
for LocationName, LocationData in grinch_locations.items(): for LocationName, LocationData in grinch_locations.items():
location_mappings.update( location_mappings.update({LocationName: GrinchLocation.get_apid(LocationData.id)})
{LocationName: GrinchLocation.get_apid(LocationData.id)}
)
return location_mappings return location_mappings

View File

@@ -1,5 +1,5 @@
from enum import STRICT, IntEnum from enum import STRICT, IntEnum
from typing import NamedTuple, Optional from typing import Optional
class UpdateMethod(IntEnum, boundary=STRICT): class UpdateMethod(IntEnum, boundary=STRICT):
@@ -9,7 +9,7 @@ class UpdateMethod(IntEnum, boundary=STRICT):
FREEZE = 4 FREEZE = 4
class GrinchRamData(NamedTuple): class GrinchRamData:
"""A Representation of an update to RAM data for The Grinch. """A Representation of an update to RAM data for The Grinch.
ram_address (int): The RAM address that we are updating, usually passed in with hex, representation (0x11111111) ram_address (int): The RAM address that we are updating, usually passed in with hex, representation (0x11111111)
@@ -23,22 +23,25 @@ class GrinchRamData(NamedTuple):
ram_address: int ram_address: int
value: Optional[int] = None # none is empty/null value: Optional[int] = None # none is empty/null
# Either set or add either hex or unsigned values through Client.py
# Hex uses 0x00, unsigned are base whole numbers
binary_bit_pos: Optional[int] = None binary_bit_pos: Optional[int] = None
byte_size: int = 1 byte_size: int = 1
endian = "little"
update_method: UpdateMethod = UpdateMethod.SET update_method: UpdateMethod = UpdateMethod.SET
min_count: int = 0 min_count: int = 0
max_count: int = 255 max_count: int = 255
ram_area = "MainRAM"
def __init__( def __init__(
self, self,
ram_address: int, ram_address: int,
value: Optional[int], value: Optional[int] = None,
byte_size: int, binary_bit_pos: int = -1,
update_method: UpdateMethod, byte_size: int = 1,
min_count: int, update_method: UpdateMethod = UpdateMethod.SET,
max_count: int, min_count: int = -1,
max_count: int = -1,
endian: str = "little",
ram_area: str = "MainRAM",
): ):
self.ram_address = ram_address self.ram_address = ram_address
@@ -47,7 +50,10 @@ class GrinchRamData(NamedTuple):
else: else:
self.value = 1 self.value = 1
if byte_size: if binary_bit_pos > -1:
self.binary_bit_pos = bin
if byte_size > 0:
self.byte_size = byte_size self.byte_size = byte_size
if update_method: if update_method:
@@ -59,8 +65,26 @@ class GrinchRamData(NamedTuple):
if max_count and max_count > -1: if max_count and max_count > -1:
self.max_count = max_count self.max_count = max_count
elif max_count and max_count > ((2 ** (self.byte_size * 8)) - 1): elif max_count and max_count > ((2 ** (self.byte_size * 8)) - 1):
raise ValueError( raise ValueError("max_count cannot be larger than the RAM addresses max possible value")
"max_count cannot be larger than the RAM addresses max possible value"
)
else: else:
self.max_count = (2 ** (self.byte_size * 8)) - 1 self.max_count = (2 ** (self.byte_size * 8)) - 1
self.endian = endian
self.ram_area = ram_area
# Error Handling
if self.value and self.value > self.max_count:
raise ValueError(
f"Value passed in is greater than max_count.\n\nRAM Address: {self.ram_address}\nValue: {self.value}\nMax Count: {self.max_count}"
)
if self.value and self.value < self.min_count:
raise ValueError(
f"Value passed in is lower than min_count.\n\nRAM Address: {self.ram_address}\nValue: {self.value}\nMin Count: {self.max_count}"
)
if self.binary_bit_pos and self.update_method not in [UpdateMethod.SET, UpdateMethod.FREEZE]:
raise ValueError(f"binary_bit_position can only be passed in if update_method is SET or FREEZE")
if self.binary_bit_pos and self.value not in [0, 1]:
raise ValueError(f"value must be 0 or 1 if using binary_bit_position")

View File

@@ -41,22 +41,16 @@ supadow_list = [
def create_regions(world: "GrinchWorld"): def create_regions(world: "GrinchWorld"):
for mainarea in mainareas_list: for area in [*mainareas_list, *subareas_list, *supadow_list]:
# Each area in mainarea, create a region for the given player # Each area in mainarea, subarea, and supadow create a region for the given player
world.multiworld.regions.append( world.multiworld.regions.append(Region(area, world.player, world.multiworld))
Region(mainarea, world.player, world.multiworld)
)
for subarea in subareas_list:
# Each area in subarea, create a region for the given player
world.multiworld.regions.append(Region(subarea, world.player, world.multiworld))
for supadow in supadow_list:
# Each area in supadow, create a region for the given player
world.multiworld.regions.append(Region(supadow, world.player, world.multiworld))
# TODO Optimize this function # TODO Optimize this function
def grinchconnect( def grinchconnect(
world: "GrinchWorld", current_region_name: str, connected_region_name: str world: "GrinchWorld",
current_region_name: str,
connected_region_name: str,
): ):
current_region = world.get_region(current_region_name) current_region = world.get_region(current_region_name)
connected_region = world.get_region(connected_region_name) connected_region = world.get_region(connected_region_name)
@@ -66,6 +60,7 @@ def grinchconnect(
current_region.connect(connected_region) current_region.connect(connected_region)
# Goes from connected to current # Goes from connected to current
connected_region.connect(current_region) connected_region.connect(current_region)
for access_rule in rule_list: for access_rule in rule_list:
for region_entrance in current_region.entrances: for region_entrance in current_region.entrances:
if ( if (
@@ -74,8 +69,10 @@ def grinchconnect(
): ):
if rule_list.index(access_rule) == 0: if rule_list.index(access_rule) == 0:
add_rule(region_entrance, access_rule) add_rule(region_entrance, access_rule)
else: else:
add_rule(region_entrance, access_rule, combine="or") add_rule(region_entrance, access_rule, combine="or")
for region_entrance in connected_region.entrances: for region_entrance in connected_region.entrances:
if ( if (
region_entrance.connected_region.name == connected_region_name region_entrance.connected_region.name == connected_region_name
@@ -83,6 +80,7 @@ def grinchconnect(
): ):
if rule_list.index(access_rule) == 0: if rule_list.index(access_rule) == 0:
add_rule(region_entrance, access_rule) add_rule(region_entrance, access_rule)
else: else:
add_rule(region_entrance, access_rule, combine="or") add_rule(region_entrance, access_rule, combine="or")

View File

@@ -20,7 +20,10 @@ def set_location_rules(world: World):
add_rule(location, access_rule, "or") add_rule(location, access_rule, "or")
def interpret_rule(rule_set: list[list[str]], player: int): def interpret_rule(
rule_set: list[list[str]],
player: int,
):
# If a region/location does not have any items required, make the section(s) return no logic. # If a region/location does not have any items required, make the section(s) return no logic.
if len(rule_set) < 1: if len(rule_set) < 1:
return True return True
@@ -29,9 +32,7 @@ def interpret_rule(rule_set: list[list[str]], player: int):
access_list: list[Callable[[CollectionState], bool]] = [] access_list: list[Callable[[CollectionState], bool]] = []
for item_set in rule_set: for item_set in rule_set:
access_list.append( access_list.append(lambda state, items=tuple(item_set): state.has_all(items, player))
lambda state, items=tuple(item_set): state.has_all(items, player)
)
return access_list return access_list
# Each item in the list is a separate list of rules. Each separate list is just an "OR" condition. # Each item in the list is a separate list of rules. Each separate list is just an "OR" condition.
@@ -130,12 +131,12 @@ access_rules_dict: dict[str, list[list[str]]] = {
], ],
"North Shore": [ "North Shore": [
[ [
grinch_items.level_items.WL_SCOUT_CLOTHESL, grinch_items.level_items.WL_SCOUT_CLOTHES,
] ]
], ],
"Mayor's Villa": [ "Mayor's Villa": [
[ [
grinch_items.level_items.WL_SCOUT_CLOTHESL, grinch_items.level_items.WL_SCOUT_CLOTHES,
] ]
], ],
"Submarine World": [ "Submarine World": [
@@ -408,9 +409,7 @@ rules_dict: dict[str, list[list[str]]] = {
"WV - TEL BP left of City Hall": [[]], "WV - TEL BP left of City Hall": [[]],
"WV - REL BP left of Clock Tower": [[]], "WV - REL BP left of Clock Tower": [[]],
"WV - Post Office - REL BP inside Silver Room": [[]], "WV - Post Office - REL BP inside Silver Room": [[]],
"WV - Post Office - REL BP at Entrance Door after Migrinch_items.gadgets.SLIME_SHOOTERion Completion": [ "WV - Post Office - REL BP at Entrance Door after Migrinch_items.gadgets.SLIME_SHOOTERion Completion": [[]],
[]
],
"WV - City Hall - GC BP in Safe Room": [[]], "WV - City Hall - GC BP in Safe Room": [[]],
"WV - City Hall - GC BP in Statue Room": [[]], "WV - City Hall - GC BP in Statue Room": [[]],
"WV - Clock Tower - GC BP in Bedroom": [ "WV - Clock Tower - GC BP in Bedroom": [
@@ -764,9 +763,7 @@ rules_dict: dict[str, list[list[str]]] = {
"WL - North Shore - MM BP inside Boulder Box behind Skunk Hut": [[]], "WL - North Shore - MM BP inside Boulder Box behind Skunk Hut": [[]],
"WL - North Shore - MM BP inside Drill House": [[]], "WL - North Shore - MM BP inside Drill House": [[]],
"WL - North Shore - MM BP on Crow Platform near Drill House": [[]], "WL - North Shore - MM BP on Crow Platform near Drill House": [[]],
"WL - Submarine World - GC BP Just Below Water Surface": [ "WL - Submarine World - GC BP Just Below Water Surface": [[grinch_items.gadgets.MARINE_MOBILE]],
[grinch_items.gadgets.MARINE_MOBILE]
],
"WL - Submarine World - GC BP Underwater": [ "WL - Submarine World - GC BP Underwater": [
[ [
grinch_items.gadgets.MARINE_MOBILE, grinch_items.gadgets.MARINE_MOBILE,

View File

@@ -18,53 +18,65 @@ class GrinchWorld(World):
game: ClassVar[str] = "The Grinch" game: ClassVar[str] = "The Grinch"
options_dataclass = Options.GrinchOptions options_dataclass = Options.GrinchOptions
options: Options.GrinchOptions options: Options.GrinchOptions
topology_present = True #not an open world game, very linear topology_present = True # not an open world game, very linear
item_name_to_id: ClassVar[dict[str,int]] = grinch_items_to_id() item_name_to_id: ClassVar[dict[str, int]] = grinch_items_to_id()
location_name_to_id: ClassVar[dict[str,int]] = grinch_locations_to_id() location_name_to_id: ClassVar[dict[str, int]] = grinch_locations_to_id()
required_client_version = (0, 6, 3) required_client_version = (0, 6, 3)
item_name_groups = get_item_names_per_category() item_name_groups = get_item_names_per_category()
location_name_groups = get_location_names_per_category() location_name_groups = get_location_names_per_category()
def __init__(self, *args, **kwargs): #Pulls __init__ function and takes control from there in BaseClasses.py def __init__(self, *args, **kwargs): # Pulls __init__ function and takes control from there in BaseClasses.py
self.origin_region_name: str = "Mount Crumpit" self.origin_region_name: str = "Mount Crumpit"
super(GrinchWorld, self).__init__(*args, **kwargs) super(GrinchWorld, self).__init__(*args, **kwargs)
def generate_early(self) -> None: #Special conditions changed before generation occurs def generate_early(self) -> None: # Special conditions changed before generation occurs
if self.options.ring_link == 1 and self.options.unlimited_eggs == 1: if self.options.ring_link == 1 and self.options.unlimited_eggs == 1:
raise OptionError("Cannot enable both unlimited rotten eggs and ring links. You can only enable one of these at a time." + raise OptionError(
f"The following player's YAML needs to be fixed: {self.player_name}") "Cannot enable both unlimited rotten eggs and ring links. You can only enable one of these at a time."
+ f"The following player's YAML needs to be fixed: {self.player_name}"
)
def create_regions(self): # Generates all regions for the multiworld
def create_regions(self): #Generates all regions for the multiworld
for region_name in access_rules_dict.keys(): for region_name in access_rules_dict.keys():
self.multiworld.regions.append(Region(region_name, self.player, self.multiworld)) self.multiworld.regions.append(Region(region_name, self.player, self.multiworld))
self.multiworld.regions.append(Region("Mount Crumpit", self.player, self.multiworld)) self.multiworld.regions.append(Region("Mount Crumpit", self.player, self.multiworld))
for location, data in grinch_locations.items(): for location, data in grinch_locations.items():
region = self.get_region(data.region) region = self.get_region(data.region)
entry = GrinchLocation(self.player, location, region, data) entry = GrinchLocation(self.player, location, region, data)
if location == "MC - Sleigh Ride - Neutralizing Santa": if location == "MC - Sleigh Ride - Neutralizing Santa":
entry.place_locked_item(Item("Goal", ItemClassification.progression, None, self.player)) entry.place_locked_item(Item("Goal", ItemClassification.progression, None, self.player))
region.locations.append(entry) region.locations.append(entry)
connect_regions(self) connect_regions(self)
def create_item(self, item: str) -> GrinchItem: #Creates specific items on demand def create_item(self, item: str) -> GrinchItem: # Creates specific items on demand
if item in ALL_ITEMS_TABLE.keys(): if item in ALL_ITEMS_TABLE.keys():
return GrinchItem(item, self.player, ALL_ITEMS_TABLE[item]) return GrinchItem(item, self.player, ALL_ITEMS_TABLE[item])
raise Exception(f"Invalid item name: {item}") raise Exception(f"Invalid item name: {item}")
def create_items(self): #Generates all items for the multiworld def create_items(self): # Generates all items for the multiworld
self_itempool: list[GrinchItem] = [] self_itempool: list[GrinchItem] = []
for item, data in ALL_ITEMS_TABLE.items(): for item, data in ALL_ITEMS_TABLE.items():
self_itempool.append(self.create_item(item)) self_itempool.append(self.create_item(item))
if item == "Heart of Stone": if item == "Heart of Stone":
for _ in range(3): for _ in range(3):
self_itempool.append(self.create_item(item)) self_itempool.append(self.create_item(item))
#Get number of current unfilled locations # Get number of current unfilled locations
unfilled_locations: int = len(self.multiworld.get_unfilled_locations(self.player)) - len(ALL_ITEMS_TABLE.keys()) - 3 unfilled_locations: int = (
len(self.multiworld.get_unfilled_locations(self.player)) - len(ALL_ITEMS_TABLE.keys()) - 3
)
for _ in range(unfilled_locations): for _ in range(unfilled_locations):
self_itempool.append(self.create_item((self.get_other_filler_item(list(MISC_ITEMS_TABLE.keys()))))) self_itempool.append(self.create_item((self.get_other_filler_item(list(MISC_ITEMS_TABLE.keys())))))
self.multiworld.itempool += self_itempool self.multiworld.itempool += self_itempool
def set_rules(self): def set_rules(self):
@@ -82,4 +94,4 @@ class GrinchWorld(World):
def generate_output(self, output_directory: str) -> None: def generate_output(self, output_directory: str) -> None:
# print("") # print("")
pass pass