-
Notifications
You must be signed in to change notification settings - Fork 657
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Yoshi's Island: Implement New Game (#2141)
Co-authored-by: Silvris <[email protected]> Co-authored-by: Alchav <[email protected]> Co-authored-by: NewSoupVi <[email protected]> Co-authored-by: Exempt-Medic <[email protected]>
- Loading branch information
1 parent
aaa3472
commit 355223b
Showing
16 changed files
with
4,559 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import logging | ||
import struct | ||
import typing | ||
import time | ||
from struct import pack | ||
|
||
from NetUtils import ClientStatus, color | ||
from worlds.AutoSNIClient import SNIClient | ||
|
||
if typing.TYPE_CHECKING: | ||
from SNIClient import SNIContext | ||
|
||
snes_logger = logging.getLogger("SNES") | ||
|
||
ROM_START = 0x000000 | ||
WRAM_START = 0xF50000 | ||
WRAM_SIZE = 0x20000 | ||
SRAM_START = 0xE00000 | ||
|
||
YOSHISISLAND_ROMHASH_START = 0x007FC0 | ||
ROMHASH_SIZE = 0x15 | ||
|
||
ITEMQUEUE_HIGH = WRAM_START + 0x1465 | ||
ITEM_RECEIVED = WRAM_START + 0x1467 | ||
DEATH_RECEIVED = WRAM_START + 0x7E23B0 | ||
GAME_MODE = WRAM_START + 0x0118 | ||
YOSHI_STATE = SRAM_START + 0x00AC | ||
DEATHLINK_ADDR = ROM_START + 0x06FC8C | ||
DEATHMUSIC_FLAG = WRAM_START + 0x004F | ||
DEATHFLAG = WRAM_START + 0x00DB | ||
DEATHLINKRECV = WRAM_START + 0x00E0 | ||
GOALFLAG = WRAM_START + 0x14B6 | ||
|
||
VALID_GAME_STATES = [0x0F, 0x10, 0x2C] | ||
|
||
|
||
class YoshisIslandSNIClient(SNIClient): | ||
game = "Yoshi's Island" | ||
|
||
async def deathlink_kill_player(self, ctx: "SNIContext") -> None: | ||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read | ||
game_state = await snes_read(ctx, GAME_MODE, 0x1) | ||
if game_state[0] != 0x0F: | ||
return | ||
|
||
yoshi_state = await snes_read(ctx, YOSHI_STATE, 0x1) | ||
if yoshi_state[0] != 0x00: | ||
return | ||
|
||
snes_buffered_write(ctx, WRAM_START + 0x026A, bytes([0x01])) | ||
snes_buffered_write(ctx, WRAM_START + 0x00E0, bytes([0x01])) | ||
await snes_flush_writes(ctx) | ||
ctx.death_state = DeathState.dead | ||
ctx.last_death_link = time.time() | ||
|
||
async def validate_rom(self, ctx: "SNIContext") -> bool: | ||
from SNIClient import snes_read | ||
|
||
rom_name = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE) | ||
if rom_name is None or rom_name[:7] != b"YOSHIAP": | ||
return False | ||
|
||
ctx.game = self.game | ||
ctx.items_handling = 0b111 # remote items | ||
ctx.rom = rom_name | ||
|
||
death_link = await snes_read(ctx, DEATHLINK_ADDR, 1) | ||
if death_link: | ||
await ctx.update_death_link(bool(death_link[0] & 0b1)) | ||
return True | ||
|
||
async def game_watcher(self, ctx: "SNIContext") -> None: | ||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read | ||
|
||
game_mode = await snes_read(ctx, GAME_MODE, 0x1) | ||
item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1) | ||
game_music = await snes_read(ctx, DEATHMUSIC_FLAG, 0x1) | ||
goal_flag = await snes_read(ctx, GOALFLAG, 0x1) | ||
|
||
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): | ||
death_flag = await snes_read(ctx, DEATHFLAG, 0x1) | ||
deathlink_death = await snes_read(ctx, DEATHLINKRECV, 0x1) | ||
currently_dead = (game_music[0] == 0x07 or game_mode[0] == 0x12 or | ||
(death_flag[0] == 0x00 and game_mode[0] == 0x11)) and deathlink_death[0] == 0x00 | ||
await ctx.handle_deathlink_state(currently_dead) | ||
|
||
if game_mode is None: | ||
return | ||
elif goal_flag[0] != 0x00: | ||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | ||
ctx.finished_game = True | ||
elif game_mode[0] not in VALID_GAME_STATES: | ||
return | ||
elif item_received[0] > 0x00: | ||
return | ||
|
||
from .Rom import item_values | ||
rom = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE) | ||
if rom != ctx.rom: | ||
ctx.rom = None | ||
return | ||
|
||
new_checks = [] | ||
from .Rom import location_table | ||
|
||
location_ram_data = await snes_read(ctx, WRAM_START + 0x1440, 0x80) | ||
for loc_id, loc_data in location_table.items(): | ||
if loc_id not in ctx.locations_checked: | ||
data = location_ram_data[loc_data[0] - 0x1440] | ||
masked_data = data & (1 << loc_data[1]) | ||
bit_set = masked_data != 0 | ||
invert_bit = ((len(loc_data) >= 3) and loc_data[2]) | ||
if bit_set != invert_bit: | ||
new_checks.append(loc_id) | ||
|
||
for new_check_id in new_checks: | ||
ctx.locations_checked.add(new_check_id) | ||
location = ctx.location_names[new_check_id] | ||
total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) | ||
snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") | ||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) | ||
|
||
recv_count = await snes_read(ctx, ITEMQUEUE_HIGH, 2) | ||
recv_index = struct.unpack("H", recv_count)[0] | ||
if recv_index < len(ctx.items_received): | ||
item = ctx.items_received[recv_index] | ||
recv_index += 1 | ||
logging.info("Received %s from %s (%s) (%d/%d in list)" % ( | ||
color(ctx.item_names[item.item], "red", "bold"), | ||
color(ctx.player_names[item.player], "yellow"), | ||
ctx.location_names[item.location], recv_index, len(ctx.items_received))) | ||
|
||
snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index)) | ||
if item.item in item_values: | ||
item_count = await snes_read(ctx, WRAM_START + item_values[item.item][0], 0x1) | ||
increment = item_values[item.item][1] | ||
new_item_count = item_count[0] | ||
if increment > 1: | ||
new_item_count = increment | ||
else: | ||
new_item_count += increment | ||
|
||
snes_buffered_write(ctx, WRAM_START + item_values[item.item][0], bytes([new_item_count])) | ||
await snes_flush_writes(ctx) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
from typing import Dict, Set, Tuple, NamedTuple, Optional | ||
from BaseClasses import ItemClassification | ||
|
||
class ItemData(NamedTuple): | ||
category: str | ||
code: Optional[int] | ||
classification: ItemClassification | ||
amount: Optional[int] = 1 | ||
|
||
item_table: Dict[str, ItemData] = { | ||
"! Switch": ItemData("Items", 0x302050, ItemClassification.progression), | ||
"Dashed Platform": ItemData("Items", 0x302051, ItemClassification.progression), | ||
"Dashed Stairs": ItemData("Items", 0x302052, ItemClassification.progression), | ||
"Beanstalk": ItemData("Items", 0x302053, ItemClassification.progression), | ||
"Helicopter Morph": ItemData("Morphs", 0x302054, ItemClassification.progression), | ||
"Spring Ball": ItemData("Items", 0x302055, ItemClassification.progression), | ||
"Large Spring Ball": ItemData("Items", 0x302056, ItemClassification.progression), | ||
"Arrow Wheel": ItemData("Items", 0x302057, ItemClassification.progression), | ||
"Vanishing Arrow Wheel": ItemData("Items", 0x302058, ItemClassification.progression), | ||
"Mole Tank Morph": ItemData("Morphs", 0x302059, ItemClassification.progression), | ||
"Watermelon": ItemData("Items", 0x30205A, ItemClassification.progression), | ||
"Ice Melon": ItemData("Items", 0x30205B, ItemClassification.progression), | ||
"Fire Melon": ItemData("Items", 0x30205C, ItemClassification.progression), | ||
"Super Star": ItemData("Items", 0x30205D, ItemClassification.progression), | ||
"Car Morph": ItemData("Morphs", 0x30205E, ItemClassification.progression), | ||
"Flashing Eggs": ItemData("Items", 0x30205F, ItemClassification.progression), | ||
"Giant Eggs": ItemData("Items", 0x302060, ItemClassification.progression), | ||
"Egg Launcher": ItemData("Items", 0x302061, ItemClassification.progression), | ||
"Egg Plant": ItemData("Items", 0x302062, ItemClassification.progression), | ||
"Submarine Morph": ItemData("Morphs", 0x302063, ItemClassification.progression), | ||
"Chomp Rock": ItemData("Items", 0x302064, ItemClassification.progression), | ||
"Poochy": ItemData("Items", 0x302065, ItemClassification.progression), | ||
"Platform Ghost": ItemData("Items", 0x302066, ItemClassification.progression), | ||
"Skis": ItemData("Items", 0x302067, ItemClassification.progression), | ||
"Train Morph": ItemData("Morphs", 0x302068, ItemClassification.progression), | ||
"Key": ItemData("Items", 0x302069, ItemClassification.progression), | ||
"Middle Ring": ItemData("Items", 0x30206A, ItemClassification.progression), | ||
"Bucket": ItemData("Items", 0x30206B, ItemClassification.progression), | ||
"Tulip": ItemData("Items", 0x30206C, ItemClassification.progression), | ||
"Egg Capacity Upgrade": ItemData("Items", 0x30206D, ItemClassification.progression, 5), | ||
"Secret Lens": ItemData("Items", 0x302081, ItemClassification.progression), | ||
|
||
"World 1 Gate": ItemData("Gates", 0x30206E, ItemClassification.progression), | ||
"World 2 Gate": ItemData("Gates", 0x30206F, ItemClassification.progression), | ||
"World 3 Gate": ItemData("Gates", 0x302070, ItemClassification.progression), | ||
"World 4 Gate": ItemData("Gates", 0x302071, ItemClassification.progression), | ||
"World 5 Gate": ItemData("Gates", 0x302072, ItemClassification.progression), | ||
"World 6 Gate": ItemData("Gates", 0x302073, ItemClassification.progression), | ||
|
||
"Extra 1": ItemData("Panels", 0x302074, ItemClassification.progression), | ||
"Extra 2": ItemData("Panels", 0x302075, ItemClassification.progression), | ||
"Extra 3": ItemData("Panels", 0x302076, ItemClassification.progression), | ||
"Extra 4": ItemData("Panels", 0x302077, ItemClassification.progression), | ||
"Extra 5": ItemData("Panels", 0x302078, ItemClassification.progression), | ||
"Extra 6": ItemData("Panels", 0x302079, ItemClassification.progression), | ||
"Extra Panels": ItemData("Panels", 0x30207A, ItemClassification.progression), | ||
|
||
"Bonus 1": ItemData("Panels", 0x30207B, ItemClassification.progression), | ||
"Bonus 2": ItemData("Panels", 0x30207C, ItemClassification.progression), | ||
"Bonus 3": ItemData("Panels", 0x30207D, ItemClassification.progression), | ||
"Bonus 4": ItemData("Panels", 0x30207E, ItemClassification.progression), | ||
"Bonus 5": ItemData("Panels", 0x30207F, ItemClassification.progression), | ||
"Bonus 6": ItemData("Panels", 0x302080, ItemClassification.progression), | ||
"Bonus Panels": ItemData("Panels", 0x302082, ItemClassification.progression), | ||
|
||
"Anytime Egg": ItemData("Consumable", 0x302083, ItemClassification.useful, 0), | ||
"Anywhere Pow": ItemData("Consumable", 0x302084, ItemClassification.filler, 0), | ||
"Winged Cloud Maker": ItemData("Consumable", 0x302085, ItemClassification.filler, 0), | ||
"Pocket Melon": ItemData("Consumable", 0x302086, ItemClassification.filler, 0), | ||
"Pocket Fire Melon": ItemData("Consumable", 0x302087, ItemClassification.filler, 0), | ||
"Pocket Ice Melon": ItemData("Consumable", 0x302088, ItemClassification.filler, 0), | ||
"Magnifying Glass": ItemData("Consumable", 0x302089, ItemClassification.filler, 0), | ||
"+10 Stars": ItemData("Consumable", 0x30208A, ItemClassification.useful, 0), | ||
"+20 Stars": ItemData("Consumable", 0x30208B, ItemClassification.useful, 0), | ||
"1-Up": ItemData("Lives", 0x30208C, ItemClassification.filler, 0), | ||
"2-Up": ItemData("Lives", 0x30208D, ItemClassification.filler, 0), | ||
"3-Up": ItemData("Lives", 0x30208E, ItemClassification.filler, 0), | ||
"10-Up": ItemData("Lives", 0x30208F, ItemClassification.filler, 5), | ||
"Bonus Consumables": ItemData("Events", None, ItemClassification.progression, 0), | ||
"Bandit Consumables": ItemData("Events", None, ItemClassification.progression, 0), | ||
"Bandit Watermelons": ItemData("Events", None, ItemClassification.progression, 0), | ||
|
||
"Fuzzy Trap": ItemData("Traps", 0x302090, ItemClassification.trap, 0), | ||
"Reversal Trap": ItemData("Traps", 0x302091, ItemClassification.trap, 0), | ||
"Darkness Trap": ItemData("Traps", 0x302092, ItemClassification.trap, 0), | ||
"Freeze Trap": ItemData("Traps", 0x302093, ItemClassification.trap, 0), | ||
|
||
"Boss Clear": ItemData("Events", None, ItemClassification.progression, 0), | ||
"Piece of Luigi": ItemData("Items", 0x302095, ItemClassification.progression, 0), | ||
"Saved Baby Luigi": ItemData("Events", None, ItemClassification.progression, 0) | ||
} | ||
|
||
filler_items: Tuple[str, ...] = ( | ||
"Anytime Egg", | ||
"Anywhere Pow", | ||
"Winged Cloud Maker", | ||
"Pocket Melon", | ||
"Pocket Fire Melon", | ||
"Pocket Ice Melon", | ||
"Magnifying Glass", | ||
"+10 Stars", | ||
"+20 Stars", | ||
"1-Up", | ||
"2-Up", | ||
"3-Up" | ||
) | ||
|
||
trap_items: Tuple[str, ...] = ( | ||
"Fuzzy Trap", | ||
"Reversal Trap", | ||
"Darkness Trap", | ||
"Freeze Trap" | ||
) | ||
|
||
def get_item_names_per_category() -> Dict[str, Set[str]]: | ||
categories: Dict[str, Set[str]] = {} | ||
|
||
for name, data in item_table.items(): | ||
if data.category != "Events": | ||
categories.setdefault(data.category, set()).add(name) | ||
|
||
return categories |
Oops, something went wrong.