35 Commits

Author SHA1 Message Date
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
7 changed files with 208 additions and 85 deletions

View File

@@ -1,7 +1,10 @@
from typing import TYPE_CHECKING
import time
from typing import TYPE_CHECKING, Sequence
import asyncio
import NetUtils
import copy
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
import worlds._bizhawk as bizhawk
@@ -23,14 +26,20 @@ 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
def __init__(self):
super().__init__()
@@ -65,7 +74,7 @@ class GrinchClient(BizHawkClient):
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
@@ -79,13 +88,33 @@ class GrinchClient(BizHawkClient):
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.")
ring_link_enabled = bool(ctx.slot_data["ring_link"])
tags = copy.deepcopy(ctx.tags)
if ring_link_enabled:
self.send_ring_link = True
Utils.async_start(self.ring_link_output(ctx), name="EggLink")
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"] != ctx.player_names[ctx.slot]:
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:
if not ctx.slot:
return
try:
@@ -102,14 +131,32 @@ class GrinchClient(BizHawkClient):
# 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))
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.bit_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)
@@ -120,15 +167,15 @@ class GrinchClient(BizHawkClient):
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.bit_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)
@@ -148,6 +195,7 @@ class GrinchClient(BizHawkClient):
# Ensures we only get the new items that we want to give the player
new_items_only = ctx.items_received[self.last_received_index:]
addr_list_to_update: list[tuple[int, Sequence[int], str]] = []
for item_received in new_items_only:
local_item = ctx.item_names.lookup_in_game(item_received.item)
@@ -167,12 +215,15 @@ class GrinchClient(BizHawkClient):
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")])
addr_list_to_update.append((addr_to_update.ram_address,
current_ram_address_value.to_bytes(addr_to_update.bit_size, "little"),"MainRAM"))
self.last_received_index += 1
await self.update_and_validate_address(ctx, RECV_ITEM_ADDR, self.last_received_index, RECV_ITEM_BITSIZE)
addr_list_to_update.append((RECV_ITEM_ADDR,
self.last_received_index.to_bytes(RECV_ITEM_BITSIZE,"little"), "MainRAM"))
await bizhawk.write(ctx.bizhawk_ctx, addr_list_to_update)
async def goal_checker(self, ctx: "BizHawkClientContext"):
if not ctx.finished_game:
@@ -189,14 +240,17 @@ class GrinchClient(BizHawkClient):
# 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"):
addr_list_to_update: list[tuple[int, Sequence[int], str]] = []
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))
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)
addr_list_to_update.append((heart_item_data.update_ram_addr[0].ram_address,
min(heart_count, 4).to_bytes(1, "little"), "MainRAM"))
# 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
addr_list_to_update.append((0x0100F0, int(0).to_bytes(4, "little"), "MainRAM"))
for (item_name, item_data) in items_to_check.items():
# If item is an event or already been received, ignore.
@@ -210,30 +264,47 @@ class GrinchClient(BizHawkClient):
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)
addr_list_to_update.append((addr_to_update.ram_address,
current_bin_value.to_bytes(1, "little"), "MainRAM"))
else:
await self.update_and_validate_address(ctx, addr_to_update.ram_address, 0, 1)
addr_list_to_update.append((addr_to_update.ram_address,
int(0).to_bytes(1, "little"), "MainRAM"))
await bizhawk.write(ctx.bizhawk_ctx, addr_list_to_update)
# Removes the regional access until you actually received it from AP.
async def constant_address_update(self, ctx: "BizHawkClientContext"):
addr_list_to_update: list[tuple[int, Sequence[int], str]] = []
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 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)
addr_list_to_update.append((addr_to_update.ram_address,
current_bin_value.to_bytes(1, "little"), "MainRAM"))
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:
addr_list_to_update.append((addr_to_update.ram_address,
addr_to_update.value.to_bytes(1, "little"), "MainRAM"))
else:
addr_list_to_update.append((addr_to_update.ram_address,
int(0).to_bytes(1, "little"), "MainRAM"))
await bizhawk.write(ctx.bizhawk_ctx, addr_list_to_update)
async def ingame_checker(self, ctx: "BizHawkClientContext"):
from CommonClient import logger
@@ -275,13 +346,43 @@ class GrinchClient(BizHawkClient):
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": ctx.player_names[ctx.slot],
"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
current_egg_count = int.from_bytes(
(await bizhawk.read(ctx.bizhawk_ctx, [(EGG_COUNT_ADDR, EGG_ADDR_BYTESIZE, "MainRAM")]))[0], "little")
current_egg_count = min(current_egg_count + egg_amount, 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.")

View File

@@ -203,6 +203,7 @@ TRAPS_TABLE: dict[str, GrinchItemData] = {
# "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()])
# "Disable Jump Trap": GrinchItemData("Traps", 613, IC.trap,[GrinchRamData(0x010026, binary_bit_pos=6)])
}
#Movesets
@@ -228,12 +229,15 @@ ALL_ITEMS_TABLE: dict[str, GrinchItemData] = {
# 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] = {}

View File

@@ -56,14 +56,14 @@ grinch_locations = {
"Launching Eggs Into Houses": GrinchLocationData("Whoville", "Whoville Missions", 203, [GrinchRamData(0x0100C7, value=10)]),
"Modifying The Mayor's Statue": GrinchLocationData("City Hall", "Whoville Missions", 204, [GrinchRamData(0x0100BE, binary_bit_pos=1)]),
"Advancing The Countdown-To-Xmas Clock": GrinchLocationData("Countdown to X-Mas Clock Tower", "Whoville Missions", 205, [GrinchRamData(0x0100BE, binary_bit_pos=2)]),
"Squashing All Gifts in Whoville": GrinchLocationData("Whoville", "Whoville Missions", 206, [GrinchRamData(0x01005C, value=500)]),
"Squashing All Gifts in Whoville": GrinchLocationData("Whoville", "Whoville Missions", 206, [GrinchRamData(0x01005C, value=500, bit_size=2)]),
#Who Forest Missions
"Making Xmas Trees Droop": GrinchLocationData("Who Forest", "Who Forest Missions", 300, [GrinchRamData(0x0100C8, value=10)]),
"Sabotaging Snow Cannon With Glue": GrinchLocationData("Who Forest", "Who Forest Missions", 301, [GrinchRamData(0x0100BE, binary_bit_pos=3)]),
"Putting Beehives In Cabins": GrinchLocationData("Who Forest", "Who Forest Missions", 302, [GrinchRamData(0x0100CA, value=10)]),
"Sliming The Mayor's Skis": GrinchLocationData("Ski Resort", "Who Forest Missions", 303, [GrinchRamData(0x0100BE, binary_bit_pos=4)]),
"Replacing The Candles On The Cake With Fireworks": GrinchLocationData("Civic Center", "Who Forest Missions", 304, [GrinchRamData(0x0100BE, binary_bit_pos=5)]),
"Squashing All Gifts in Who Forest": GrinchLocationData("Who Forest", "Who Forest Missions", 305, [GrinchRamData(0x01005E, value=750)]),
"Squashing All Gifts in Who Forest": GrinchLocationData("Who Forest", "Who Forest Missions", 305, [GrinchRamData(0x01005E, value=750, bit_size=2)]),
#Who Dump Missions
"Stealing Food From Birds": GrinchLocationData("Who Dump", "Who Dump Missions", 400, [GrinchRamData(0x0100CB, value=10)]),
"Feeding The Computer With Robot Parts": GrinchLocationData("Who Dump", "Who Dump Missions", 401, [GrinchRamData(0x0100BF, binary_bit_pos=2)]),
@@ -71,14 +71,14 @@ grinch_locations = {
"Conducting The Stinky Gas To Who-Bris' Shack": GrinchLocationData("Who Dump", "Who Dump Missions", 403, [GrinchRamData(0x0100BE, binary_bit_pos=7)]),
"Shaving Who Dump Guardian": GrinchLocationData("Minefield", "Who Dump Missions", 404, [GrinchRamData(0x0100BF, binary_bit_pos=0)]),
"Short-Circuiting Power-Plant": GrinchLocationData("Generator Building", "Who Dump Missions", 405, [GrinchRamData(0x0100BF, binary_bit_pos=1)]),
"Squashing All Gifts in Who Dump": GrinchLocationData("Who Dump", "Who Dump Missions", 406, [GrinchRamData(0x010060, value=750)]),
"Squashing All Gifts in Who Dump": GrinchLocationData("Who Dump", "Who Dump Missions", 406, [GrinchRamData(0x010060, value=750, bit_size=2)]),
#Who Lake Missions
"Putting Thistles In Shorts": GrinchLocationData("Who Lake", "Who Lake Missions", 500, [GrinchRamData(0x0100E5, value=10)]),
"Sabotaging The Tents": GrinchLocationData("Who Lake", "Who Lake Missions", 501, [GrinchRamData(0x0100E6, value=10)]),
"Drilling Holes In Canoes": GrinchLocationData("North Shore", "Who Lake Missions", 502, [GrinchRamData(0x0100EE, value=10)]),
"Modifying The Marine Mobile": GrinchLocationData("Submarine World", "Who Lake Missions", 503, [GrinchRamData(0x0100BF, binary_bit_pos=4)]),
"Hooking The Mayor's Bed To The Motorboat": GrinchLocationData("Mayor's Villa", "Who Lake Missions", 504, [GrinchRamData(0x0100BF, binary_bit_pos=3)]),
"Squashing All Gifts in Who Lake": GrinchLocationData("Who Lake", "Who Lake Missions", 505, [GrinchRamData(0x010062, value=1000)]),
"Squashing All Gifts in Who Lake": GrinchLocationData("Who Lake", "Who Lake Missions", 505, [GrinchRamData(0x010062, value=1000, bit_size=2)]),
#Need to find binary values for individual blueprints, but all ram addresses are found
#Blueprints
#Binoculars Blueprints
@@ -182,6 +182,12 @@ grinch_locations = {
"Tires in Who Dump": GrinchLocationData("Sleigh Room", "Sleigh Ride", 1602, [GrinchRamData(0x0101FB, binary_bit_pos=4)]),
"Twin-End Tuba in Submarine World": GrinchLocationData("Sleigh Room", "Sleigh Ride", 1603, [GrinchRamData(0x0101FB, binary_bit_pos=6)]),
"GPS in Who Lake": GrinchLocationData("Sleigh Room", "Sleigh Ride", 1604, [GrinchRamData(0x0101FB, binary_bit_pos=5)]),
# Mount Crumpit Locations
"1st Crate Squashed": GrinchLocationData("Mount Crumpit", "Mount Crumpit", 1700, [GrinchRamData(0x095343, value=1)]),
"2nd Crate Squashed": GrinchLocationData("Mount Crumpit", "Mount Crumpit", 1701, [GrinchRamData(0x095343, value=2)]),
"3rd Crate Squashed": GrinchLocationData("Mount Crumpit", "Mount Crumpit", 1702, [GrinchRamData(0x095343, value=3)]),
"4th Crate Squashed": GrinchLocationData("Mount Crumpit", "Mount Crumpit", 1703, [GrinchRamData(0x095343, value=4)]),
"5th Crate Squashed": GrinchLocationData("Mount Crumpit", "Mount Crumpit", 1704, [GrinchRamData(0x095343, value=5)]),
}
def grinch_locations_to_id() -> dict[str,int]:

View File

@@ -69,7 +69,7 @@ class UnlimitedEggs(Toggle):
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."""
class TrapLinkOption(Toggle):
"""If a trap is sent from Grinch, traps that are compatible with other games are triggered aswell. [NOT IMPLEMENTED]"""
@@ -80,7 +80,6 @@ class GrinchOptions(PerGameCommonOptions):#DeathLinkMixin
progressive_vacuum: ProgressiveVacuum
missionsanity: Missionsanity
annoying_locations: AnnoyingLocations
minigamesanity: Supadow
progressive_gadget: ProgressiveGadget
supadow_minigames: Supadow
giftsanity: Gifts

View File

@@ -26,7 +26,7 @@ def interpret_rule(rule_set: list[list[str]], player: int):
access_list: list[Callable[[CollectionState], bool]] = []
for item_set in rule_set:
access_list.append(lambda state: state.has_all(item_set, player))
access_list.append(lambda state, items=tuple(item_set): state.has_all(items, player))
return access_list
#Each item in the list is a separate list of rules. Each separate list is just an "OR" condition.
@@ -295,7 +295,7 @@ rules_dict: dict[str,list[list[str]]] = {
# ["Max"]
],
"OCD Blueprint - Guardian's House - Right Side": [
["Rotten Egg Launcher", "Grinch Copter"],
["Grinch Copter"],
["Slime Shooter", "Rocket Spring"]
],
"OCD Blueprint - Inside Guardian's House": [
@@ -495,6 +495,21 @@ rules_dict: dict[str,list[list[str]]] = {
"GPS in Who Lake": [
["Who Lake Vacuum Access", "Rotten Egg Launcher"]
],
"1st Crate Squashed": [
[]
],
"2nd Crate Squashed": [
[]
],
"3rd Crate Squashed": [
[]
],
"4th Crate Squashed": [
[]
],
"5th Crate Squashed": [
[]
]
}
@@ -560,19 +575,23 @@ access_rules_dict: dict[str,list[list[str]]] = {
["Sleigh Room Key"]
],
"Spin N' Win Supadow": [
["Spin N' Win Door Unlock"],
[]
# ["Spin N' Win Door Unlock"],
# ["Progressive Supadow Door Unlock"]
],
"Dankamania Supadow": [
["Dankamania Door Unlock"],
[]
# ["Dankamania Door Unlock"],
# ["Progressive Supadow Door Unlock: 2"]
],
"The Copter Race Contest Supadow": [
["The Copter Race Contest Door Unlock"],
[]
# ["The Copter Race Contest Door Unlock"],
# ["Progressive Supadow Door Unlock: 3"]
],
"Bike Race": [
["Bike Race Access"],
[]
# ["Bike Race Access"],
# ["Progressive Supadow Door Unlock: 4"]
]
}

View File

@@ -8,15 +8,16 @@ from .Client import *
from typing import ClassVar
from worlds.AutoWorld import World
import Options
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()
@@ -28,7 +29,10 @@ class GrinchWorld(World):
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 Options.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():
@@ -72,7 +76,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

@@ -8,24 +8,29 @@ BizHawk support.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) Version 2.9.1 is supported, but I can't promise if any version is stable or not.
- 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.
## 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.
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 +41,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]`)