Skip to content

Commit

Permalink
Yoshi's Island: Implement New Game (#2141)
Browse files Browse the repository at this point in the history
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
5 people authored Mar 22, 2024
1 parent aaa3472 commit 355223b
Show file tree
Hide file tree
Showing 16 changed files with 4,559 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Currently, the following games are supported:
* Zork Grand Inquisitor
* Castlevania 64
* A Short Hike
* Yoshi's Island

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
Expand Down
3 changes: 3 additions & 0 deletions docs/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@
# The Witness
/worlds/witness/ @NewSoupVi @blastron

# Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch

# Zillion
/worlds/zillion/ @beauxq

Expand Down
5 changes: 5 additions & 0 deletions inno_setup.iss
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";

Root: HKCR; Subkey: ".apyi"; ValueData: "{#MyAppName}yipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archipelago Yoshi's Island Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";

Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
Expand Down
144 changes: 144 additions & 0 deletions worlds/yoshisisland/Client.py
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)
122 changes: 122 additions & 0 deletions worlds/yoshisisland/Items.py
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
Loading

0 comments on commit 355223b

Please sign in to comment.