103 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
MarioSpore
d3a7b014bd Added gadget_rando & some description adjustments 2025-10-03 16:50:06 -04:00
MarioSpore
3ec8631203 Adjustments for move/gadget rando options 2025-10-03 16:44:34 -04:00
MarioSpore
2081912a39 Fixed Exception error 2025-10-03 16:40:20 -04:00
MarioSpore
67bbde2556 Log for sending locations now retriggers when exiting out of the game 2025-10-02 20:47:32 -04:00
MarioSpore
0503c2ead3 Extra space for newly added initial_cutscene_checker 2025-10-02 20:12:21 -04:00
MarioSpore
7a642cc1a9 Add ram address for additional checking in demo mode checking 2025-10-02 16:27:45 -04:00
MarioSpore
eb8d44e975 Update setup_en.md 2025-10-01 16:44:24 -04:00
MarioSpore
1f35f5fa93 Update setup_en.md 2025-10-01 15:26:16 -04:00
MarioSpore
6bc819f4bc Removed test loggers that was for testing 2025-09-30 01:32:12 -04:00
MarioSpore
bb0c5f5b9a Fixed "WL - South Shore - First Visit" location not generating due to missed space 2025-09-30 01:14:31 -04:00
MarioSpore
0e397c7079 Forgot PC for abbreviation in rules 2025-09-30 00:59:38 -04:00
MarioSpore
2572a25089 Abbreviation of items part 4 w/ rules adjusted with new abbreviations 2025-09-30 00:58:59 -04:00
MarioSpore
dcdf168618 Abbreviation of items part 3 2025-09-30 00:52:38 -04:00
MarioSpore
8a8136a267 Add address for whoville vacuum tube! 2025-09-30 00:50:14 -04:00
MarioSpore
5f158497f9 more inital names added to list 2025-09-29 18:56:02 -04:00
MarioSpore
bc74e67e07 Moverando item renames pt 2 2025-09-29 18:10:18 -04:00
MarioSpore
dbc592dad0 Moverando item renames 2025-09-29 18:05:14 -04:00
MarioSpore
afe1345e34 Added a whole bunch of location groups 2025-09-27 23:09:13 -04:00
MarioSpore
410df2a948 Redone options by various name changes and grammatical changes 2025-09-27 16:11:10 -04:00
MarioSpore
7c6eada7b2 Renamed movesanity to move_rando 2025-09-27 12:15:04 -04:00
MarioSpore
8e1217d1a5 Fixed small issue with location groups & ram watching with goal 2025-09-26 21:40:52 -04:00
MarioSpore
5615277705 Forgot to put Who Dump Vacuum Tube as part of a list for location groups 2025-09-26 18:52:46 -04:00
MarioSpore
2de0f9d766 Crazy I figured this out. Location groups fully works 2025-09-26 18:09:18 -04:00
MarioSpore
4da88cf794 Yeah Jake is right. The ClassVar code is not needed. 2025-09-26 18:05:20 -04:00
MarioSpore
e2cc1b5de7 Possibly implemented location groups? 2025-09-26 17:11:48 -04:00
MarioSpore
103a6f79d1 Maybe fix for compensating for multiple item groups in a list? 2025-09-26 16:52:57 -04:00
MarioSpore
17861c1050 Add abbreviations to various items for ease of typing 2025-09-24 18:56:57 -04:00
MarioSpore
a63c33a711 Added list[str] for location groups for inevitable location group implementation 2025-09-22 23:25:40 -04:00
MarioSpore
c84ef117c8 Removed arbitrary second_item_group variable when we start to implement multiple item groups 2025-09-22 23:24:27 -04:00
MarioSpore
834096f282 Fixed item groups 2025-09-22 23:23:41 -04:00
MarioSpore
0a31d96ee4 Make grinch goal earlier by triggering goal when "Ending (Santa costume)" region is loaded 2025-09-22 21:47:42 -04:00
MarioSpore
3edb733dcb Ringlink toggle command implement 2025-09-22 21:38:06 -04:00
MarioSpore
9c01cc31e0 Found address that handles when grinch is wearing a disguise. Sets address to 0 when "Dump it to Crumpit" is activated and as a failsafe if he is wearing one or not. 2025-09-22 20:36:48 -04:00
MarioSpore
ea8262855e Abbreviated regions & blueprints 2025-09-20 21:41:40 -04:00
MarioSpore
98ea11887e Added Artamiss to credits 2025-09-20 17:21:02 -04:00
MarioSpore
18aefcd3f2 Changed "Access" to "Tube" for planned Progressive vacuum implementation 2025-09-20 15:49:43 -04:00
MarioSpore
3a13332533 Renamed description from "Access" to "Tubes" 2025-09-20 15:48:40 -04:00
MarioSpore
c279ef7bc6 - Major refactoring of location, region, and goal item names 2025-09-20 15:48:24 -04:00
MarioSpore
55ef0cc8c9 Damage is considered "exhaustion" according to the game manual 2025-09-18 22:15:07 -04:00
MarioSpore
babc4f441c Changed "Vacuum Access" to "Vacuum Tube" to reflect the game manual 2025-09-18 22:12:11 -04:00
MarioSpore
92d932da55 Psuedocode generic giftsanity logic 2025-09-14 17:47:03 -04:00
MarioSpore
b1b65a3adf Minor option tweaks 2025-09-14 00:17:32 -04:00
MarioSpore
d6f5e87ccf Option changes based on feedback along with typos and punctuation issues 2025-09-14 00:11:21 -04:00
MarioSpore
079239ea70 Minor tweaks in bios setup 2025-09-13 14:29:03 -04:00
MarioSpore
93bac29e8c Added section for requiring PSX bios and how to set it up 2025-09-13 14:24:55 -04:00
MarioSpore
c9fea29d92 Fixed item groups? 2025-09-13 13:38:08 -04:00
MarioSpore
e17895902e Add unique id to compensate for multiple grinch games in one multiworld 2025-09-13 00:58:12 -04:00
MarioSpore
1596550111 Merge pull request #2 from MarioSpore/devjake
Fixed previous fixes for address optimization.
2025-09-12 23:16:01 -04:00
SomeJakeGuy
c888e17845 Fixed previous fixes for address optimization. 2025-09-12 23:10:19 -04:00
MarioSpore
3dee611b51 Fixed double receive eggs to self 2025-09-12 22:22:54 -04:00
MarioSpore
d6c7a04316 Fixed negative int bug 2025-09-12 21:27:11 -04:00
MarioSpore
900c8a519a Fixed SADX/SA2B crashes? 2025-09-12 05:44:49 -04:00
MarioSpore
43acc9f003 Minefield logical access oversight 2025-09-11 19:35:42 -04:00
MarioSpore
96eb8fcd9a Fix ringlink output 2025-09-11 00:11:25 -04:00
MarioSpore
d4bd682ac9 Fix location send speed code 2025-09-10 23:19:36 -04:00
MarioSpore
61d4783f61 Minor setup doc tweak 2025-09-10 18:49:31 -04:00
MarioSpore
00fff466ff Updated setup docs that actually make more sense 2025-09-10 18:36:38 -04:00
MarioSpore
d8483bef6e Fixes client crash if the emulator is paused with ringlink enabled. Still won't be able to send out ringlink when this occurs 2025-09-09 23:09:56 -04:00
MarioSpore
56a198fcfd Vastly improved speed of remove_physical_items, constant_address_update, & receiving_items_handler 2025-09-09 22:06:07 -04:00
MarioSpore
4e362dc722 Fixed output for ring link to be in a loop 2025-09-09 21:01:56 -04:00
MarioSpore
cfcfc9ecfd Fixed ring link input 2025-09-09 17:44:04 -04:00
MarioSpore
a3a415adfd Merge pull request #1 from MarioSpore/jake_ring-link-fix
Fix some small issues with async on_package and changing things to be async starts instead
2025-09-09 05:46:14 -04:00
SomeJakeGuy
14c95aa85b Fix some small issues with async on_package and changing things to be async starts instead. 2025-09-09 01:29:32 -04:00
MarioSpore
8d941dad6f Adjust import of options to fix AttributeError for previous commit 2025-09-09 00:23:36 -04:00
MarioSpore
8628f6637a Fully implement ringlink 2025-09-09 00:04:09 -04:00
MarioSpore
17b7914c35 Now passing ring link option value to client 2025-09-08 22:48:43 -04:00
MarioSpore
b8dfd5ce4c Adds mount crumpit crate locations 2025-09-08 22:41:22 -04:00
MarioSpore
b3749b7fe3 Somehow, OR conditional logic was STILL not being considered. This should fix it. 2025-09-07 12:56:26 -04:00
MarioSpore
3aaf625282 Comment out mount crumpit checks until v1.1 2025-09-06 21:54:53 -04:00
MarioSpore
9df2360b8b Adds no jump trap 2025-09-06 21:03:08 -04:00
MarioSpore
d61ac9a135 Implement crate tutorial checks and logic 2025-09-06 21:02:54 -04:00
MarioSpore
c8fc56d7c4 No longer requires REL if you have GC for the guardian house right side location 2025-09-06 20:10:52 -04:00
MarioSpore
51aad167cc Update ap connection detection to only after the slot name is entered and you fully connect 2025-09-06 20:05:36 -04:00
MarioSpore
e2def66522 Added comment explaining recently added except block 2025-09-06 19:43:57 -04:00
MarioSpore
73e9d9d577 Added 2nd exception if theres other error types while playing the game 2025-09-06 19:37:46 -04:00
MarioSpore
a5d7ff65c1 Update constant address update to always give or take away mission specific items/keys 2025-09-06 19:22:34 -04:00
MarioSpore
05bf60abf7 Part 2 of fixing test_default_all_state_can_reach_everything to comply with banadium 2025-09-06 17:26:38 -04:00
MarioSpore
7f627e2c07 Remove duplication of "Supadow" option 2025-09-06 17:00:59 -04:00
MarioSpore
19e0fe1286 Temporairly disable supadow regions to comply with banadium to test_default_all_state_can_reach_everything 2025-09-06 16:59:00 -04:00
MarioSpore
b390974019 Fixes
"AssertionError: True is not false : Unexpected assignment to GrinchWorld.options!"
2025-09-06 16:50:38 -04:00
MarioSpore
9da65fab09 psuedocode more traplink 2025-09-05 20:18:33 -04:00
MarioSpore
02d2eab5a4 psuedocode more traplink 2025-09-05 20:18:20 -04:00
MarioSpore
985c8b681b ring link psuedocode part 3 2025-09-05 00:09:25 -04:00
MarioSpore
cf5a4012c0 ring link psuedocode part 2 2025-09-05 00:02:01 -04:00
MarioSpore
c59e75ef7b Ring link psuedocode, thanks for graymondgt for getting this started 2025-09-04 23:48:53 -04:00
MarioSpore
2dbe344348 More trap link psuedo code 2025-09-04 22:51:14 -04:00
MarioSpore
90ba4fbda7 Minor hotfix that reads the correct bit_size for Squashing All Gifts missions 2025-09-01 19:10:04 -04:00
MarioSpore
8ff2fb91d4 Minor fix to prevent trigger from occuring too early 2025-09-01 15:49:09 -04:00
11 changed files with 3302 additions and 1012 deletions

View File

@@ -1,9 +1,18 @@
from typing import TYPE_CHECKING
import time
from typing import TYPE_CHECKING, Sequence
import asyncio
import NetUtils
import copy
import uuid
import Utils
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
from worlds._bizhawk.client import BizHawkClient
@@ -23,75 +32,146 @@ MAX_DEMO_MODE_CHECK = 30
# List of Menu Map IDs
MENU_MAP_IDS: list[int] = [0x00, 0x02, 0x35, 0x36, 0x37]
MAX_EGGS: int = 200
EGG_COUNT_ADDR: int = 0x010058
EGG_ADDR_BYTESIZE: int = 2
class GrinchClient(BizHawkClient):
game = "The Grinch"
system = "PSX"
patch_suffix = ".apgrinch"
items_handling = 0b111
demo_mode_buffer = 0
last_map_location = -1
ingame_log = False
demo_mode_buffer: int = 0
last_map_location: int = -1
ingame_log: bool = False
previous_egg_count: int = 0
send_ring_link: bool = False
unique_client_id: int = 0
ring_link_enabled = False
def __init__(self):
super().__init__()
self.last_received_index = 0
self.loading_bios_msg = False
self.loc_unlimited_eggs = False
self.unique_client_id = 0
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
# TODO Check the ROM data to see if it matches against bytes expected
grinch_identifier_ram_address: int = 0x00928C
bios_identifier_ram_address: int = 0x097F30
try:
bytes_actual: bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(
grinch_identifier_ram_address, 11, "MainRAM")]))[0]
bytes_actual: bytes = (
await bizhawk.read(
ctx.bizhawk_ctx, [(grinch_identifier_ram_address, 11, "MainRAM")]
)
)[0]
psx_rom_name = bytes_actual.decode("ascii")
if psx_rom_name != "SLUS_011.97":
bios_bytes_check: bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(
bios_identifier_ram_address, 24, "MainRAM")]))[0]
bios_bytes_check: bytes = (
await bizhawk.read(
ctx.bizhawk_ctx, [(bios_identifier_ram_address, 24, "MainRAM")]
)
)[0]
if "System ROM Version" in bios_bytes_check.decode("ascii"):
if not self.loading_bios_msg:
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
logger.error("Invalid rom detected. You are not playing Grinch USA Version.")
raise Exception("Invalid rom detected. You are not playing Grinch USA Version.")
logger.error(
"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
except Exception:
return False
ctx.game = self.game
ctx.items_handling = self.items_handling
ctx.want_slot_data = True
ctx.watcher_timeout = 0.25
ctx.watcher_timeout = 0.125
self.loading_bios_msg = False
return True
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
from CommonClient import logger
super().on_package(ctx, cmd, args)
match cmd:
case "Connected": # On Connect
self.loc_unlimited_eggs = bool(ctx.slot_data["give_unlimited_eggs"])
logger.info("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.")
self.unique_client_id = self._get_uuid()
logger.info(
"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."
)
self.ring_link_enabled = bool(ctx.slot_data["ring_link"])
tags = copy.deepcopy(ctx.tags)
if self.ring_link_enabled:
ctx.tags.add("RingLink")
else:
ctx.tags -= {"RingLink"}
if tags != ctx.tags:
Utils.async_start(
ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}]),
"Update RingLink Tags",
)
case "Bounced":
if "tags" not in args:
return
if (
"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:
await ctx.get_username()
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from CommonClient import logger
#If the player is not connected to an AP Server, or their connection was disconnected.
if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None:
from CommonClient import logger
# If the player is not connected to an AP Server, or their connection was disconnected.
if not ctx.slot:
return
try:
if not await self.ingame_checker(ctx):
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.receiving_items_handler(ctx)
await self.goal_checker(ctx)
@@ -100,16 +180,46 @@ class GrinchClient(BizHawkClient):
except bizhawk.RequestFailedError as ex:
# 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
except Exception as genericEx:
# 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)
)
await ctx.disconnect(False)
pass
async def location_checker(self, ctx: "BizHawkClientContext"):
from CommonClient import logger
# Update the AP Server to know what locations are not checked yet.
local_locations_checked: list[int] = []
addr_list_to_read: list[tuple[int, int, str]] = []
local_ap_locations: set[int] = copy.deepcopy(ctx.missing_locations)
# Loop through the first time of everything left to create the list of RAM addresses to read / monitor.
for missing_location in local_ap_locations:
grinch_loc_name = ctx.location_names.lookup_in_game(missing_location)
grinch_loc_ram_data = grinch_locations[grinch_loc_name]
missing_addr_list: list[tuple[int, int, str]] = [
(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]
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,
# and check to see if that ram address has our expected value.
for missing_location in local_ap_locations:
# local_location = ctx.location_names.lookup_in_game(missing_location)
# Missing location is the AP ID & we need to convert it back to a location name within our game.
# Using the location name, we can then get the Grinch ram data from there.
grinch_loc_name = ctx.location_names.lookup_in_game(missing_location)
@@ -118,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
# 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] = []
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
current_ram_address_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little")
orig_index: int = addr_list_to_read.index(
(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:
ram_checked_list.append((current_ram_address_value & (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:
expected_int_value = addr_to_update.value
ram_checked_list.append(expected_int_value == current_ram_address_value)
if all(ram_checked_list):
local_locations_checked.append(GrinchLocation.get_apid(grinch_loc_ram_data.id))
ram_checked_list.append(
expected_int_value == value_read_from_bizhawk
)
if all(ram_checked_list):
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)
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:
await self.remove_physical_items(ctx)
ctx.locations_checked = set(local_locations_checked)
async def receiving_items_handler(self, ctx: "BizHawkClientContext"):
@@ -141,13 +270,21 @@ class GrinchClient(BizHawkClient):
# 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.
self.last_received_index = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
RECV_ITEM_ADDR, RECV_ITEM_BITSIZE, "MainRAM")]))[0], "little")
self.last_received_index = int.from_bytes(
(
await bizhawk.read(
ctx.bizhawk_ctx, [(RECV_ITEM_ADDR, RECV_ITEM_BITSIZE, "MainRAM")]
)
)[0],
"little",
)
if len(ctx.items_received) == self.last_received_index:
return
# 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]] = {}
for item_received in new_items_only:
local_item = ctx.item_names.lookup_in_game(item_received.item)
@@ -155,103 +292,260 @@ class GrinchClient(BizHawkClient):
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
current_ram_address_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little")
if addr_to_update.ram_address in ram_addr_dict.keys():
current_ram_address_value = ram_addr_dict[
addr_to_update.ram_address
][0]
else:
current_ram_address_value = int.from_bytes(
(
await bizhawk.read(
ctx.bizhawk_ctx,
[
(
addr_to_update.ram_address,
addr_to_update.byte_size,
"MainRAM",
)
],
)
)[0],
"little",
)
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:
# 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 = 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:
current_ram_address_value = addr_to_update.value
# Write the updated value back into RAM
await self.update_and_validate_address(ctx, addr_to_update.ram_address, current_ram_address_value, addr_to_update.bit_size)
# await bizhawk.write(ctx.bizhawk_ctx, [(addr_to_update.ram_address,
# current_ram_address_value.to_bytes(addr_to_update.bit_size, "little"), "MainRAM")])
ram_addr_dict[addr_to_update.ram_address] = [
current_ram_address_value,
addr_to_update.byte_size,
]
self.last_received_index += 1
await self.update_and_validate_address(ctx, RECV_ITEM_ADDR, self.last_received_index, RECV_ITEM_BITSIZE)
# Update the latest received item index to ram as well.
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)
)
async def goal_checker(self, ctx: "BizHawkClientContext"):
if not ctx.finished_game:
goal_loc = grinch_locations["Neutralizing Santa"]
goal_loc = grinch_locations["MC - Sleigh Ride - Neutralizing Santa"]
goal_ram_address = goal_loc.update_ram_addr[0]
current_ram_address_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
goal_ram_address.ram_address, goal_ram_address.bit_size, "MainRAM")]))[0], "little")
current_ram_address_value = int.from_bytes(
(
await bizhawk.read(
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
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": NetUtils.ClientStatus.CLIENT_GOAL,
}])
await ctx.send_msgs(
[
{
"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
async def remove_physical_items(self, ctx: "BizHawkClientContext"):
ram_addr_dict: dict[int, list[int]] = {}
list_recv_itemids: list[int] = [netItem.item for netItem in ctx.items_received]
items_to_check: dict[str, GrinchItemData] = {**GADGETS_TABLE} #, **SLEIGH_PARTS_TABLE
heart_count = len(list(item_id for item_id in list_recv_itemids if item_id == 42570))
items_to_check: dict[str, GrinchItemData] = {
**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"]
await self.update_and_validate_address(ctx, 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 Who Lake Mission Count back to 0 to prevent warping after completing 3 missions
await self.update_and_validate_address(ctx,0x0100F0, 0, 4)
# Setting mission count for all accesses back to 0 to prevent warping/unlocking after completing 3 missions
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_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
# 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:
is_binary = True if not addr_to_update.binary_bit_pos is None else False
if is_binary:
current_bin_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little")
if addr_to_update.ram_address in ram_addr_dict.keys():
current_bin_value = ram_addr_dict[addr_to_update.ram_address][0]
else:
current_bin_value = int.from_bytes(
(
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)
await self.update_and_validate_address(ctx, addr_to_update.ram_address, current_bin_value, 1)
ram_addr_dict[addr_to_update.ram_address] = [current_bin_value, 1]
else:
await self.update_and_validate_address(ctx, addr_to_update.ram_address, 0, 1)
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)
)
def convert_dict_to_ram_list(
self, addr_dict: dict[int, list[int]]
) -> list[tuple[int, Sequence[int], str]]:
addr_list_to_update: list[tuple[int, Sequence[int], str]] = []
for key, val in addr_dict.items():
addr_list_to_update.append(
(key, val[0].to_bytes(val[1], "little"), "MainRAM")
)
return addr_list_to_update
# Removes the regional access until you actually received it from AP.
async def constant_address_update(self, ctx: "BizHawkClientContext"):
list_recv_itemids: list[int] = [netItem.item for netItem in ctx.items_received]
items_to_check: dict[str, GrinchItemData] = {**KEYS_TABLE, **MISSION_ITEMS_TABLE}
ram_addr_dict: dict[int, list[int]] = {}
for (item_name, item_data) in items_to_check.items():
list_recv_itemids: list[int] = [netItem.item for netItem in ctx.items_received]
items_to_check: dict[str, GrinchItemData] = {
**KEYS_TABLE,
**MISSION_ITEMS_TABLE,
}
for item_name, item_data in items_to_check.items():
# 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
# This assumes we don't have the item so we must set all the data to 0
# 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:
is_binary = True if not addr_to_update.binary_bit_pos is None else False
if is_binary:
current_bin_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
addr_to_update.ram_address, addr_to_update.bit_size, "MainRAM")]))[0], "little")
current_bin_value &= ~(1 << addr_to_update.binary_bit_pos)
await self.update_and_validate_address(ctx, addr_to_update.ram_address, current_bin_value, 1)
if addr_to_update.ram_address in ram_addr_dict.keys():
current_bin_value = ram_addr_dict[addr_to_update.ram_address][0]
else:
current_bin_value = int.from_bytes(
(
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:
current_bin_value |= 1 << addr_to_update.binary_bit_pos
else:
current_bin_value &= ~(1 << addr_to_update.binary_bit_pos)
ram_addr_dict[addr_to_update.ram_address] = [current_bin_value, 1]
else:
await self.update_and_validate_address(ctx, addr_to_update.ram_address, 0, 1)
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.byte_size,
]
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"):
from CommonClient import logger
ingame_map_id = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
0x010000, 1, "MainRAM")]))[0], "little")
ingame_map_id = int.from_bytes(
(await bizhawk.read(ctx.bizhawk_ctx, [(0x010000, 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:
self.ingame_log = False
return False
#If grinch has changed maps
# If grinch has changed maps
if not ingame_map_id == self.last_map_location:
# If the last "map" we were on was a menu or a publisher logo
if self.last_map_location in MENU_MAP_IDS:
# Reset our demo mode checker just in case the game is in demo mode.
self.demo_mode_buffer = 0
self.ingame_log = False
if initial_cutscene_checker != 1:
return False
# Update the previous map we were on to be the current map.
@@ -263,25 +557,129 @@ class GrinchClient(BizHawkClient):
self.demo_mode_buffer += 1
return False
demo_mode = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(
0x01008A, 1, "MainRAM")]))[0], "little")
demo_mode = int.from_bytes(
(await bizhawk.read(ctx.bizhawk_ctx, [(0x01008A, 1, "MainRAM")]))[0],
"little",
)
if demo_mode == 1:
return False
if not self.ingame_log:
logger.info("You can now start sending locations from the Grinch!")
self.ingame_log = True
return True
async def option_handler(self, ctx: "BizHawkClientContext"):
if self.loc_unlimited_eggs:
max_eggs: int = 200
await bizhawk.write(ctx.bizhawk_ctx, [(0x010058, 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 update_and_validate_address(self, ctx: "BizHawkClientContext", address_to_validate: int, expected_value: int, byte_size: int):
await bizhawk.write(ctx.bizhawk_ctx, [(address_to_validate, expected_value.to_bytes(byte_size, "little"), "MainRAM")])
current_value = int.from_bytes((await bizhawk.read(ctx.bizhawk_ctx, [(address_to_validate, byte_size, "MainRAM")]))[0], "little")
if not current_value == expected_value:
if address_to_validate == 0x010000 or address_to_validate == 0x08FB94: # TODO Temporairly skips teleportation addresses; to be changed later on.
return
raise Exception("Unable to update address as expected. Address: "+ str(address_to_validate)+"; Expected Value: "+str(expected_value))
async def ring_link_output(self, ctx: "BizHawkClientContext"):
from CommonClient import logger
while self.send_ring_link and ctx.slot:
try:
current_egg_count = int.from_bytes(
(
await bizhawk.read(
ctx.bizhawk_ctx,
[(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")],
)
)[0],
"little",
)
if (current_egg_count - self.previous_egg_count) != 0:
msg = {
"cmd": "Bounce",
"data": {
"time": time.time(),
"source": self.unique_client_id,
"amount": current_egg_count - self.previous_egg_count,
},
"tags": ["RingLink"],
}
await ctx.send_msgs([msg])
self.previous_egg_count = current_egg_count
# logger.info(f"RingLink: You sent {str(current_egg_count - self.previous_egg_count)} rotten eggs.")
await asyncio.sleep(0.1)
except Exception as ex:
logger.error(
"While monitoring grinch's egg count ingame, an error occurred. Details:"
+ str(ex)
)
self.send_ring_link = False
if not ctx.slot:
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"):
from CommonClient import logger
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
)
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",
)
],
)
self.previous_egg_count = current_egg_count
# logger.info(f"RingLink: You received {str(egg_amount)} rotten eggs.")
def _get_uuid(self) -> int:
string_id = str(uuid.uuid4())
uid: int = 0
for char in string_id:
uid += ord(char)
return uid
def _cmd_ringlink(self):
"""Toggle ringling from client. Overrides default setting."""
if not self.ctx.slot:
return
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):
"""Helper function to set Ring Link connection tag on/off and update the connection if already connected."""
old_tags = copy.deepcopy(ctx.tags)
if ring_link:
ctx.tags.add("RingLink")
else:
ctx.tags -= {"RingLink"}
if old_tags != ctx.tags and ctx.server and not ctx.server.socket.closed:
await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}])

View File

@@ -1,242 +1,609 @@
from typing import NamedTuple, Optional
from .RamHandler import GrinchRamData
from .RamHandler import GrinchRamData, UpdateMethod
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):
item_group: 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]
classification: IC
update_ram_addr: list[GrinchRamData]
second_item_group: Optional[str] = None
class GrinchItem(Item):
game: str = "The Grinch"
#Tells server what item id it is
# Tells server what item id it is
@staticmethod
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
return base_id + id if id is not None else None
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.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]]:
categories: dict[str, set[str]] = {}
for name, data in ALL_ITEMS_TABLE.items():
categories.setdefault(data.item_group, set()).add(name)
for group in data.item_group: # iterate over each category
categories.setdefault(group, set()).add(name)
return categories
#Gadgets
#All gadgets require at least 4 different blueprints to be unlocked in the computer in Mount Crumpit.
class grinch_items:
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] = {
"Binoculars": GrinchItemData("Gadgets", 100, IC.useful,
[GrinchRamData(0x0102B6, value=0x40), GrinchRamData(0x0102B7, value=0x41),
GrinchRamData(0x0102B8, value=0x44), GrinchRamData(0x0102B9, value=0x45),
# GrinchRamData(0x0100BC, binary_bit_pos=0)
]),
"Rotten Egg Launcher": GrinchItemData("Gadgets", 101, IC.progression,
[GrinchRamData(0x0102BA, value=0x40), GrinchRamData(0x0102BB, value=0x41),
GrinchRamData(0x0102BC, value=0x44), GrinchRamData(0x0102BD, value=0x45),
# GrinchRamData(0x0100BC, binary_bit_pos=1)
]),
"Rocket Spring": GrinchItemData("Gadgets", 102, IC.progression,
[GrinchRamData(0x0102BE, value=0x40), GrinchRamData(0x0102BF, value=0x41),
GrinchRamData(0x0102C0, value=0x42), GrinchRamData(0x0102C1, value=0x44),
GrinchRamData(0x0102C2, value=0x45), GrinchRamData(0x0102C3, value=0x46),
GrinchRamData(0x0102C4, value=0x48), GrinchRamData(0x0102C5, value=0x49),
GrinchRamData(0x0102C6, value=0x4A),
# GrinchRamData(0x0100BC, binary_bit_pos=2)
]),
"Slime Shooter": GrinchItemData("Gadgets", 103, IC.progression,
[GrinchRamData(0x0102C7, value=0x40), GrinchRamData(0x0102C8, value=0x41),
GrinchRamData(0x0102C9, value=0x42), GrinchRamData(0x0102CA, value=0x44),
GrinchRamData(0x0102CB, value=0x45), GrinchRamData(0x0102CC, value=0x46),
GrinchRamData(0x0102CD, value=0x48), GrinchRamData(0x0102CE, value=0x49),
GrinchRamData(0x0102CF, value=0x4A),
# GrinchRamData(0x0100BC, binary_bit_pos=3)
]),
"Octopus Climbing Device": GrinchItemData("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)
]),
"Marine Mobile": GrinchItemData("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 Copter": GrinchItemData("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)
])
grinch_items.gadgets.BINOCULARS: GrinchItemData(
[grinch_categories.GADGETS],
100,
IC.useful,
[
GrinchRamData(0x0102B6, value=0x40),
GrinchRamData(0x0102B7, value=0x41),
GrinchRamData(0x0102B8, value=0x44),
GrinchRamData(0x0102B9, value=0x45),
# GrinchRamData(0x0100BC, binary_bit_pos=0)
],
),
grinch_items.gadgets.ROCKET_EGG_LAUNCHER: GrinchItemData(
[grinch_categories.GADGETS],
101,
IC.progression,
[
GrinchRamData(0x0102BA, value=0x40),
GrinchRamData(0x0102BB, value=0x41),
GrinchRamData(0x0102BC, value=0x44),
GrinchRamData(0x0102BD, value=0x45),
# GrinchRamData(0x0100BC, binary_bit_pos=1)
],
),
grinch_items.gadgets.ROCKET_SPRING: GrinchItemData(
[grinch_categories.GADGETS],
102,
IC.progression,
[
GrinchRamData(0x0102BE, value=0x40),
GrinchRamData(0x0102BF, value=0x41),
GrinchRamData(0x0102C0, value=0x42),
GrinchRamData(0x0102C1, value=0x44),
GrinchRamData(0x0102C2, value=0x45),
GrinchRamData(0x0102C3, value=0x46),
GrinchRamData(0x0102C4, value=0x48),
GrinchRamData(0x0102C5, value=0x49),
GrinchRamData(0x0102C6, value=0x4A),
# GrinchRamData(0x0100BC, binary_bit_pos=2)
],
),
grinch_items.gadgets.SLIME_SHOOTER: GrinchItemData(
[
grinch_categories.GADGETS,
"Slime Gun", # For canon --MarioSpore
],
103,
IC.progression,
[
GrinchRamData(0x0102C7, value=0x40),
GrinchRamData(0x0102C8, value=0x41),
GrinchRamData(0x0102C9, value=0x42),
GrinchRamData(0x0102CA, value=0x44),
GrinchRamData(0x0102CB, value=0x45),
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] = {
"Who Cloak": GrinchItemData("Mission Specific Items", 200, IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=0)], second_item_group="Useful Items"),
"Painting Bucket": GrinchItemData("Mission Specific Items", 201, IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=1)], second_item_group="Useful Items"),
"Scissors": GrinchItemData("Mission Specific Items", 202, IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=6), GrinchRamData(0x0100C2, binary_bit_pos=1)],
second_item_group="Useful Items"),
"Glue Bucket": GrinchItemData("Mission Specific Items", 203, IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=4)], second_item_group="Useful Items"),
"Cable Car Access Card": GrinchItemData("Mission Specific Items", 204, IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=5)], second_item_group="Useful Items"),
"Drill": GrinchItemData("Mission Specific Items", 205, IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=2)], second_item_group="Useful Items"),
"Rope": GrinchItemData("Mission Specific Items", 206, IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=1)], second_item_group="Useful Items"),
"Hook": GrinchItemData("Mission Specific Items", 207, IC.progression_deprioritized,
[GrinchRamData(0x0101FA, binary_bit_pos=0)], second_item_group="Useful Items"),
"Sculpting Tools": GrinchItemData("Mission Specific Items", 208, IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=2)], second_item_group="Useful Items"),
"Hammer": GrinchItemData("Mission Specific Items", 209, IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=3)], second_item_group="Useful Items"),
"Scout Clothes": GrinchItemData("Mission Specific Items", 210, IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=7)], second_item_group="Useful Items")
grinch_items.level_items.WV_WHO_CLOAK: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
200,
IC.progression,
[GrinchRamData(0x0101F9, binary_bit_pos=0)],
),
grinch_items.level_items.WV_PAINT_BUCKET: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
grinch_categories.USEFUL_ITEMS,
],
201,
IC.progression_deprioritized,
[GrinchRamData(0x0101F9, binary_bit_pos=1)],
),
grinch_items.level_items.WD_SCISSORS: GrinchItemData(
[
grinch_categories.MISSION_SPECIFIC_ITEMS,
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] = {
# "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)]),
# "GPS": GrinchItemData("Sleigh Parts", 301, IC.useful,
# "GPS": GrinchItemData(["Sleigh Parts"], 301, IC.useful,
# [GrinchRamData(0x0101FB, binary_bit_pos=5)]),
# "Tires": GrinchItemData("Sleigh Parts", 302, IC.progression_skip_balancing,
# "Tires": GrinchItemData(["Sleigh Parts"], 302, IC.progression_skip_balancing,
# [GrinchRamData(0x0101FB, binary_bit_pos=4)]),
# "Skis": GrinchItemData("Sleigh Parts", 303, IC.progression_skip_balancing,
# "Skis": GrinchItemData(["Sleigh Parts"], 303, IC.progression_skip_balancing,
# [GrinchRamData(0x0101FB, binary_bit_pos=3)]),
# "Twin-End Tuba": GrinchItemData("Sleigh Parts", 304, IC.progression_skip_balancing,
# "Twin-End Tuba": GrinchItemData(["Sleigh Parts"], 304, IC.progression_skip_balancing,
# [GrinchRamData(0x0101FB, binary_bit_pos=6)])
# }
#Access Keys
# Access Keys
KEYS_TABLE: dict[str, GrinchItemData] = {
# "Whoville Vacuum Access": GrinchItemData("Vacuum Access", 400, IC.progression,
grinch_items.keys.WHOVILLE: GrinchItemData(
[grinch_categories.VACUUM_TUBES],
400,
IC.progression,
[GrinchRamData(0x010200, binary_bit_pos=1)],
),
grinch_items.keys.WHO_FOREST: GrinchItemData(
[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,
# [GrinchRamData()]),
"Who Forest Vacuum Access": GrinchItemData("Vacuum Access", 401, IC.progression,
[GrinchRamData(0x0100AA, binary_bit_pos=2)]),
"Who Dump Vacuum Access": GrinchItemData("Vacuum Access", 402, IC.progression,
[GrinchRamData(0x0100AA, binary_bit_pos=3)]),
"Who Lake Vacuum Access": GrinchItemData("Vacuum Access", 403, IC.progression,
[GrinchRamData(0x0100AA, binary_bit_pos=4)]),
# "Progressive Vacuum Access": GrinchItemData("Vacuum Access", 404, IC.progression,
# "Spin N' Win Door Unlock": GrinchItemData(["Supadow Door Unlocks"], 405, IC.progression,
# [GrinchRamData()]),
# "Spin N' Win Door Unlock": GrinchItemData("Supadow Door Unlocks", 405, IC.progression,
# [GrinchRamData()]),
# "Dankamania Door Unlock": GrinchItemData("Supadow Door Unlocks", 406, IC.progression,
# "Dankamania Door Unlock": GrinchItemData(["Supadow Door Unlocks"], 406, IC.progression,
# [GrinchRamData()]),
# "The Copter Race Contest Door Unlock": GrinchItemData("Supadow Door Unlocks", 407, IC.progression,
# [GrinchRamData()]),
# "Progressive Supadow Door Unlock": GrinchItemData("Supadow Door Unlocks", 408, IC.progression,
# [GrinchRamData()]),
# "Bike Race Access": GrinchItemData("Supadow Door Unlocks", 409, IC.progression,
# "Bike Race Access": GrinchItemData(["Supadow Door Unlocks", 409, IC.progression,
# [GrinchRamData()])
"Sleigh Room Key": GrinchItemData("Sleigh Room", 410, IC.progression,
[GrinchRamData(0x010200, binary_bit_pos=6), GrinchRamData(0x0100AA, binary_bit_pos=5)])
grinch_items.keys.SLEIGH_ROOM_KEY: GrinchItemData(
[
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] = {
# This item may not function properly if you receive it during a loading screen or in Mount Crumpit
# "Fully Healed Grinch": GrinchItemData("Health Items", 500, IC.filler,
# "Fully Healed Grinch": GrinchItemData(["Health Items", "Filler"], 500, IC.filler,
# [GrinchRamData(0x0E8FDC, value=120)]),
"5 Rotten Eggs": GrinchItemData("Rotten Egg Bundles", 502, IC.filler,
[GrinchRamData(0x010058, value=5, update_existing_value=True, max_count=200, bit_size=2)]),
"10 Rotten Eggs": GrinchItemData("Rotten Egg Bundles", 503, IC.filler,
[GrinchRamData(0x010058, value=10, update_existing_value=True, max_count=200, bit_size=2)]),
"20 Rotten Eggs": GrinchItemData("Rotten Egg Bundles", 504, IC.filler,
[GrinchRamData(0x010058, value=20, update_existing_value=True, max_count=200, bit_size=2)])
"5 Rotten Eggs": GrinchItemData(
[
grinch_categories.ROTTEN_EGG_BUNDLES,
grinch_categories.FILLER,
],
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] = {
"Heart of Stone": GrinchItemData("Health Items", 501, IC.useful,
[GrinchRamData(0x0100ED, value=1, update_existing_value=True, max_count=4)])
USEFUL_ITEMS_TABLE: dict[str, GrinchItemData] = {
grinch_items.useful_items.HEART_OF_STONE: GrinchItemData(
[
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] = {
# alias to Ice Trap for traplink
# "Freeze Trap": GrinchItemData("Traps", 600, IC.trap, [GrinchRamData()]),
# "Bee Trap": GrinchItemData("Traps", 601, IC.trap, [GrinchRamData()]),
# "Electrocution Trap": GrinchItemData("Traps", 602, IC.trap, [GrinchRamData()]),
# alias to Slowness Trap for traplink
# "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
# "Damage Trap": GrinchItemData("Traps", 604, IC.trap, [GrinchRamData(0x0E8FDC, value=-20, update_existing_value=True)]),
"Depletion Trap": GrinchItemData("Traps", 605, IC.trap, [GrinchRamData(0x010058, value=0, bit_size=2)]),
"Dump it to Crumpit": GrinchItemData("Traps", 606, IC.trap, #Alias to Home Trap for traplink
[GrinchRamData(0x010000, value=0x05), GrinchRamData(0x08FB94, value=1)]),
#alias to Spring Trap for traplink
# "Rocket Spring Trap": GrinchItemData("Traps", 607, IC.trap, [GrinchRamData()]),
#alias to Home Trap for traplink
"Who sent me back?": GrinchItemData("Traps", 608, IC.trap, [GrinchRamData(0x08FB94, value=1)]),
# "Cutscene Trap": GrinchItemData("Traps", 609, IC.trap, [GrinchRamData()]),
# "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)])
# "Child Trap": GrinchItemData("Traps", 612, IC.trap,[GrinchRamData()])
# alias to Ice Trap for traplink
# "Freeze Trap": GrinchItemData(["Traps"], 600, IC.trap, [GrinchRamData()]),
# "Bee Trap": GrinchItemData(["Traps"], 601, IC.trap, [GrinchRamData()]),
# "Electrocution Trap": GrinchItemData(["Traps"], 602, IC.trap, [GrinchRamData()]),
# alias to Slowness Trap for traplink
# "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
# alias to Exhaustion Trap
# "Damage Trap": GrinchItemData(["Traps"], 604, IC.trap, [GrinchRamData(0x0E8FDC, value=-20, update_method=UpdateMethod.ADD)]),
grinch_items.trap_items.DEPLETION_TRAP: GrinchItemData(
[grinch_categories.TRAPS],
605,
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()]),
# alias to Home Trap for traplink
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()]),
# "No Vac Trap": GrinchItemData(["Traps"], 610, IC.trap, [GrinchRamData(0x0102DA, value=0]),
# "Invisible Trap": GrinchItemData(["Traps"], 611, IC.trap, [GrinchRamData(0x0102DA, value=0, byte_size=4)])
# "Child Trap": GrinchItemData(["Traps"], 612, IC.trap,[GrinchRamData()])
# "Disable Jump Trap": GrinchItemData(["Traps"], 613, IC.trap,[GrinchRamData(0x010026, binary_bit_pos=6)])
}
#Movesets
# MOVES_TABLE: dict[str, GrinchItemData] = {
# "Bad Breath": GrinchItemData("Movesets", 700, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=1)]),
# "Pancake": GrinchItemData("Movesets", 701, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=2)]),
# "Push & Pull": GrinchItemData("Movesets", 702, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=3)]),
# "Max": GrinchItemData("Movesets", 703, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=4)]),
# "Tip Toe": GrinchItemData("Movesets", 704, IC.progression, [GrinchRamData(0x0100BB, binary_bit_pos=5)])
# }
#Double star combines all dictionaries from each individual list together
# Movesets
MOVES_TABLE: dict[str, GrinchItemData] = {
grinch_items.moves.BAD_BREATH: GrinchItemData(
[grinch_categories.MOVES],
700,
IC.progression,
[
GrinchRamData(0x0100BB, binary_bit_pos=1),
],
),
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] = {
**GADGETS_TABLE,
**MISSION_ITEMS_TABLE,
**KEYS_TABLE,
**MISC_ITEMS_TABLE,
**TRAPS_TABLE,
**USEFUL_IC_TABLE,
**USEFUL_ITEMS_TABLE,
# **SLEIGH_PARTS_TABLE,
# **MOVES_TABLE,
**MOVES_TABLE,
}
# Psuedocoding traplink table
# BEE_TRAP_EQUIV = ["Army Trap", "Buyon Trap", "Ghost", "Gooey Bag", "OmoTrap", "Police Trap"]
# ICE_TRAP_EQUIV = ["Chaos Control Trap", "Freeze Trap", "Frozen Trap", "Honey Trap", "Paralyze Trap", "Stun Trap", "Bubble Trap"]
# DAMAGE_TRAP_EQUIV = ["Banana Trap", "Bomb", "Bonk Trap", "Fire Trap", "Laughter Trap", "Nut Trap", "Push Trap", "Squash Trap", "Thwimp Trap", "TNT Barrel Trap", "Meteor Trap"]
# DAMAGE_TRAP_EQUIV = ["Banana Trap", "Bomb", "Bonk Trap", "Fire Trap", "Laughter Trap", "Nut Trap", "Push Trap",
# "Squash Trap", "Thwimp Trap", "TNT Barrel Trap", "Meteor Trap", "Double Damage", "Spike Ball Trap"]
# SPRING_TRAP_EQUIV = ["Eject Ability", "Hiccup Trap", "Jump Trap", "Jumping Jacks Trap", "Whoops! Trap"]
# HOME_TRAP_EQUIV = ["Blue Balls Curse", "Instant Death Trap"]
# HOME_TRAP_EQUIV = ["Blue Balls Curse", "Instant Death Trap", "Get Out Trap"]
# SLOWNESS_TRAP_EQUIV = ["Iron Boots Trap", "Slow Trap", "Sticky Floor Trap"]
# CUTSCENE_TRAP_EQUIV = ["Phone Trap"]
# ELEC_TRAP_EQUIV = []
# DEPL_TRAP_EQUIV = ["Dry Trap"]
def grinch_items_to_id() -> dict[str, int]:
item_mappings: dict[str, int] = {}
for ItemName, ItemData in ALL_ITEMS_TABLE.items():
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 Options import FreeText, NumericOption, Toggle, DefaultOnToggle, Choice, TextChoice, Range, NamedRange, OptionList, \
PerGameCommonOptions
from Options import (
FreeText,
NumericOption,
Toggle,
DefaultOnToggle,
Choice,
TextChoice,
Range,
NamedRange,
OptionList,
PerGameCommonOptions,
OptionSet,
)
class StartingArea(Choice):
"""
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.
"""
option_whoville = 0
option_who_forest = 1
option_who_dump = 2
@@ -15,13 +28,16 @@ class StartingArea(Choice):
default = 0
display_name = "Starting Area"
class ProgressiveVacuum(Toggle):#DefaultOnToggle
class ProgressiveVacuum(Toggle): # DefaultOnToggle
"""
Determines whether you get access to main areas progressively [NOT IMPLEMENTED]
Enabled: Whoville > Who Forest > Who Dump > Who Lake
"""
display_name = "Progressive Vacuum Access"
display_name = "Progressive Vacuum Tubes"
class Missionsanity(Choice):
"""
@@ -32,6 +48,7 @@ class Missionsanity(Choice):
Individual: Individual tasks for one mission, such as individual snowmen squashed, are checks.
Both: Both individual tasks and mission completion are randomized.
"""
display_name = "Mission Locations"
option_none = 0
option_completion = 1
@@ -39,52 +56,101 @@ class Missionsanity(Choice):
option_both = 3
default = 1
class AnnoyingLocations(DefaultOnToggle):
"""Makes certain long, annoying, and tedious checks to be excluded [NOT IMPLEMENTED]"""
display_name = "Annoying Locations"
class ProgressiveGadget(Toggle):#DefaultOnToggle
class ExcludeEnvironments(OptionSet):
"""
Determines whether you get access to a gadget as individual blueprint count [NOT IMPLEMENTED]
Allows entire environments to be an excluded location to ensure you are not logically required to enter the environment along
with any and all checks that are in that environment too.
WARNING: Excluding too many environments may cause generation to fail.
[NOT IMPLEMENTED]
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"
"""
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]
"""
display_name = "Progressive Gadgets"
class Supadow(Toggle):
"""Enables completing minigames through the Supadows in Mount Crumpit as checks. (9 locations) [NOT IMPLEMENTED]"""
display_name = "Supadow Minigame Locations"
"""Enables completing minigames through the Supadows in Mount Crumpit as checks. NOT IMPLEMENTED]"""
display_name = "Supadow Minigames"
class Gifts(Toggle):
"""Missions that require you to squash every present in a level. (4 locations) [NOT IMPLEMENTED]"""
display_name = "Gift Collection Locations"
class Gifts(Range):
"""
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]
"""
display_name = "Gifts Squashed per Check"
range_start = 0
range_end = 300
default = 0
class Movesanity(Toggle):
class Moverando(Toggle):
"""Randomizes Grinch's moveset along with randomizing max into the pool. [NOT IMPLEMENTED]"""
display_name = "Movesanity"
display_name = "Moves Randomized"
class UnlimitedEggs(Toggle):
"""Determine whether or not you run out of rotten eggs when you utilize your gadgets."""
display_name = "Unlimited Rotten Eggs"
class RingLinkOption(Toggle):
"""Whenever this is toggled, your ammo is linked with other ringlink-compatible games that also have this enabled. [NOT IMPLEMENTED]"""
"""Whenever this is toggled, your ammo is linked with other ringlink-compatible games that also have this enabled."""
display_name = "Ring Link"
class TrapLinkOption(Toggle):
"""If a trap is sent from Grinch, traps that are compatible with other games are triggered aswell. [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"
@dataclass
class GrinchOptions(PerGameCommonOptions):#DeathLinkMixin
class GrinchOptions(PerGameCommonOptions): # DeathLinkMixin
starting_area: StartingArea
progressive_vacuum: ProgressiveVacuum
missionsanity: Missionsanity
annoying_locations: AnnoyingLocations
minigamesanity: Supadow
exclude_environments: ExcludeEnvironments
progressive_gadget: ProgressiveGadget
supadow_minigames: Supadow
giftsanity: Gifts
movesanity: Movesanity
move_rando: Moverando
unlimited_eggs: UnlimitedEggs
ring_link: RingLinkOption
trap_link: TrapLinkOption

View File

@@ -1,12 +1,66 @@
from enum import STRICT, IntEnum
from typing import NamedTuple, Optional
class UpdateMethod(IntEnum, boundary=STRICT):
SET = 1
ADD = 2
SUBTRACT = 3
FREEZE = 4
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
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
bit_size: int = 1
update_existing_value: bool = False
max_count: int = 0
byte_size: int = 1
update_method: UpdateMethod = UpdateMethod.SET
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,13 +13,13 @@ mainareas_list = [
"Whoville",
"Who Forest",
"Who Dump",
"Who Lake"
"Who Lake",
]
subareas_list = [
"Post Office",
"City Hall",
"Countdown to X-Mas Tower",
"Clock Tower",
"Ski Resort",
"Civic Center",
"Minefield",
@@ -29,29 +29,35 @@ subareas_list = [
"Scout's Hut",
"North Shore",
"Mayor's Villa",
"Sleigh Room"
"Sleigh Room",
]
supadow_list = [
"Spin N' Win Supadow",
"Dankamania Supadow",
"The Copter Race Contest Supadow",
"Bike Race"
"Bike Race",
]
def create_regions(world: "GrinchWorld"):
for mainarea in mainareas_list:
#Each area in mainarea, create a region for the given player
world.multiworld.regions.append(Region(mainarea, world.player, world.multiworld))
# Each area in mainarea, create a region for the given player
world.multiworld.regions.append(
Region(mainarea, world.player, world.multiworld)
)
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))
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))
# 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)
connected_region = world.get_region(connected_region_name)
required_items: list[list[str]] = access_rules_dict[connected_region.name]
@@ -62,33 +68,38 @@ def grinchconnect(world: "GrinchWorld", current_region_name: str, connected_regi
connected_region.connect(current_region)
for access_rule in rule_list:
for region_entrance in current_region.entrances:
if region_entrance.connected_region.name == current_region_name and \
region_entrance.parent_region.name == connected_region_name:
if (
region_entrance.connected_region.name == current_region_name
and region_entrance.parent_region.name == connected_region_name
):
if rule_list.index(access_rule) == 0:
add_rule(region_entrance, access_rule)
else:
add_rule(region_entrance, access_rule, combine="or")
for region_entrance in connected_region.entrances:
if region_entrance.connected_region.name == connected_region_name and \
region_entrance.parent_region.name == current_region_name:
if (
region_entrance.connected_region.name == connected_region_name
and region_entrance.parent_region.name == current_region_name
):
if rule_list.index(access_rule) == 0:
add_rule(region_entrance, access_rule)
else:
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"):
grinchconnect(world, "Mount Crumpit", "Whoville")
grinchconnect(world, "Mount Crumpit", "Who Forest")
grinchconnect(world, "Mount Crumpit", "Who Dump")
grinchconnect(world, "Mount Crumpit", "Who Lake")
grinchconnect(world, "Mount Crumpit", "Sleigh Room")
grinchconnect(world, "Mount Crumpit", "Spin N' Win Supadow")
grinchconnect(world, "Mount Crumpit", "Dankamania Supadow")
grinchconnect(world, "Mount Crumpit", "The Copter Race Contest Supadow")
grinchconnect(world, "Mount Crumpit", "Spin N' Win")
grinchconnect(world, "Mount Crumpit", "Dankamania")
grinchconnect(world, "Mount Crumpit", "The Copter Race Contest")
grinchconnect(world, "Whoville", "Post Office")
grinchconnect(world, "Whoville", "City Hall")
grinchconnect(world, "Whoville", "Countdown to X-Mas Clock Tower")
grinchconnect(world, "Whoville", "Clock Tower")
grinchconnect(world, "Who Forest", "Ski Resort")
grinchconnect(world, "Who Forest", "Civic Center")
grinchconnect(world, "Who Dump", "Minefield")

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
from BaseClasses import Region, Item, ItemClassification
from .Locations import grinch_locations_to_id, grinch_locations, GrinchLocation
from .Items import grinch_items_to_id, GrinchItem, ALL_ITEMS_TABLE, MISC_ITEMS_TABLE
from .Locations import grinch_locations_to_id, grinch_locations, GrinchLocation, get_location_names_per_category
from .Items import grinch_items_to_id, GrinchItem, ALL_ITEMS_TABLE, MISC_ITEMS_TABLE, get_item_names_per_category
from .Regions import connect_regions
from .Rules import set_location_rules
@@ -8,27 +8,32 @@ from .Client import *
from typing import ClassVar
from worlds.AutoWorld import World
from Options import OptionError
from . import Options
from .Options import GrinchOptions
from .Rules import access_rules_dict
class GrinchWorld(World):
game: ClassVar[str] = "The Grinch"
options_dataclass = Options.GrinchOptions
options = Options.GrinchOptions
options: Options.GrinchOptions
topology_present = True #not an open world game, very linear
item_name_to_id: ClassVar[dict[str,int]] = grinch_items_to_id()
location_name_to_id: ClassVar[dict[str,int]] = grinch_locations_to_id()
required_client_version = (0, 6, 3)
item_name_groups = get_item_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
self.origin_region_name: str = "Mount Crumpit"
super(GrinchWorld, self).__init__(*args, **kwargs)
def generate_early(self) -> None: #Special conditions changed before generation occurs
pass
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." +
f"The following player's YAML needs to be fixed: {self.player_name}")
def create_regions(self): #Generates all regions for the multiworld
for region_name in access_rules_dict.keys():
@@ -37,7 +42,7 @@ class GrinchWorld(World):
for location, data in grinch_locations.items():
region = self.get_region(data.region)
entry = GrinchLocation(self.player, location, region, data)
if location == "Neutralizing Santa":
if location == "MC - Sleigh Ride - Neutralizing Santa":
entry.place_locked_item(Item("Goal", ItemClassification.progression, None, self.player))
region.locations.append(entry)
connect_regions(self)
@@ -72,7 +77,7 @@ class GrinchWorld(World):
def fill_slot_data(self):
return {
"give_unlimited_eggs": self.options.unlimited_eggs.value,
"ring_link": self.options.ring_link.value,
}
def generate_output(self, output_directory: str) -> None:

View File

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

View File

@@ -1,5 +1,7 @@
- Credit to Raven-187 & Hacc on gamehacking.org for providing the addresses for various cheat codes. Without them, this
would of made RAM searching much more tedious.
- Shoutouts to SomeJakeGuy for basically teaching me how to code in general.
- Shoutouts to SomeJakeGuy for basically teaching me how to code in general along with starting decompilation of the game
into motion
- Shoutouts to BootsinSoots for helping with the implementation of the logic rules code itself.
- Thanks to the Grinch PS1 speedrunning discord community server for encouraging the production of this randomizer.
- Thanks to the Grinch PS1 speedrunning discord community server for encouraging the production of this randomizer.
- Credit to Artamiss for the vast majority of BZZ decompilation and general backend coding.

View File

@@ -1,31 +1,45 @@
# The Grinch - Setup Guide
# The Grinch (PS1) - Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Please use version 0.6.2 or later for integrated
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Please use version 0.6.3 or later for integrated
BizHawk support.
- Legally obtained NTSC Bin ROM file, probably named something like `Grinch, The (USA) (En,Fr,Es).bin`.
- CUE files may work, but I have not tested this.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) Version 2.9.1 is supported, but I can't promise if any version is stable or not.
- 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.
- [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.
- 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`.
## Configuring your Config (.yaml) file
## Configuring BizHawk
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
### What is a config file and why do I need one?
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
tabbed out of EmuHawk.
- Under `Config > Preferred Cores > PSX`, select NymaShock.
- Open any PlayStation game in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
`Controllers…`, it's because you need to load a game first.
You may need to invert Sensitivity for the up/down axis to -100%.
This can be found under Analog Controls through `Config > Controllers…`.
Depending on your controller, you may also want to tweak the Deadzone. Something like 6% is recommended for a DualShock 4.
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
clear it.
- You are required to legally obtain a PSX Bios BIN firmware file for the game to be opened. To import this, you go to
`Config > Firmware... > Tools` and scrolling until you see the PlayStation tab. You might right click on the bios region
and click `Set Customization` or `Import` on the top of the window. Then a window should open, telling you what file to
import, which is the BIN file required. The bios should be recognized if Bizhawk displays a checkmark beside it, saying
the bios version you have is stable and should run without issues. If a bios is already been imported and the game
runs fine by itself, you may skip this step.
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
## Generating a Game
### Where do I get a config file?
The Player options page on the website allows you to configure your personal
options and export a config file from them: [The Grinch Player Options Page](../player-options)
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do
so on the YAML Validator page: [YAML Validation page](/check)
## Joining a MultiWorld Game
1. Create your options file (YAML). After installing the `grinch.apworld` file, you can generate a template within the Archipelago Launcher by clicking `Generate Template Settings`.
2. Follow the general Archipelago instructions for [generating a game](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game).
3. Open `ArchipelagoLauncher.exe`
4. Select "BizHawk Client" in the right-side column. On your first time opening BizHawk Client, you will also be asked to
locate `EmuHawk.exe` in your BizHawk install.
### Connect to the Multiserver
@@ -36,19 +50,4 @@ script. Navigate to your Archipelago install folder and open `data/lua/connector
To connect the client to the multiserver simply put `<address>:<port>` on the text field on top and
press enter (if the server uses a password, type in the bottom text field
`/connect <address>:<port> [password]`)
## Hosting a MultiWorld game
The recommended way to host a game is to use our hosting service. The process is relatively simple:
1. Collect config files from your players.
2. Upload the config files to the Generate page above.
- Generate page: [WebHost Seed Generation Page](/generate)
3. Wait a moment while the seed is generated.
4. When the seed is generated, you will be redirected to a "Seed Info" page.
5. Click "Create New Room". This will take you to the server page. Provide the link to this page to
your players, so they may download their patch files from there.
6. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the
progress of all players in the game. Any observers may also be given the link to this page.
7. Once all players have joined, you may begin playing.
`/connect <address>:<port> [password]`)