15 Commits

Author SHA1 Message Date
d0a8df25f6 Linted worlds/grinch files through python Black
Moved Item name references to a class-based Enum to improve future QoL
Moved categories to an Enum to improve future QoL
Replace bit_size with byte_size to match what the variable is actually measuring
Moved Sleigh Parts regions to match where you actually get the checks
Updated the RAM Handler with comments, renamed bit_size to byte_size, replaced update_value with update_method that now takes one of several methods, and created an __init__ function

NOTE: DOES NOT CURRENLTY FUNCTION
2025-10-27 13:17:04 -06:00
MarioSpore
6f552949fa Downgrade world_version. Didn't expect to release a hotfix before v1.3 2025-10-25 18:43:50 -04:00
MarioSpore
ca1eb82ce1 Forgot to uncomment binary set for goal_checker 2025-10-25 18:20:49 -04:00
MarioSpore
414a155323 Temporairly reverted goal position back to after credits to compensate for game watcher 2025-10-25 18:00:18 -04:00
MarioSpore
b9d8d9174f Merge remote-tracking branch 'origin/dev' into dev 2025-10-24 21:03:17 -04:00
MarioSpore
8e58f9662e Update archipelago.json to no longer arbitrary "version". Isnt needed & already added what I needed. 2025-10-24 13:20:35 -04:00
MarioSpore
6f4597398f New bizhawk version requirement 2025-10-13 22:08:38 -04:00
MarioSpore
5e71874446 I think this is how archipelago.json works? 2025-10-12 16:52:03 -04:00
MarioSpore
1870dd24ba Sleigh room part locations are now behind Sleigh Room region to prevent confusion when using UT 2025-10-12 16:51:42 -04:00
MarioSpore
f70b6c4c9c Pseudocode for move_rando pt 2 2025-10-04 23:57:22 -04:00
MarioSpore
79d4d5b10b Renamed inital for Seize 2025-10-04 23:50:44 -04:00
MarioSpore
7fea34adc3 Removed gadget_rando for now 2025-10-04 23:50:29 -04:00
MarioSpore
a3f9e6cbc9 Started psuedocode for moverando logic 2025-10-04 23:37:22 -04:00
MarioSpore
bccc83f864 Fixed Exhaust Pipes not being logically required for WV 2025-10-03 23:07:15 -04:00
SomeJakeGuy
6409721841 Fixed an issue where RingLink could disconnect because BizHawk is paused, so moved it to gamewatcher to be started there instead. 2025-10-03 22:23:16 -04:00
9 changed files with 2907 additions and 892 deletions

View File

@@ -6,7 +6,13 @@ import copy
import uuid import uuid
import Utils import Utils
from .Locations import grinch_locations, GrinchLocation from .Locations import grinch_locations, GrinchLocation
from .Items import ALL_ITEMS_TABLE, MISSION_ITEMS_TABLE, GADGETS_TABLE, KEYS_TABLE, GrinchItemData #, SLEIGH_PARTS_TABLE from .Items import (
ALL_ITEMS_TABLE,
MISSION_ITEMS_TABLE,
GADGETS_TABLE,
KEYS_TABLE,
GrinchItemData,
) # , SLEIGH_PARTS_TABLE
import worlds._bizhawk as bizhawk import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient from worlds._bizhawk.client import BizHawkClient
@@ -30,6 +36,7 @@ MAX_EGGS: int = 200
EGG_COUNT_ADDR: int = 0x010058 EGG_COUNT_ADDR: int = 0x010058
EGG_ADDR_BYTESIZE: int = 2 EGG_ADDR_BYTESIZE: int = 2
class GrinchClient(BizHawkClient): class GrinchClient(BizHawkClient):
game = "The Grinch" game = "The Grinch"
system = "PSX" system = "PSX"
@@ -41,6 +48,7 @@ class GrinchClient(BizHawkClient):
previous_egg_count: int = 0 previous_egg_count: int = 0
send_ring_link: bool = False send_ring_link: bool = False
unique_client_id: int = 0 unique_client_id: int = 0
ring_link_enabled = False
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -51,27 +59,44 @@ class GrinchClient(BizHawkClient):
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger from CommonClient import logger
# TODO Check the ROM data to see if it matches against bytes expected # TODO Check the ROM data to see if it matches against bytes expected
grinch_identifier_ram_address: int = 0x00928C grinch_identifier_ram_address: int = 0x00928C
bios_identifier_ram_address: int = 0x097F30 bios_identifier_ram_address: int = 0x097F30
try: try:
bytes_actual: bytes = (await bizhawk.read(ctx.bizhawk_ctx, [( bytes_actual: bytes = (
grinch_identifier_ram_address, 11, "MainRAM")]))[0] await bizhawk.read(
ctx.bizhawk_ctx, [(grinch_identifier_ram_address, 11, "MainRAM")]
)
)[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 = (await bizhawk.read(ctx.bizhawk_ctx, [( bios_bytes_check: bytes = (
bios_identifier_ram_address, 24, "MainRAM")]))[0] await bizhawk.read(
ctx.bizhawk_ctx, [(bios_identifier_ram_address, 24, "MainRAM")]
)
)[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("BIOS is currently loading. Will wait up to 5 seconds before retrying.") logger.error(
"BIOS is currently loading. Will wait up to 5 seconds before retrying."
)
return False return False
logger.error("Invalid rom detected. You are not playing Grinch USA Version.") logger.error(
raise Exception("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."
)
ctx.command_processor.commands["ringlink"] = _cmd_ringlink ctx.command_processor.commands["ringlink"] = _cmd_ringlink
except Exception: except Exception:
return False return False
@@ -85,40 +110,54 @@ class GrinchClient(BizHawkClient):
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
from CommonClient import logger from CommonClient import logger
super().on_package(ctx, cmd, args) super().on_package(ctx, cmd, args)
match cmd: match cmd:
case "Connected": # On Connect case "Connected": # On Connect
self.loc_unlimited_eggs = bool(ctx.slot_data["give_unlimited_eggs"]) self.loc_unlimited_eggs = bool(ctx.slot_data["give_unlimited_eggs"])
self.unique_client_id = self._get_uuid() self.unique_client_id = self._get_uuid()
logger.info("You are now connected to the client. "+ logger.info(
"There may be a slight delay to check you are not in demo mode before locations start to send.") "You are now connected to the client. "
+ "There may be a slight delay to check you are not in demo mode before locations start to send."
)
ring_link_enabled = bool(ctx.slot_data["ring_link"]) self.ring_link_enabled = bool(ctx.slot_data["ring_link"])
tags = copy.deepcopy(ctx.tags) tags = copy.deepcopy(ctx.tags)
if ring_link_enabled:
self.send_ring_link = True if self.ring_link_enabled:
Utils.async_start(self.ring_link_output(ctx), name="EggLink")
ctx.tags.add("RingLink") ctx.tags.add("RingLink")
else: else:
ctx.tags -= { "RingLink" } ctx.tags -= {"RingLink"}
if tags != ctx.tags: if tags != ctx.tags:
Utils.async_start(ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}]), "Update RingLink Tags") Utils.async_start(
ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}]),
"Update RingLink Tags",
)
case "Bounced": case "Bounced":
if "tags" not in args: if "tags" not in args:
return return
if "RingLink" in ctx.tags and "RingLink" in args["tags"] and args["data"]["source"] != self.unique_client_id: if (
Utils.async_start(self.ring_link_input(args["data"]["amount"], ctx), "SyncEggs") "RingLink" in ctx.tags
and "RingLink" in args["tags"]
and args["data"]["source"] != self.unique_client_id
):
Utils.async_start(
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()
async def game_watcher(self, ctx: "BizHawkClientContext") -> None: async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from CommonClient import logger from CommonClient import logger
#If the player is not connected to an AP Server, or their connection was disconnected.
# If the player is not connected to an AP Server, or their connection was disconnected.
if not ctx.slot: if not ctx.slot:
return return
@@ -126,6 +165,13 @@ class GrinchClient(BizHawkClient):
if not await self.ingame_checker(ctx): if not await self.ingame_checker(ctx):
return return
if not any(
task.get_name() == "Grinch EggLink" for task in asyncio.all_tasks()
):
print("EggLink")
self.send_ring_link = True
Utils.async_start(self.ring_link_output(ctx), name="Grinch EggLink")
await self.location_checker(ctx) await self.location_checker(ctx)
await self.receiving_items_handler(ctx) await self.receiving_items_handler(ctx)
await self.goal_checker(ctx) await self.goal_checker(ctx)
@@ -134,16 +180,24 @@ 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("Failure to connect / authenticate the grinch. Error details: " + str(ex)) logger.error(
"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("Unknown error occurred while playing the grinch. Error details: " + str(genericEx)) logger.error(
"Unknown error occurred while playing the grinch. Error details: "
+ str(genericEx)
)
await ctx.disconnect(False) await ctx.disconnect(False)
pass pass
async def location_checker(self, ctx: "BizHawkClientContext"): async def location_checker(self, ctx: "BizHawkClientContext"):
from CommonClient import logger from CommonClient import logger
# Update the AP Server to know what locations are not checked yet. # Update the AP Server to know what locations are not checked yet.
local_locations_checked: list[int] = [] local_locations_checked: list[int] = []
addr_list_to_read: list[tuple[int, int, str]] = [] addr_list_to_read: list[tuple[int, int, str]] = []
@@ -153,11 +207,15 @@ class GrinchClient(BizHawkClient):
for missing_location in local_ap_locations: for missing_location in local_ap_locations:
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]] = [(read_addr.ram_address, read_addr.bit_size, "MainRAM") for missing_addr_list: list[tuple[int, int, str]] = [
read_addr in grinch_loc_ram_data.update_ram_addr] (read_addr.ram_address, read_addr.byte_size, "MainRAM")
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(ctx.bizhawk_ctx, addr_list_to_read) returned_bytes: list[bytes] = await bizhawk.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.
@@ -170,22 +228,41 @@ class GrinchClient(BizHawkClient):
# Grinch ram data may have more than one address to update, so we are going to loop through all addresses in a location # Grinch ram data may have more than one address to update, so we are going to loop through all addresses in a location
# We use a list here to keep track of all our checks. If they are all true, then and only then do we mark that location as checked. # We use a list here to keep track of all our checks. If they are all true, then and only then do we mark that location as checked.
ram_checked_list: list[bool] = [] ram_checked_list: list[bool] = []
for addr_to_update in grinch_loc_ram_data.update_ram_addr: for addr_to_update in grinch_loc_ram_data.update_ram_addr:
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
orig_index: int = addr_list_to_read.index((addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")) orig_index: int = addr_list_to_read.index(
value_read_from_bizhawk: int = int.from_bytes(returned_bytes[orig_index], "little") (addr_to_update.ram_address, addr_to_update.byte_size, "MainRAM")
)
value_read_from_bizhawk: int = int.from_bytes(
returned_bytes[orig_index], "little"
)
if is_binary: if is_binary:
ram_checked_list.append((value_read_from_bizhawk & (1 << addr_to_update.binary_bit_pos)) > 0) ram_checked_list.append(
(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(expected_int_value == value_read_from_bizhawk) ram_checked_list.append(
expected_int_value == value_read_from_bizhawk
)
if all(ram_checked_list): if all(ram_checked_list):
local_locations_checked.append(GrinchLocation.get_apid(grinch_loc_ram_data.id)) local_locations_checked.append(
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(local_locations_checked) locations_sent_to_ap: set[int] = await ctx.check_locations(
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)
ctx.locations_checked = set(local_locations_checked) ctx.locations_checked = set(local_locations_checked)
async def receiving_items_handler(self, ctx: "BizHawkClientContext"): async def receiving_items_handler(self, ctx: "BizHawkClientContext"):
@@ -193,12 +270,20 @@ class GrinchClient(BizHawkClient):
# If the list says that we have 3 items and we already received items, we will ignore and continue. # If the list says that we have 3 items and we already received items, we will ignore and continue.
# 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((await bizhawk.read(ctx.bizhawk_ctx, [( self.last_received_index = int.from_bytes(
RECV_ITEM_ADDR, RECV_ITEM_BITSIZE, "MainRAM")]))[0], "little") (
await bizhawk.read(
ctx.bizhawk_ctx, [(RECV_ITEM_ADDR, RECV_ITEM_BITSIZE, "MainRAM")]
)
)[0],
"little",
)
if len(ctx.items_received) == self.last_received_index: if len(ctx.items_received) == self.last_received_index:
return return
# Ensures we only get the new items that we want to give the player # Ensures we only get the new items that we want to give the player
new_items_only = ctx.items_received[self.last_received_index:] new_items_only = ctx.items_received[self.last_received_index :]
ram_addr_dict: dict[int, list[int]] = {} ram_addr_dict: dict[int, list[int]] = {}
for item_received in new_items_only: for item_received in new_items_only:
@@ -207,83 +292,164 @@ class GrinchClient(BizHawkClient):
for addr_to_update in grinch_item_ram_data.update_ram_addr: for addr_to_update in grinch_item_ram_data.update_ram_addr:
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[addr_to_update.ram_address][0] current_ram_address_value = ram_addr_dict[
addr_to_update.ram_address
][0]
else: else:
current_ram_address_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( current_ram_address_value = int.from_bytes(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little") (
await bizhawk.read(
ctx.bizhawk_ctx,
[
(
addr_to_update.ram_address,
addr_to_update.byte_size,
"MainRAM",
)
],
)
)[0],
"little",
)
if is_binary: if is_binary:
current_ram_address_value = (current_ram_address_value | (1 << addr_to_update.binary_bit_pos)) current_ram_address_value = current_ram_address_value | (
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, addr_to_update.max_count) current_ram_address_value = min(
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
# Write the updated value back into RAM # Write the updated value back into RAM
ram_addr_dict[addr_to_update.ram_address] = [current_ram_address_value, addr_to_update.bit_size] ram_addr_dict[addr_to_update.ram_address] = [
current_ram_address_value,
addr_to_update.byte_size,
]
self.last_received_index += 1 self.last_received_index += 1
# 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(ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict))
await bizhawk.write(
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:
goal_loc = grinch_locations["MC - Sleigh Ride - Neutralizing Santa"] goal_loc = grinch_locations["MC - Sleigh Ride - Neutralizing Santa"]
goal_ram_address = goal_loc.update_ram_addr[0] goal_ram_address = goal_loc.update_ram_addr[0]
current_ram_address_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( current_ram_address_value = int.from_bytes(
goal_ram_address.ram_address, goal_ram_address.bit_size, "MainRAM")]))[0], "little") (
# if (current_ram_address_value & (1 << goal_ram_address.binary_bit_pos)) > 0: await bizhawk.read(
if current_ram_address_value == goal_ram_address.value: ctx.bizhawk_ctx,
[
(
goal_ram_address.ram_address,
goal_ram_address.byte_size,
"MainRAM",
)
],
)
)[0],
"little",
)
if (current_ram_address_value & (1 << goal_ram_address.binary_bit_pos)) > 0:
# if current_ram_address_value == goal_ram_address.value:
ctx.finished_game = True ctx.finished_game = True
await ctx.send_msgs([{ await ctx.send_msgs(
"cmd": "StatusUpdate", [
"status": NetUtils.ClientStatus.CLIENT_GOAL, {
}]) "cmd": "StatusUpdate",
"status": NetUtils.ClientStatus.CLIENT_GOAL,
}
]
)
# This function's entire purpose is to take away items we physically received ingame, but have not received from AP # This function's entire purpose is to take away items we physically received ingame, but have not received from AP
async def remove_physical_items(self, ctx: "BizHawkClientContext"): async def remove_physical_items(self, ctx: "BizHawkClientContext"):
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] = {**GADGETS_TABLE} #, **SLEIGH_PARTS_TABLE items_to_check: dict[str, GrinchItemData] = {
heart_count = len(list(item_id for item_id in list_recv_itemids if item_id == 42570)) **GADGETS_TABLE
} # , **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] = [min(heart_count, 4), 1] ram_addr_dict[heart_item_data.update_ram_addr[0].ram_address] = [
min(heart_count, 4),
1,
]
# Setting mission count for all accesses back to 0 to prevent warping/unlocking after completing 3 missions # Setting mission count for all accesses back to 0 to prevent warping/unlocking after completing 3 missions
ram_addr_dict[0x0100F0] = [0, 4] ram_addr_dict[0x0100F0] = [0, 4]
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 item_data.id is None or GrinchLocation.get_apid(item_data.id) in list_recv_itemids: if (
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
for addr_to_update in item_data.update_ram_addr: for addr_to_update in item_data.update_ram_addr:
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 is_binary: if is_binary:
if addr_to_update.ram_address in ram_addr_dict.keys(): if addr_to_update.ram_address in ram_addr_dict.keys():
current_bin_value = ram_addr_dict[addr_to_update.ram_address][0] current_bin_value = ram_addr_dict[addr_to_update.ram_address][0]
else: else:
current_bin_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( current_bin_value = int.from_bytes(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little") (
await bizhawk.read(
ctx.bizhawk_ctx,
[
(
addr_to_update.ram_address,
addr_to_update.byte_size,
"MainRAM",
)
],
)
)[0],
"little",
)
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]
else: else:
ram_addr_dict[addr_to_update.ram_address] = [0, addr_to_update.bit_size] ram_addr_dict[addr_to_update.ram_address] = [
0,
addr_to_update.byte_size,
]
await bizhawk.write(ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict)) await bizhawk.write(
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((key, val[0].to_bytes(val[1], "little"), "MainRAM")) addr_list_to_update.append(
(key, val[0].to_bytes(val[1], "little"), "MainRAM")
)
return addr_list_to_update return addr_list_to_update
@@ -292,56 +458,93 @@ 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] = {**KEYS_TABLE, **MISSION_ITEMS_TABLE} items_to_check: dict[str, GrinchItemData] = {
**KEYS_TABLE,
**MISSION_ITEMS_TABLE,
}
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 item_data.id is None: # or GrinchLocation.get_apid(item_data.id) in list_recv_itemids: if (
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
for addr_to_update in item_data.update_ram_addr: for addr_to_update in item_data.update_ram_addr:
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 is_binary: if is_binary:
if addr_to_update.ram_address in ram_addr_dict.keys(): if addr_to_update.ram_address in ram_addr_dict.keys():
current_bin_value = ram_addr_dict[addr_to_update.ram_address][0] current_bin_value = ram_addr_dict[addr_to_update.ram_address][0]
else: else:
current_bin_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( current_bin_value = int.from_bytes(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little") (
await bizhawk.read(
ctx.bizhawk_ctx,
[
(
addr_to_update.ram_address,
addr_to_update.byte_size,
"MainRAM",
)
],
)
)[0],
"little",
)
if GrinchLocation.get_apid(item_data.id) in list_recv_itemids: if GrinchLocation.get_apid(item_data.id) in list_recv_itemids:
current_bin_value |= (1 << addr_to_update.binary_bit_pos) current_bin_value |= 1 << addr_to_update.binary_bit_pos
else: else:
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]
else: else:
if GrinchLocation.get_apid(item_data.id) in list_recv_itemids: if GrinchLocation.get_apid(item_data.id) in list_recv_itemids:
ram_addr_dict[addr_to_update.ram_address] = [addr_to_update.value, addr_to_update.bit_size] ram_addr_dict[addr_to_update.ram_address] = [
else: addr_to_update.value,
ram_addr_dict[addr_to_update.ram_address] = [0, addr_to_update.bit_size] addr_to_update.byte_size,
]
await bizhawk.write(ctx.bizhawk_ctx, self.convert_dict_to_ram_list(ram_addr_dict)) else:
ram_addr_dict[addr_to_update.ram_address] = [
0,
addr_to_update.byte_size,
]
await bizhawk.write(
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
ingame_map_id = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( ingame_map_id = int.from_bytes(
0x010000, 1, "MainRAM")]))[0], "little") (await bizhawk.read(ctx.bizhawk_ctx, [(0x010000, 1, "MainRAM")]))[0],
initial_cutscene_checker = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( "little",
0x010094, 1, "MainRAM")]))[0], "little") )
initial_cutscene_checker = int.from_bytes(
(await bizhawk.read(ctx.bizhawk_ctx, [(0x010094, 1, "MainRAM")]))[0],
"little",
)
#If not in game or at a menu, or loading the publisher logos # If not in game or at a menu, or loading the publisher logos
if ingame_map_id <= 0x04 or ingame_map_id >= 0x35: if ingame_map_id <= 0x04 or ingame_map_id >= 0x35:
self.ingame_log = False self.ingame_log = False
return False return False
#If grinch has changed maps # If grinch has changed maps
if not ingame_map_id == self.last_map_location: if not ingame_map_id == self.last_map_location:
# If the last "map" we were on was a menu or a publisher logo # If the last "map" we were on was a menu or a publisher logo
if self.last_map_location in MENU_MAP_IDS: if self.last_map_location in MENU_MAP_IDS:
# Reset our demo mode checker just in case the game is in demo mode. # Reset our demo mode checker just in case the game is in demo mode.
self.demo_mode_buffer = 0 self.demo_mode_buffer = 0
self.ingame_log = False self.ingame_log = False
if initial_cutscene_checker != 1: if initial_cutscene_checker != 1:
return False return False
@@ -354,27 +557,42 @@ class GrinchClient(BizHawkClient):
self.demo_mode_buffer += 1 self.demo_mode_buffer += 1
return False return False
demo_mode = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [( demo_mode = int.from_bytes(
0x01008A, 1, "MainRAM")]))[0], "little") (await bizhawk.read(ctx.bizhawk_ctx, [(0x01008A, 1, "MainRAM")]))[0],
"little",
)
if demo_mode == 1: if demo_mode == 1:
return False return False
if not self.ingame_log: if not self.ingame_log:
logger.info("You can now start sending locations from the Grinch!") logger.info("You can now start sending locations from the Grinch!")
self.ingame_log = True self.ingame_log = True
return True return True
async def option_handler(self, ctx: "BizHawkClientContext"): async def option_handler(self, ctx: "BizHawkClientContext"):
if self.loc_unlimited_eggs: if self.loc_unlimited_eggs:
await bizhawk.write(ctx.bizhawk_ctx, [(EGG_COUNT_ADDR, MAX_EGGS.to_bytes(2,"little"), "MainRAM")]) await bizhawk.write(
ctx.bizhawk_ctx,
[(EGG_COUNT_ADDR, MAX_EGGS.to_bytes(2, "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(
(await bizhawk.read(ctx.bizhawk_ctx, [(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")]))[0], "little") (
await bizhawk.read(
ctx.bizhawk_ctx,
[(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")],
)
)[0],
"little",
)
if (current_egg_count - self.previous_egg_count) != 0: if (current_egg_count - self.previous_egg_count) != 0:
msg = { msg = {
@@ -382,29 +600,55 @@ class GrinchClient(BizHawkClient):
"data": { "data": {
"time": time.time(), "time": time.time(),
"source": self.unique_client_id, "source": self.unique_client_id,
"amount": current_egg_count - self.previous_egg_count "amount": current_egg_count - self.previous_egg_count,
}, },
"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.")
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
except Exception as ex: except Exception as ex:
logger.error("While monitoring grinch's egg count ingame, an error occurred. Details:"+ str(ex)) logger.error(
"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("You must be connected to the multi-world in order for RingLink to work properly.") logger.info(
"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], "little") (
non_neg_eggs = game_egg_count + egg_amount if game_egg_count + egg_amount > 0 else 0 await bizhawk.read(
ctx.bizhawk_ctx, [(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")]
)
)[0],
"little",
)
non_neg_eggs = (
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(ctx.bizhawk_ctx, [(EGG_COUNT_ADDR,
int(current_egg_count).to_bytes(EGG_ADDR_BYTESIZE, "little"), "MainRAM")]) await bizhawk.write(
ctx.bizhawk_ctx,
[
(
EGG_COUNT_ADDR,
int(current_egg_count).to_bytes(EGG_ADDR_BYTESIZE, "little"),
"MainRAM",
)
],
)
self.previous_egg_count = current_egg_count self.previous_egg_count = current_egg_count
# logger.info(f"RingLink: You received {str(egg_amount)} rotten eggs.") # logger.info(f"RingLink: You received {str(egg_amount)} rotten eggs.")
@@ -415,18 +659,27 @@ class GrinchClient(BizHawkClient):
uid += ord(char) uid += ord(char)
return uid return uid
def _cmd_ringlink(self): def _cmd_ringlink(self):
"""Toggle ringling from client. Overrides default setting.""" """Toggle ringling from client. Overrides default setting."""
if not self.ctx.slot: if not self.ctx.slot:
return return
Utils.async_start(_update_ring_link(self.ctx, not "RingLink" in self.ctx.tags), name="Update RingLink")
Utils.async_start(
_update_ring_link(self.ctx, not "RingLink" in self.ctx.tags),
name="Update RingLink",
)
async def _update_ring_link(ctx: "BizHawkClientContext", ring_link: bool): async def _update_ring_link(ctx: "BizHawkClientContext", ring_link: bool):
"""Helper function to set Ring Link connection tag on/off and update the connection if already connected.""" """Helper function to set Ring Link connection tag on/off and update the connection if already connected."""
old_tags = copy.deepcopy(ctx.tags) old_tags = copy.deepcopy(ctx.tags)
if ring_link: if ring_link:
ctx.tags.add("RingLink") ctx.tags.add("RingLink")
else: else:
ctx.tags -= {"RingLink"} ctx.tags -= {"RingLink"}
if old_tags != ctx.tags and ctx.server and not ctx.server.socket.closed: if old_tags != ctx.tags and ctx.server and not ctx.server.socket.closed:
await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}]) await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}])

View File

@@ -1,32 +1,41 @@
from typing import NamedTuple, Optional from typing import NamedTuple, Optional
from .RamHandler import GrinchRamData from .RamHandler import GrinchRamData, UpdateMethod
from BaseClasses import Item from BaseClasses import Item
from BaseClasses import ItemClassification as IC #IC can be any name, saves having to type the whole word in code from BaseClasses import (
ItemClassification as IC,
) # IC can be any name, saves having to type the whole word in code
class GrinchItemData(NamedTuple): class GrinchItemData(NamedTuple):
item_group: list[str] #arbituary that can be whatever it can be, basically the field/property for item groups item_group: list[
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]
class GrinchItem(Item): class GrinchItem(Item):
game: str = "The Grinch" game: str = "The Grinch"
#Tells server what item id it is # Tells server what item id it is
@staticmethod @staticmethod
def get_apid(id: int): def get_apid(id: int):
#If you give me an input id, I will return the Grinch equivalent server/ap id # If you give me an input id, I will return the Grinch equivalent server/ap id
base_id: int = 42069 base_id: int = 42069
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__(name,data.classification, GrinchItem.get_apid(data.id), player) super(GrinchItem, self).__init__(
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
#allows hinting of items via category
# allows hinting of items via category
def get_item_names_per_category() -> dict[str, set[str]]: def get_item_names_per_category() -> dict[str, set[str]]:
categories: dict[str, set[str]] = {} categories: dict[str, set[str]] = {}
@@ -36,123 +45,306 @@ def get_item_names_per_category() -> dict[str, set[str]]:
return categories return categories
REL: str = "Rotten Egg Launcher"
RS: str = "Rocket Spring"
SS: str = "Slime Shooter"
OCD: str = "Octopus Climbing Device"
MM: str = "Marine Mobile"
GC: str = "Grinch Copter"
WV: str = "Whoville Vacuum Tube"
WF: str = "Who Forest Vacuum Tube"
WD: str = "Who Dump Vacuum Tube"
WL: str = "Who Lake Vacuum Tube"
VT: str = "Progressive Vacuum Tube"
PC: str = "Pancake"
SR: str = "Sleigh Room Key"
BB: str = "Bad Breath"
SE: str = "Seize"
MX: str = "Max"
SN: str = "Sneak"
WC: str = "Who Cloak"
PB: str = "Painting Bucket"
SC: str = "Scissors"
GB: str = "Glue Bucket"
CCAC: str = "Cable Car Access Card"
DRL: str = "Drill"
RP: str = "Rope"
HK: str = "Hook"
ST: str = "Sculpting Tools"
HMR: str = "Hammer"
SCL: str = "Scout Clothes"
#Gadgets class grinch_items:
#All gadgets require at least 4 different blueprints to be unlocked in the computer in Mount Crumpit. class gadgets:
BINOCULARS: str = "Binoculars"
ROCKET_EGG_LAUNCHER: str = "Rotten Egg Launcher"
ROCKET_SPRING: str = "Rocket Spring"
SLIME_SHOOTER: str = "Slime Shooter"
OCTOPUS_CLIMBING_DEVICE: str = "Octopus Climbing Device"
MARINE_MOBILE: str = "Marine Mobile"
GRINCH_COPTER: str = "Grinch Copter"
class keys:
WHOVILLE: str = "Whoville Vacuum Tube"
WHO_FOREST: str = "Who Forest Vacuum Tube"
WHO_DUMP: str = "Who Dump Vacuum Tube"
WHO_LAKE: str = "Who Lake Vacuum Tube"
PROGRESSIVE_VACUUM_TUBE: str = "Progressive Vacuum Tube"
SLEIGH_ROOM_KEY: str = "Sleigh Room Key"
class moves:
PANCAKE: str = "Pancake"
BAD_BREATH: str = "Bad Breath"
SIEZE: str = "Seize"
MAX: str = "Max"
SNEAK: str = "Sneak"
class level_items:
WV_WHO_CLOAK: str = "Who Cloak"
WV_PAINT_BUCKET: str = "Painting Bucket"
WV_HAMMER: str = "Hammer"
WV_SCULPTIN_TOOLS: str = "Sculpting Tools"
WF_GLUE_BUCKET: str = "Glue Bucket"
WF_CABLE_CAR_ACCESS_CARD: str = "Cable Car Access Card"
WD_SCISSORS: str = "Scissors"
WL_ROPE: str = "Rope"
WL_HOOK: str = "Hook"
WL_DRILLL: str = "Drill"
WL_SCOUT_CLOTHES: str = "Scout Clothes"
class useful_items:
HEART_OF_STONE: str = "Heart of Stone"
class trap_items:
DEPLETION_TRAP: str = "Depletion Trap"
DUMP_IT_TO_CRUMPIT: str = "Dump it to Crumpit"
WHO_SENT_ME_BACK: str = "Who sent me back?"
class grinch_categories:
FILLER: str = "Filler"
GADGETS: str = "Gadgets"
HEALING_ITEMS: str = "Healing Items"
MISSION_SPECIFIC_ITEMS: str = "Mission Specific Items"
MOVES: str = "Moves"
REQUIRED_ITEM: str = "Required Items"
ROTTEN_EGG_BUNDLES: str = "Rotten Egg Bundles"
SLEIGH_ROOM: str = "Sleigh Room"
TRAPS: str = "Traps"
USEFUL_ITEMS: str = "Useful Items"
VACUUM_TUBES: str = "Vacuum Tubes"
# Gadgets
# All gadgets require at least 4 different blueprints to be unlocked in the computer in Mount Crumpit.
GADGETS_TABLE: dict[str, GrinchItemData] = { GADGETS_TABLE: dict[str, GrinchItemData] = {
"Binoculars": GrinchItemData(["Gadgets"], 100, IC.useful, grinch_items.gadgets.BINOCULARS: GrinchItemData(
[GrinchRamData(0x0102B6, value=0x40), GrinchRamData(0x0102B7, value=0x41), [grinch_categories.GADGETS],
GrinchRamData(0x0102B8, value=0x44), GrinchRamData(0x0102B9, value=0x45), 100,
# GrinchRamData(0x0100BC, binary_bit_pos=0) IC.useful,
]), [
"Rotten Egg Launcher": GrinchItemData(["Gadgets"], 101, IC.progression, GrinchRamData(0x0102B6, value=0x40),
[GrinchRamData(0x0102BA, value=0x40), GrinchRamData(0x0102BB, value=0x41), GrinchRamData(0x0102B7, value=0x41),
GrinchRamData(0x0102BC, value=0x44), GrinchRamData(0x0102BD, value=0x45), GrinchRamData(0x0102B8, value=0x44),
# GrinchRamData(0x0100BC, binary_bit_pos=1) GrinchRamData(0x0102B9, value=0x45),
]), # GrinchRamData(0x0100BC, binary_bit_pos=0)
"Rocket Spring": GrinchItemData(["Gadgets"], 102, IC.progression, ],
[GrinchRamData(0x0102BE, value=0x40), GrinchRamData(0x0102BF, value=0x41), ),
GrinchRamData(0x0102C0, value=0x42), GrinchRamData(0x0102C1, value=0x44), grinch_items.gadgets.ROCKET_EGG_LAUNCHER: GrinchItemData(
GrinchRamData(0x0102C2, value=0x45), GrinchRamData(0x0102C3, value=0x46), [grinch_categories.GADGETS],
GrinchRamData(0x0102C4, value=0x48), GrinchRamData(0x0102C5, value=0x49), 101,
GrinchRamData(0x0102C6, value=0x4A), IC.progression,
# GrinchRamData(0x0100BC, binary_bit_pos=2) [
]), GrinchRamData(0x0102BA, value=0x40),
"Slime Shooter": GrinchItemData(["Gadgets", "Slime Gun"], 103, IC.progression, GrinchRamData(0x0102BB, value=0x41),
[GrinchRamData(0x0102C7, value=0x40), GrinchRamData(0x0102C8, value=0x41), GrinchRamData(0x0102BC, value=0x44),
GrinchRamData(0x0102C9, value=0x42), GrinchRamData(0x0102CA, value=0x44), GrinchRamData(0x0102BD, value=0x45),
GrinchRamData(0x0102CB, value=0x45), GrinchRamData(0x0102CC, value=0x46), # GrinchRamData(0x0100BC, binary_bit_pos=1)
GrinchRamData(0x0102CD, value=0x48), GrinchRamData(0x0102CE, value=0x49), ],
GrinchRamData(0x0102CF, value=0x4A), ),
# GrinchRamData(0x0100BC, binary_bit_pos=3) grinch_items.gadgets.ROCKET_SPRING: GrinchItemData(
]), [grinch_categories.GADGETS],
"Octopus Climbing Device": GrinchItemData(["Gadgets"], 104, IC.progression, 102,
[GrinchRamData(0x0102D0, value=0x40), GrinchRamData(0x0102D1, value=0x41), IC.progression,
GrinchRamData(0x0102D2, value=0x42), GrinchRamData(0x0102D3, value=0x44), [
GrinchRamData(0x0102D4, value=0x45), GrinchRamData(0x0102D5, value=0x46), GrinchRamData(0x0102BE, value=0x40),
GrinchRamData(0x0102D6, value=0x48), GrinchRamData(0x0102D7, value=0x49), GrinchRamData(0x0102BF, value=0x41),
GrinchRamData(0x0102D8, value=0x4A), GrinchRamData(0x0102C0, value=0x42),
# GrinchRamData(0x0100BC, binary_bit_pos=4) GrinchRamData(0x0102C1, value=0x44),
]), GrinchRamData(0x0102C2, value=0x45),
"Marine Mobile": GrinchItemData(["Gadgets"], 105, IC.progression, GrinchRamData(0x0102C3, value=0x46),
[GrinchRamData(0x0102D9, value=0x40), GrinchRamData(0x0102DA, value=0x41), GrinchRamData(0x0102C4, value=0x48),
GrinchRamData(0x0102DB, value=0x42), GrinchRamData(0x0102DC, value=0x43), GrinchRamData(0x0102C5, value=0x49),
GrinchRamData(0x0102DD, value=0x44), GrinchRamData(0x0102DE, value=0x45), GrinchRamData(0x0102C6, value=0x4A),
GrinchRamData(0x0102DF, value=0x46), GrinchRamData(0x0102E0, value=0x47), # GrinchRamData(0x0100BC, binary_bit_pos=2)
GrinchRamData(0x0102E1, value=0x48), GrinchRamData(0x0102E2, value=0x49), ],
GrinchRamData(0x0102E3, value=0x4A), GrinchRamData(0x0102E4, value=0x4B), ),
GrinchRamData(0x0102E5, value=0x4C), GrinchRamData(0x0102E6, value=0x4D), grinch_items.gadgets.SLIME_SHOOTER: GrinchItemData(
GrinchRamData(0x0102E7, value=0x4E), GrinchRamData(0x0102E8, value=0x4F), [
# GrinchRamData(0x0100BC, binary_bit_pos=5) grinch_categories.GADGETS,
]), "Slime Gun", # For canon --MarioSpore
"Grinch Copter": GrinchItemData(["Gadgets"], 106, IC.progression, ],
[GrinchRamData(0x0102E9, value=0x40), GrinchRamData(0x0102EA, value=0x41), 103,
GrinchRamData(0x0102EB, value=0x42), GrinchRamData(0x0102EC, value=0x43), IC.progression,
GrinchRamData(0x0102ED, value=0x44), GrinchRamData(0x0102EE, value=0x45), [
GrinchRamData(0x0102EF, value=0x46), GrinchRamData(0x0102F0, value=0x47), GrinchRamData(0x0102C7, value=0x40),
GrinchRamData(0x0102F1, value=0x48), GrinchRamData(0x0102F2, value=0x49), GrinchRamData(0x0102C8, value=0x41),
GrinchRamData(0x0102F3, value=0x4A), GrinchRamData(0x0102F4, value=0x4B), GrinchRamData(0x0102C9, value=0x42),
GrinchRamData(0x0102F5, value=0x4C), GrinchRamData(0x0102F6, value=0x4D), GrinchRamData(0x0102CA, value=0x44),
GrinchRamData(0x0102F7, value=0x4E), GrinchRamData(0x0102F8, value=0x4F), GrinchRamData(0x0102CB, value=0x45),
# GrinchRamData(0x0100BC, binary_bit_pos=6) GrinchRamData(0x0102CC, value=0x46),
]) GrinchRamData(0x0102CD, value=0x48),
GrinchRamData(0x0102CE, value=0x49),
GrinchRamData(0x0102CF, value=0x4A),
# GrinchRamData(0x0100BC, binary_bit_pos=3)
],
),
grinch_items.gadgets.OCTOPUS_CLIMBING_DEVICE: GrinchItemData(
[grinch_categories.GADGETS],
104,
IC.progression,
[
GrinchRamData(0x0102D0, value=0x40),
GrinchRamData(0x0102D1, value=0x41),
GrinchRamData(0x0102D2, value=0x42),
GrinchRamData(0x0102D3, value=0x44),
GrinchRamData(0x0102D4, value=0x45),
GrinchRamData(0x0102D5, value=0x46),
GrinchRamData(0x0102D6, value=0x48),
GrinchRamData(0x0102D7, value=0x49),
GrinchRamData(0x0102D8, value=0x4A),
# GrinchRamData(0x0100BC, binary_bit_pos=4)
],
),
grinch_items.gadgets.MARINE_MOBILE: GrinchItemData(
[grinch_categories.GADGETS],
105,
IC.progression,
[
GrinchRamData(0x0102D9, value=0x40),
GrinchRamData(0x0102DA, value=0x41),
GrinchRamData(0x0102DB, value=0x42),
GrinchRamData(0x0102DC, value=0x43),
GrinchRamData(0x0102DD, value=0x44),
GrinchRamData(0x0102DE, value=0x45),
GrinchRamData(0x0102DF, value=0x46),
GrinchRamData(0x0102E0, value=0x47),
GrinchRamData(0x0102E1, value=0x48),
GrinchRamData(0x0102E2, value=0x49),
GrinchRamData(0x0102E3, value=0x4A),
GrinchRamData(0x0102E4, value=0x4B),
GrinchRamData(0x0102E5, value=0x4C),
GrinchRamData(0x0102E6, value=0x4D),
GrinchRamData(0x0102E7, value=0x4E),
GrinchRamData(0x0102E8, value=0x4F),
# GrinchRamData(0x0100BC, binary_bit_pos=5)
],
),
grinch_items.gadgets.GRINCH_COPTER: GrinchItemData(
[grinch_categories.GADGETS],
106,
IC.progression,
[
GrinchRamData(0x0102E9, value=0x40),
GrinchRamData(0x0102EA, value=0x41),
GrinchRamData(0x0102EB, value=0x42),
GrinchRamData(0x0102EC, value=0x43),
GrinchRamData(0x0102ED, value=0x44),
GrinchRamData(0x0102EE, value=0x45),
GrinchRamData(0x0102EF, value=0x46),
GrinchRamData(0x0102F0, value=0x47),
GrinchRamData(0x0102F1, value=0x48),
GrinchRamData(0x0102F2, value=0x49),
GrinchRamData(0x0102F3, value=0x4A),
GrinchRamData(0x0102F4, value=0x4B),
GrinchRamData(0x0102F5, value=0x4C),
GrinchRamData(0x0102F6, value=0x4D),
GrinchRamData(0x0102F7, value=0x4E),
GrinchRamData(0x0102F8, value=0x4F),
# GrinchRamData(0x0100BC, binary_bit_pos=6)
],
),
} }
#Mission Specific Items # Mission Specific Items
MISSION_ITEMS_TABLE: dict[str, GrinchItemData] = { MISSION_ITEMS_TABLE: dict[str, GrinchItemData] = {
"Who Cloak": GrinchItemData(["Mission Specific Items", "Useful Items"], 200, IC.progression, grinch_items.level_items.WV_WHO_CLOAK: GrinchItemData(
[GrinchRamData(0x0101F9, binary_bit_pos=0)]), [
"Painting Bucket": GrinchItemData(["Mission Specific Items", "Useful Items"], 201, IC.progression_deprioritized, grinch_categories.MISSION_SPECIFIC_ITEMS,
[GrinchRamData(0x0101F9, binary_bit_pos=1)]), grinch_categories.USEFUL_ITEMS,
"Scissors": GrinchItemData(["Mission Specific Items", "Useful Items"], 202, IC.progression_deprioritized, ],
[GrinchRamData(0x0101F9, binary_bit_pos=6), GrinchRamData(0x0100C2, binary_bit_pos=1)]), 200,
"Glue Bucket": GrinchItemData(["Mission Specific Items", "Useful Items"], 203, IC.progression_deprioritized, IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=4)]), [GrinchRamData(0x0101F9, binary_bit_pos=0)],
"Cable Car Access Card": GrinchItemData(["Mission Specific Items", "Useful Items"], 204, IC.progression, ),
[GrinchRamData(0x0101F9, binary_bit_pos=5)]), grinch_items.level_items.WV_PAINT_BUCKET: GrinchItemData(
"Drill": GrinchItemData(["Mission Specific Items", "Useful Items"], 205, IC.progression_deprioritized, [
[GrinchRamData(0x0101FA, binary_bit_pos=2)]), grinch_categories.MISSION_SPECIFIC_ITEMS,
"Rope": GrinchItemData(["Mission Specific Items", "Useful Items"], 206, IC.progression_deprioritized, grinch_categories.USEFUL_ITEMS,
[GrinchRamData(0x0101FA, binary_bit_pos=1)]), ],
"Hook": GrinchItemData(["Mission Specific Items", "Useful Items"], 207, IC.progression_deprioritized, 201,
[GrinchRamData(0x0101FA, binary_bit_pos=0)]), IC.progression_deprioritized,
"Sculpting Tools": GrinchItemData(["Mission Specific Items", "Useful Items"], 208, IC.progression_deprioritized, [GrinchRamData(0x0101F9, binary_bit_pos=1)],
[GrinchRamData(0x0101F9, binary_bit_pos=2)]), ),
"Hammer": GrinchItemData(["Mission Specific Items", "Useful Items"], 209, IC.progression_deprioritized, grinch_items.level_items.WD_SCISSORS: GrinchItemData(
[GrinchRamData(0x0101F9, binary_bit_pos=3)]), [
"Scout Clothes": GrinchItemData(["Mission Specific Items", "Useful Items"], 210, IC.progression, grinch_categories.MISSION_SPECIFIC_ITEMS,
[GrinchRamData(0x0101F9, binary_bit_pos=7)]) grinch_categories.USEFUL_ITEMS,
],
202,
IC.progression_deprioritized,
[
GrinchRamData(0x0101F9, binary_bit_pos=6),
GrinchRamData(0x0100C2, binary_bit_pos=1),
],
),
grinch_items.level_items.WF_GLUE_BUCKET: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
203,
IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=4)],
),
grinch_items.level_items.WF_CABLE_CAR_ACCESS_CARD: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
204,
IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=5)],
),
grinch_items.level_items.WL_DRILLL: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
205,
IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=2)],
),
grinch_items.level_items.WL_ROPE: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
206,
IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=1)],
),
grinch_items.level_items.WL_HOOK: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
207,
IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=0)],
),
grinch_items.level_items.WV_SCULPTIN_TOOLS: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
208,
IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=2)],
),
grinch_items.level_items.WV_HAMMER: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
209,
IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=3)],
),
grinch_items.level_items.WL_SCOUT_CLOTHES: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
210,
IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=7)],
),
} }
#Sleigh Parts # Sleigh Parts
# SLEIGH_PARTS_TABLE: dict[str, GrinchItemData] = { # SLEIGH_PARTS_TABLE: dict[str, GrinchItemData] = {
# "Exhaust Pipes": GrinchItemData(["Sleigh Parts"], 300, IC.progression_skip_balancing, # "Exhaust Pipes": GrinchItemData(["Sleigh Parts"], 300, IC.progression_skip_balancing,
# [GrinchRamData(0x0101FB, binary_bit_pos=2)]), # [GrinchRamData(0x0101FB, binary_bit_pos=2)]),
@@ -166,16 +358,32 @@ MISSION_ITEMS_TABLE: dict[str, GrinchItemData] = {
# [GrinchRamData(0x0101FB, binary_bit_pos=6)]) # [GrinchRamData(0x0101FB, binary_bit_pos=6)])
# } # }
#Access Keys # Access Keys
KEYS_TABLE: dict[str, GrinchItemData] = { KEYS_TABLE: dict[str, GrinchItemData] = {
"Whoville Vacuum Tube": GrinchItemData(["Vacuum Tubes"], 400, IC.progression, grinch_items.keys.WHOVILLE: GrinchItemData(
[GrinchRamData(0x010200, binary_bit_pos=1)]), [grinch_categories.VACUUM_TUBES],
"Who Forest Vacuum Tube": GrinchItemData(["Vacuum Tubes"], 401, IC.progression, 400,
[GrinchRamData(0x0100AA, binary_bit_pos=2)]), IC.progression,
"Who Dump Vacuum Tube": GrinchItemData(["Vacuum Tubes"], 402, IC.progression, [GrinchRamData(0x010200, binary_bit_pos=1)],
[GrinchRamData(0x0100AA, binary_bit_pos=3)]), ),
"Who Lake Vacuum Tube": GrinchItemData(["Vacuum Tubes"], 403, IC.progression, grinch_items.keys.WHO_FOREST: GrinchItemData(
[GrinchRamData(0x0100AA, binary_bit_pos=4)]), [grinch_categories.VACUUM_TUBES],
401,
IC.progression,
[GrinchRamData(0x0100AA, binary_bit_pos=2)],
),
grinch_items.keys.WHO_DUMP: GrinchItemData(
[grinch_categories.VACUUM_TUBES],
402,
IC.progression,
[GrinchRamData(0x0100AA, binary_bit_pos=3)],
),
grinch_items.keys.WHO_LAKE: GrinchItemData(
[grinch_categories.VACUUM_TUBES],
403,
IC.progression,
[GrinchRamData(0x0100AA, binary_bit_pos=4)],
),
# "Progressive Vacuum Tube": GrinchItemData(["Vacuum Tubes"], 404, IC.progression, # "Progressive Vacuum Tube": GrinchItemData(["Vacuum Tubes"], 404, IC.progression,
# [GrinchRamData()]), # [GrinchRamData()]),
# "Spin N' Win Door Unlock": GrinchItemData(["Supadow Door Unlocks"], 405, IC.progression, # "Spin N' Win Door Unlock": GrinchItemData(["Supadow Door Unlocks"], 405, IC.progression,
@@ -188,71 +396,196 @@ KEYS_TABLE: dict[str, GrinchItemData] = {
# [GrinchRamData()]), # [GrinchRamData()]),
# "Bike Race Access": GrinchItemData(["Supadow Door Unlocks", 409, IC.progression, # "Bike Race Access": GrinchItemData(["Supadow Door Unlocks", 409, IC.progression,
# [GrinchRamData()]) # [GrinchRamData()])
"Sleigh Room Key": GrinchItemData(["Sleigh Room"], 410, IC.progression, grinch_items.keys.SLEIGH_ROOM_KEY: GrinchItemData(
[GrinchRamData(0x010200, binary_bit_pos=6), GrinchRamData(0x0100AA, binary_bit_pos=5)]) [
grinch_categories.SLEIGH_ROOM,
grinch_categories.REQUIRED_ITEM,
],
410,
IC.progression,
[
GrinchRamData(0x010200, binary_bit_pos=6),
GrinchRamData(0x0100AA, binary_bit_pos=5),
],
),
} }
#Misc Items # Misc Items
MISC_ITEMS_TABLE: dict[str, GrinchItemData] = { MISC_ITEMS_TABLE: dict[str, GrinchItemData] = {
# This item may not function properly if you receive it during a loading screen or in Mount Crumpit # This item may not function properly if you receive it during a loading screen or in Mount Crumpit
# "Fully Healed Grinch": GrinchItemData(["Health Items", "Filler"], 500, IC.filler, # "Fully Healed Grinch": GrinchItemData(["Health Items", "Filler"], 500, IC.filler,
# [GrinchRamData(0x0E8FDC, value=120)]), # [GrinchRamData(0x0E8FDC, value=120)]),
"5 Rotten Eggs": GrinchItemData(["Rotten Egg Bundles", "Filler"], 502, IC.filler, "5 Rotten Eggs": GrinchItemData(
[GrinchRamData(0x010058, value=5, update_existing_value=True, max_count=200, bit_size=2)]), [
"10 Rotten Eggs": GrinchItemData(["Rotten Egg Bundles", "Filler"], 503, IC.filler, grinch_categories.ROTTEN_EGG_BUNDLES,
[GrinchRamData(0x010058, value=10, update_existing_value=True, max_count=200, bit_size=2)]), grinch_categories.FILLER,
"20 Rotten Eggs": GrinchItemData(["Rotten Egg Bundles", "Filler"], 504, IC.filler, ],
[GrinchRamData(0x010058, value=20, update_existing_value=True, max_count=200, bit_size=2)]) 502,
IC.filler,
[
GrinchRamData(
0x010058,
value=5,
update_method=UpdateMethod.ADD,
max_count=200,
byte_size=2,
)
],
),
"10 Rotten Eggs": GrinchItemData(
[
grinch_categories.ROTTEN_EGG_BUNDLES,
grinch_categories.FILLER,
],
503,
IC.filler,
[
GrinchRamData(
0x010058,
value=10,
update_method=UpdateMethod.ADD,
max_count=200,
byte_size=2,
)
],
),
"20 Rotten Eggs": GrinchItemData(
[
grinch_categories.ROTTEN_EGG_BUNDLES,
grinch_categories.FILLER,
],
504,
IC.filler,
[
GrinchRamData(
0x010058,
value=20,
update_method=UpdateMethod.ADD,
max_count=200,
byte_size=2,
)
],
),
} }
USEFUL_IC_TABLE: dict[str, GrinchItemData] = { USEFUL_ITEMS_TABLE: dict[str, GrinchItemData] = {
"Heart of Stone": GrinchItemData(["Health Items"], 501, IC.useful, grinch_items.useful_items.HEART_OF_STONE: GrinchItemData(
[GrinchRamData(0x0100ED, value=1, update_existing_value=True, max_count=4)]) [
grinch_categories.USEFUL_ITEMS,
grinch_categories.HEALING_ITEMS,
],
501,
IC.useful,
[
GrinchRamData(
0x0100ED,
value=1,
update_method=UpdateMethod.ADD,
max_count=4,
)
],
)
} }
#Traps # Traps
TRAPS_TABLE: dict[str, GrinchItemData] = { TRAPS_TABLE: dict[str, GrinchItemData] = {
# alias to Ice Trap for traplink # alias to Ice Trap for traplink
# "Freeze Trap": GrinchItemData(["Traps"], 600, IC.trap, [GrinchRamData()]), # "Freeze Trap": GrinchItemData(["Traps"], 600, IC.trap, [GrinchRamData()]),
# "Bee Trap": GrinchItemData(["Traps"], 601, IC.trap, [GrinchRamData()]), # "Bee Trap": GrinchItemData(["Traps"], 601, IC.trap, [GrinchRamData()]),
# "Electrocution Trap": GrinchItemData(["Traps"], 602, IC.trap, [GrinchRamData()]), # "Electrocution Trap": GrinchItemData(["Traps"], 602, IC.trap, [GrinchRamData()]),
# alias to Slowness Trap for traplink # alias to Slowness Trap for traplink
# "Tip Toe Trap": GrinchItemData(["Traps"], 603, IC.trap, [GrinchRamData()]), # "Tip Toe Trap": GrinchItemData(["Traps"], 603, IC.trap, [GrinchRamData()]),
# This item may not function properly if you receive it during a loading screen or in Mount Crumpit # This item may not function properly if you receive it during a loading screen or in Mount Crumpit
# alias to Exhaustion Trap # alias to Exhaustion Trap
# "Damage Trap": GrinchItemData(["Traps"], 604, IC.trap, [GrinchRamData(0x0E8FDC, value=-20, update_existing_value=True)]), # "Damage Trap": GrinchItemData(["Traps"], 604, IC.trap, [GrinchRamData(0x0E8FDC, value=-20, update_method=UpdateMethod.ADD)]),
"Depletion Trap": GrinchItemData(["Traps"], 605, IC.trap, [GrinchRamData(0x010058, value=0, bit_size=2)]), grinch_items.trap_items.DEPLETION_TRAP: GrinchItemData(
"Dump it to Crumpit": GrinchItemData(["Traps"], 606, IC.trap, #Alias to Home Trap for traplink [grinch_categories.TRAPS],
[GrinchRamData(0x010000, value=0x05), GrinchRamData(0x08FB94, value=1), GrinchRamData(0x0100B4, value=0)]), 605,
#alias to Spring Trap for traplink IC.trap,
[GrinchRamData(0x010058, value=0, byte_size=2)],
),
grinch_items.trap_items.DUMP_IT_TO_CRUMPIT: GrinchItemData(
[grinch_categories.TRAPS],
606,
IC.trap, # Alias to Home Trap for traplink
[
GrinchRamData(0x010000, value=0x05),
GrinchRamData(0x08FB94, value=1),
GrinchRamData(0x0100B4, value=0),
],
),
# alias to Spring Trap for traplink
# "Rocket Spring Trap": GrinchItemData(["Traps"], 607, IC.trap, [GrinchRamData()]), # "Rocket Spring Trap": GrinchItemData(["Traps"], 607, IC.trap, [GrinchRamData()]),
#alias to Home Trap for traplink # alias to Home Trap for traplink
"Who sent me back?": GrinchItemData(["Traps"], 608, IC.trap, [GrinchRamData(0x08FB94, value=1)]), grinch_items.trap_items.WHO_SENT_ME_BACK: GrinchItemData(
[grinch_categories.TRAPS],
608,
IC.trap,
[
GrinchRamData(0x08FB94, value=1),
],
),
# "Cutscene Trap": GrinchItemData(["Traps"], 609, IC.trap, [GrinchRamData()]), # "Cutscene Trap": GrinchItemData(["Traps"], 609, IC.trap, [GrinchRamData()]),
# "No Vac Trap": GrinchItemData(["Traps"], 610, IC.trap, [GrinchRamData(0x0102DA, value=0]), # "No Vac Trap": GrinchItemData(["Traps"], 610, IC.trap, [GrinchRamData(0x0102DA, value=0]),
# "Invisible Trap": GrinchItemData(["Traps"], 611, IC.trap, [GrinchRamData(0x0102DA, value=0, bit_size=4)]) # "Invisible Trap": GrinchItemData(["Traps"], 611, IC.trap, [GrinchRamData(0x0102DA, value=0, byte_size=4)])
# "Child Trap": GrinchItemData(["Traps"], 612, IC.trap,[GrinchRamData()]) # "Child Trap": GrinchItemData(["Traps"], 612, IC.trap,[GrinchRamData()])
# "Disable Jump Trap": GrinchItemData(["Traps"], 613, IC.trap,[GrinchRamData(0x010026, binary_bit_pos=6)]) # "Disable Jump Trap": GrinchItemData(["Traps"], 613, IC.trap,[GrinchRamData(0x010026, binary_bit_pos=6)])
} }
#Movesets # Movesets
# MOVES_TABLE: dict[str, GrinchItemData] = { MOVES_TABLE: dict[str, GrinchItemData] = {
# "Bad Breath": GrinchItemData(["Movesets"], 700, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=1)]), grinch_items.moves.BAD_BREATH: GrinchItemData(
# "Pancake": GrinchItemData(["Movesets"], 701, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=2)]), [grinch_categories.MOVES],
# "Push & Pull": GrinchItemData(["Movesets"], 702, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=3)]), 700,
# "Max": GrinchItemData(["Movesets"], 703, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=4)]), IC.progression,
# "Tip Toe": GrinchItemData(["Movesets"], 704, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=5)]) [
# } GrinchRamData(0x0100BB, binary_bit_pos=1),
#Double star combines all dictionaries from each individual list together ],
),
grinch_items.moves.PANCAKE: GrinchItemData(
[grinch_categories.MOVES],
701,
IC.progression,
[
GrinchRamData(0x0100BB, binary_bit_pos=2),
],
),
grinch_items.moves.SIEZE: GrinchItemData(
[grinch_categories.MOVES],
702,
IC.progression,
[
GrinchRamData(0x0100BB, binary_bit_pos=3),
],
),
grinch_items.moves.MAX: GrinchItemData(
[grinch_categories.MOVES],
703,
IC.progression,
[
GrinchRamData(0x0100BB, binary_bit_pos=4),
],
),
grinch_items.moves.SNEAK: GrinchItemData(
[grinch_categories.MOVES],
704,
IC.progression,
[
GrinchRamData(0x0100BB, binary_bit_pos=5),
],
),
}
# Double star combines all dictionaries from each individual list together
ALL_ITEMS_TABLE: dict[str, GrinchItemData] = { ALL_ITEMS_TABLE: dict[str, GrinchItemData] = {
**GADGETS_TABLE, **GADGETS_TABLE,
**MISSION_ITEMS_TABLE, **MISSION_ITEMS_TABLE,
**KEYS_TABLE, **KEYS_TABLE,
**MISC_ITEMS_TABLE, **MISC_ITEMS_TABLE,
**TRAPS_TABLE, **TRAPS_TABLE,
**USEFUL_IC_TABLE, **USEFUL_ITEMS_TABLE,
# **SLEIGH_PARTS_TABLE, # **SLEIGH_PARTS_TABLE,
# **MOVES_TABLE, **MOVES_TABLE,
} }
# Psuedocoding traplink table # Psuedocoding traplink table
@@ -268,8 +601,9 @@ ALL_ITEMS_TABLE: dict[str, GrinchItemData] = {
# ELEC_TRAP_EQUIV = [] # ELEC_TRAP_EQUIV = []
# DEPL_TRAP_EQUIV = ["Dry Trap"] # DEPL_TRAP_EQUIV = ["Dry Trap"]
def grinch_items_to_id() -> dict[str, int]: def grinch_items_to_id() -> dict[str, int]:
item_mappings: dict[str, int] = {} item_mappings: dict[str, int] = {}
for ItemName, ItemData in ALL_ITEMS_TABLE.items(): for ItemName, ItemData in ALL_ITEMS_TABLE.items():
item_mappings.update({ItemName: GrinchItem.get_apid(ItemData.id)}) item_mappings.update({ItemName: GrinchItem.get_apid(ItemData.id)})
return item_mappings return item_mappings

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,26 @@
from dataclasses import dataclass from dataclasses import dataclass
from Options import FreeText, NumericOption, Toggle, DefaultOnToggle, Choice, TextChoice, Range, NamedRange, OptionList, \ from Options import (
PerGameCommonOptions, OptionSet FreeText,
NumericOption,
Toggle,
DefaultOnToggle,
Choice,
TextChoice,
Range,
NamedRange,
OptionList,
PerGameCommonOptions,
OptionSet,
)
class StartingArea(Choice): class StartingArea(Choice):
""" """
Here, you can select which area you'll start the game with. [NOT IMPLEMENTED] Here, you can select which area you'll start the game with. [NOT IMPLEMENTED]
Whichever one you pick is the region you'll have access to at the start of the Multiworld. Whichever one you pick is the region you'll have access to at the start of the Multiworld.
""" """
option_whoville = 0 option_whoville = 0
option_who_forest = 1 option_who_forest = 1
option_who_dump = 2 option_who_dump = 2
@@ -15,14 +28,17 @@ class StartingArea(Choice):
default = 0 default = 0
display_name = "Starting Area" display_name = "Starting Area"
class ProgressiveVacuum(Toggle):#DefaultOnToggle
class ProgressiveVacuum(Toggle): # DefaultOnToggle
""" """
Determines whether you get access to main areas progressively [NOT IMPLEMENTED] Determines whether you get access to main areas progressively [NOT IMPLEMENTED]
Enabled: Whoville > Who Forest > Who Dump > Who Lake Enabled: Whoville > Who Forest > Who Dump > Who Lake
""" """
display_name = "Progressive Vacuum Tubes" display_name = "Progressive Vacuum Tubes"
class Missionsanity(Choice): class Missionsanity(Choice):
""" """
How mission checks are randomized in the pool [NOT IMPLEMENTED] How mission checks are randomized in the pool [NOT IMPLEMENTED]
@@ -32,6 +48,7 @@ class Missionsanity(Choice):
Individual: Individual tasks for one mission, such as individual snowmen squashed, are checks. Individual: Individual tasks for one mission, such as individual snowmen squashed, are checks.
Both: Both individual tasks and mission completion are randomized. Both: Both individual tasks and mission completion are randomized.
""" """
display_name = "Mission Locations" display_name = "Mission Locations"
option_none = 0 option_none = 0
option_completion = 1 option_completion = 1
@@ -39,6 +56,7 @@ class Missionsanity(Choice):
option_both = 3 option_both = 3
default = 1 default = 1
class ExcludeEnvironments(OptionSet): class ExcludeEnvironments(OptionSet):
""" """
Allows entire environments to be an excluded location to ensure you are not logically required to enter the environment along Allows entire environments to be an excluded location to ensure you are not logically required to enter the environment along
@@ -51,74 +69,80 @@ class ExcludeEnvironments(OptionSet):
"Ski Resort", "Civic Center", "Minefield", "Power Plant", "Generator Building", "Scout's Hut", "Ski Resort", "Civic Center", "Minefield", "Power Plant", "Generator Building", "Scout's Hut",
"North Shore", "Mayor's Villa", "Sleigh Ride" "North Shore", "Mayor's Villa", "Sleigh Ride"
""" """
display_name = "Exclude Environments"
valid_keys = {"Whoville", "Who Forest", "Who Dump", "Who Lake", "Post Office", "Clock Tower", "City Hall",
"Ski Resort", "Civic Center", "Minefield", "Power Plant", "Generator Building", "Scout's Hut",
"North Shore", "Mayor's Villa", "Sleigh Ride"}
class ProgressiveGadget(Toggle):#DefaultOnToggle display_name = "Exclude Environments"
valid_keys = {
"Whoville",
"Who Forest",
"Who Dump",
"Who Lake",
"Post Office",
"Clock Tower",
"City Hall",
"Ski Resort",
"Civic Center",
"Minefield",
"Power Plant",
"Generator Building",
"Scout's Hut",
"North Shore",
"Mayor's Villa",
"Sleigh Ride",
}
class ProgressiveGadget(Toggle): # DefaultOnToggle
""" """
Determines whether you get access to a gadget as individual blueprint count. [NOT IMPLEMENTED] Determines whether you get access to a gadget as individual blueprint count. [NOT IMPLEMENTED]
""" """
display_name = "Progressive Gadgets" display_name = "Progressive Gadgets"
class Supadow(Toggle): class Supadow(Toggle):
"""Enables completing minigames through the Supadows in Mount Crumpit as checks. NOT IMPLEMENTED]""" """Enables completing minigames through the Supadows in Mount Crumpit as checks. NOT IMPLEMENTED]"""
display_name = "Supadow Minigames" display_name = "Supadow Minigames"
class Gifts(Range): class Gifts(Range):
""" """
Considers how many gifts must be squashed per check. Considers how many gifts must be squashed per check.
Enabling this will also enable squashing all gifts in a region mission along side this. [NOT IMPLEMENTED] Enabling this will also enable squashing all gifts in a region mission along side this. [NOT IMPLEMENTED]
""" """
display_name = "Gifts Squashed per Check" display_name = "Gifts Squashed per Check"
range_start = 0 range_start = 0
range_end = 300 range_end = 300
default = 0 default = 0
class GadgetRando(OptionSet):
"""
Randomizes Grinch's gadgets along with randomizing Binoculars into the pool. [NOT IMPLEMENTED]
"""
display_name = "Gadgets Randomized"
default = [
"Binoculars",
"Rotten Egg Launcher",
"Rocket Spring",
"Slime Shooter",
"Octopus Climbing Device",
"Marine Mobile",
"Grinch Copter"
]
class Moverando(OptionSet): class Moverando(Toggle):
"""Randomizes Grinch's moveset along with randomizing max into the pool. [NOT IMPLEMENTED] """Randomizes Grinch's moveset along with randomizing max into the pool. [NOT IMPLEMENTED]"""
Valid keys: "Pancake", "Seize", "Max", "Bad Breath", "Sneak"
"""
display_name = "Moves Randomized" display_name = "Moves Randomized"
default = [
"Pancake",
"Seize",
"Max",
"Bad Breath",
"Sneak"
]
class UnlimitedEggs(Toggle): class UnlimitedEggs(Toggle):
"""Determine whether or not you run out of rotten eggs when you utilize your gadgets.""" """Determine whether or not you run out of rotten eggs when you utilize your gadgets."""
display_name = "Unlimited Rotten Eggs" display_name = "Unlimited Rotten Eggs"
class RingLinkOption(Toggle): class RingLinkOption(Toggle):
"""Whenever this is toggled, your ammo is linked with other ringlink-compatible games that also have this enabled.""" """Whenever this is toggled, your ammo is linked with other ringlink-compatible games that also have this enabled."""
display_name = "Ring Link" display_name = "Ring Link"
class TrapLinkOption(Toggle): class TrapLinkOption(Toggle):
"""If a trap is sent from Grinch, traps that are compatible with other games are triggered as well. [NOT IMPLEMENTED]""" """If a trap is sent from Grinch, traps that are compatible with other games are triggered as well. [NOT IMPLEMENTED]"""
display_name = "Trap Link" display_name = "Trap Link"
@dataclass @dataclass
class GrinchOptions(PerGameCommonOptions):#DeathLinkMixin class GrinchOptions(PerGameCommonOptions): # DeathLinkMixin
starting_area: StartingArea starting_area: StartingArea
progressive_vacuum: ProgressiveVacuum progressive_vacuum: ProgressiveVacuum
missionsanity: Missionsanity missionsanity: Missionsanity
@@ -126,7 +150,6 @@ class GrinchOptions(PerGameCommonOptions):#DeathLinkMixin
progressive_gadget: ProgressiveGadget progressive_gadget: ProgressiveGadget
supadow_minigames: Supadow supadow_minigames: Supadow
giftsanity: Gifts giftsanity: Gifts
gadget_rando: GadgetRando
move_rando: Moverando move_rando: Moverando
unlimited_eggs: UnlimitedEggs unlimited_eggs: UnlimitedEggs
ring_link: RingLinkOption ring_link: RingLinkOption

View File

@@ -1,12 +1,66 @@
from enum import STRICT, IntEnum
from typing import NamedTuple, Optional from typing import NamedTuple, Optional
class UpdateMethod(IntEnum, boundary=STRICT):
SET = 1
ADD = 2
SUBTRACT = 3
FREEZE = 4
class GrinchRamData(NamedTuple): class GrinchRamData(NamedTuple):
"""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)
value (int; Optional): The value we are using to set, add, or subtract from the RAM Address Value. Defaults to 1 if binary_bit_pos is passed in
binary_bit_pos: (int; Optional): If passed in, we are looking for a specific bit within the byte of the ram_address. This is represented as a small-endian bit position, meaning the right-most bit is 0, and the left-most bit is 7
byte_size: (int: Default: 1): The size of the RAM Address address we are looking for.
update_method (UpdateMethod; Default: SET): Determines what we are doing to the RAM Address. We can either SET the address, simply assigning a value. We can ADD or SUBTRACT, modifying hte existing value by a set amount. And we can FREEZE the address, preventing it from updating in the future
min_count: The minimum amount that a value can go down to using SUBTRACT
max_count: The maximum amount that a value can go down to using ADD
"""
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 # Either set or add either hex or unsigned values through Client.py
# Hex uses 0x00, unsigned are base whole numbers # Hex uses 0x00, unsigned are base whole numbers
binary_bit_pos: Optional[int] = None binary_bit_pos: Optional[int] = None
bit_size: int = 1 byte_size: int = 1
update_existing_value: bool = False update_method: UpdateMethod = UpdateMethod.SET
max_count: int = 0 min_count: int = 0
max_count: int = 255
def __init__(
self,
ram_address: int,
value: Optional[int],
byte_size: int,
update_method: UpdateMethod,
min_count: int,
max_count: int,
):
self.ram_address = ram_address
if value:
self.value = value
else:
self.value = 1
if byte_size:
self.byte_size = byte_size
if update_method:
self.update_method = update_method
if min_count and min_count > -1:
self.min_count = min_count
if max_count and max_count > -1:
self.max_count = max_count
elif max_count and max_count > ((2 ** (self.byte_size * 8)) - 1):
raise ValueError(
"max_count cannot be larger than the RAM addresses max possible value"
)
else:
self.max_count = (2 ** (self.byte_size * 8)) - 1

View File

@@ -13,7 +13,7 @@ mainareas_list = [
"Whoville", "Whoville",
"Who Forest", "Who Forest",
"Who Dump", "Who Dump",
"Who Lake" "Who Lake",
] ]
subareas_list = [ subareas_list = [
@@ -29,29 +29,35 @@ subareas_list = [
"Scout's Hut", "Scout's Hut",
"North Shore", "North Shore",
"Mayor's Villa", "Mayor's Villa",
"Sleigh Room" "Sleigh Room",
] ]
supadow_list = [ supadow_list = [
"Spin N' Win Supadow", "Spin N' Win Supadow",
"Dankamania Supadow", "Dankamania Supadow",
"The Copter Race Contest Supadow", "The Copter Race Contest Supadow",
"Bike Race" "Bike Race",
] ]
def create_regions(world: "GrinchWorld"): def create_regions(world: "GrinchWorld"):
for mainarea in mainareas_list: for mainarea in mainareas_list:
#Each area in mainarea, create a region for the given player # Each area in mainarea, create a region for the given player
world.multiworld.regions.append(Region(mainarea, world.player, world.multiworld)) world.multiworld.regions.append(
Region(mainarea, world.player, world.multiworld)
)
for subarea in subareas_list: for subarea in subareas_list:
#Each area in subarea, create a region for the given player # Each area in subarea, create a region for the given player
world.multiworld.regions.append(Region(subarea, world.player, world.multiworld)) world.multiworld.regions.append(Region(subarea, world.player, world.multiworld))
for supadow in supadow_list: for supadow in supadow_list:
#Each area in supadow, create a region for the given player # Each area in supadow, create a region for the given player
world.multiworld.regions.append(Region(supadow, world.player, world.multiworld)) world.multiworld.regions.append(Region(supadow, world.player, world.multiworld))
# TODO Optimize this function # TODO Optimize this function
def grinchconnect(world: "GrinchWorld", current_region_name: str, connected_region_name: str): def grinchconnect(
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)
required_items: list[list[str]] = access_rules_dict[connected_region.name] required_items: list[list[str]] = access_rules_dict[connected_region.name]
@@ -62,21 +68,26 @@ def grinchconnect(world: "GrinchWorld", current_region_name: str, connected_regi
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 region_entrance.connected_region.name == current_region_name and \ if (
region_entrance.parent_region.name == connected_region_name: region_entrance.connected_region.name == current_region_name
and region_entrance.parent_region.name == connected_region_name
):
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 region_entrance.connected_region.name == connected_region_name and \ if (
region_entrance.parent_region.name == current_region_name: region_entrance.connected_region.name == connected_region_name
and region_entrance.parent_region.name == current_region_name
):
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")
#What regions are connected to each other
# What regions are connected to each other
def connect_regions(world: "GrinchWorld"): def connect_regions(world: "GrinchWorld"):
grinchconnect(world, "Mount Crumpit", "Whoville") grinchconnect(world, "Mount Crumpit", "Whoville")
grinchconnect(world, "Mount Crumpit", "Who Forest") grinchconnect(world, "Mount Crumpit", "Who Forest")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
{
"minimum_ap_version": "0.6.3",
"world_version": "1.2.3",
"authors": ["MarioSpore"],
"game": "The Grinch"
}

View File

@@ -5,7 +5,8 @@
BizHawk support. BizHawk support.
- Legally obtained NTSC Bin ROM file, probably named something like `Grinch, The (USA) (En,Fr,Es).bin`. - Legally obtained NTSC Bin ROM file, probably named something like `Grinch, The (USA) (En,Fr,Es).bin`.
The game's CUE file should also work aswell along side the BIN file if you have troubles opening the BIN file. The game's CUE file should also work aswell along side the BIN file if you have troubles opening the BIN file.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) Versions 2.9.1 & 2.10 is supported, but I can't promise if any version is stable or not. As of September 2025, the Grinch may not be compatible with 2.11 yet. - [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) Version 2.9.1 is required to play. Any version is NOT compatible and may have unintended behavior in
the game as well as not being able to connect to the client through the LUA Console.
- The latest `grinch.apworld` file. You can find this on the [Releases page](https://github.com/MarioSpore/Grinch-AP/releases/latest). Put this in your `Archipelago/custom_worlds` folder. - The latest `grinch.apworld` file. You can find this on the [Releases page](https://github.com/MarioSpore/Grinch-AP/releases/latest). Put this in your `Archipelago/custom_worlds` folder.
- PSX BIOS Firmware bin file, which is required to run the game through Bizhawk. The file you need should be - PSX BIOS Firmware bin file, which is required to run the game through Bizhawk. The file you need should be
named something like `SCPH-5501.BIN`. named something like `SCPH-5501.BIN`.