Skip to content

Commit 355223b

Browse files
PinkSwitchSilvrisAlchavNewSoupViExempt-Medic
authored
Yoshi's Island: Implement New Game (#2141)
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
1 parent aaa3472 commit 355223b

16 files changed

+4559
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Currently, the following games are supported:
6464
* Zork Grand Inquisitor
6565
* Castlevania 64
6666
* A Short Hike
67+
* Yoshi's Island
6768

6869
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
6970
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

docs/CODEOWNERS

+3
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@
191191
# The Witness
192192
/worlds/witness/ @NewSoupVi @blastron
193193

194+
# Yoshi's Island
195+
/worlds/yoshisisland/ @PinkSwitch
196+
194197
# Zillion
195198
/worlds/zillion/ @beauxq
196199

inno_setup.iss

+5
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Arc
189189
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
190190
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";
191191

192+
Root: HKCR; Subkey: ".apyi"; ValueData: "{#MyAppName}yipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
193+
Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archipelago Yoshi's Island Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
194+
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
195+
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
196+
192197
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
193198
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
194199
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";

worlds/yoshisisland/Client.py

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import logging
2+
import struct
3+
import typing
4+
import time
5+
from struct import pack
6+
7+
from NetUtils import ClientStatus, color
8+
from worlds.AutoSNIClient import SNIClient
9+
10+
if typing.TYPE_CHECKING:
11+
from SNIClient import SNIContext
12+
13+
snes_logger = logging.getLogger("SNES")
14+
15+
ROM_START = 0x000000
16+
WRAM_START = 0xF50000
17+
WRAM_SIZE = 0x20000
18+
SRAM_START = 0xE00000
19+
20+
YOSHISISLAND_ROMHASH_START = 0x007FC0
21+
ROMHASH_SIZE = 0x15
22+
23+
ITEMQUEUE_HIGH = WRAM_START + 0x1465
24+
ITEM_RECEIVED = WRAM_START + 0x1467
25+
DEATH_RECEIVED = WRAM_START + 0x7E23B0
26+
GAME_MODE = WRAM_START + 0x0118
27+
YOSHI_STATE = SRAM_START + 0x00AC
28+
DEATHLINK_ADDR = ROM_START + 0x06FC8C
29+
DEATHMUSIC_FLAG = WRAM_START + 0x004F
30+
DEATHFLAG = WRAM_START + 0x00DB
31+
DEATHLINKRECV = WRAM_START + 0x00E0
32+
GOALFLAG = WRAM_START + 0x14B6
33+
34+
VALID_GAME_STATES = [0x0F, 0x10, 0x2C]
35+
36+
37+
class YoshisIslandSNIClient(SNIClient):
38+
game = "Yoshi's Island"
39+
40+
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
41+
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
42+
game_state = await snes_read(ctx, GAME_MODE, 0x1)
43+
if game_state[0] != 0x0F:
44+
return
45+
46+
yoshi_state = await snes_read(ctx, YOSHI_STATE, 0x1)
47+
if yoshi_state[0] != 0x00:
48+
return
49+
50+
snes_buffered_write(ctx, WRAM_START + 0x026A, bytes([0x01]))
51+
snes_buffered_write(ctx, WRAM_START + 0x00E0, bytes([0x01]))
52+
await snes_flush_writes(ctx)
53+
ctx.death_state = DeathState.dead
54+
ctx.last_death_link = time.time()
55+
56+
async def validate_rom(self, ctx: "SNIContext") -> bool:
57+
from SNIClient import snes_read
58+
59+
rom_name = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE)
60+
if rom_name is None or rom_name[:7] != b"YOSHIAP":
61+
return False
62+
63+
ctx.game = self.game
64+
ctx.items_handling = 0b111 # remote items
65+
ctx.rom = rom_name
66+
67+
death_link = await snes_read(ctx, DEATHLINK_ADDR, 1)
68+
if death_link:
69+
await ctx.update_death_link(bool(death_link[0] & 0b1))
70+
return True
71+
72+
async def game_watcher(self, ctx: "SNIContext") -> None:
73+
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
74+
75+
game_mode = await snes_read(ctx, GAME_MODE, 0x1)
76+
item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1)
77+
game_music = await snes_read(ctx, DEATHMUSIC_FLAG, 0x1)
78+
goal_flag = await snes_read(ctx, GOALFLAG, 0x1)
79+
80+
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
81+
death_flag = await snes_read(ctx, DEATHFLAG, 0x1)
82+
deathlink_death = await snes_read(ctx, DEATHLINKRECV, 0x1)
83+
currently_dead = (game_music[0] == 0x07 or game_mode[0] == 0x12 or
84+
(death_flag[0] == 0x00 and game_mode[0] == 0x11)) and deathlink_death[0] == 0x00
85+
await ctx.handle_deathlink_state(currently_dead)
86+
87+
if game_mode is None:
88+
return
89+
elif goal_flag[0] != 0x00:
90+
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
91+
ctx.finished_game = True
92+
elif game_mode[0] not in VALID_GAME_STATES:
93+
return
94+
elif item_received[0] > 0x00:
95+
return
96+
97+
from .Rom import item_values
98+
rom = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE)
99+
if rom != ctx.rom:
100+
ctx.rom = None
101+
return
102+
103+
new_checks = []
104+
from .Rom import location_table
105+
106+
location_ram_data = await snes_read(ctx, WRAM_START + 0x1440, 0x80)
107+
for loc_id, loc_data in location_table.items():
108+
if loc_id not in ctx.locations_checked:
109+
data = location_ram_data[loc_data[0] - 0x1440]
110+
masked_data = data & (1 << loc_data[1])
111+
bit_set = masked_data != 0
112+
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
113+
if bit_set != invert_bit:
114+
new_checks.append(loc_id)
115+
116+
for new_check_id in new_checks:
117+
ctx.locations_checked.add(new_check_id)
118+
location = ctx.location_names[new_check_id]
119+
total_locations = len(ctx.missing_locations) + len(ctx.checked_locations)
120+
snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})")
121+
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}])
122+
123+
recv_count = await snes_read(ctx, ITEMQUEUE_HIGH, 2)
124+
recv_index = struct.unpack("H", recv_count)[0]
125+
if recv_index < len(ctx.items_received):
126+
item = ctx.items_received[recv_index]
127+
recv_index += 1
128+
logging.info("Received %s from %s (%s) (%d/%d in list)" % (
129+
color(ctx.item_names[item.item], "red", "bold"),
130+
color(ctx.player_names[item.player], "yellow"),
131+
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
132+
133+
snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index))
134+
if item.item in item_values:
135+
item_count = await snes_read(ctx, WRAM_START + item_values[item.item][0], 0x1)
136+
increment = item_values[item.item][1]
137+
new_item_count = item_count[0]
138+
if increment > 1:
139+
new_item_count = increment
140+
else:
141+
new_item_count += increment
142+
143+
snes_buffered_write(ctx, WRAM_START + item_values[item.item][0], bytes([new_item_count]))
144+
await snes_flush_writes(ctx)

worlds/yoshisisland/Items.py

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from typing import Dict, Set, Tuple, NamedTuple, Optional
2+
from BaseClasses import ItemClassification
3+
4+
class ItemData(NamedTuple):
5+
category: str
6+
code: Optional[int]
7+
classification: ItemClassification
8+
amount: Optional[int] = 1
9+
10+
item_table: Dict[str, ItemData] = {
11+
"! Switch": ItemData("Items", 0x302050, ItemClassification.progression),
12+
"Dashed Platform": ItemData("Items", 0x302051, ItemClassification.progression),
13+
"Dashed Stairs": ItemData("Items", 0x302052, ItemClassification.progression),
14+
"Beanstalk": ItemData("Items", 0x302053, ItemClassification.progression),
15+
"Helicopter Morph": ItemData("Morphs", 0x302054, ItemClassification.progression),
16+
"Spring Ball": ItemData("Items", 0x302055, ItemClassification.progression),
17+
"Large Spring Ball": ItemData("Items", 0x302056, ItemClassification.progression),
18+
"Arrow Wheel": ItemData("Items", 0x302057, ItemClassification.progression),
19+
"Vanishing Arrow Wheel": ItemData("Items", 0x302058, ItemClassification.progression),
20+
"Mole Tank Morph": ItemData("Morphs", 0x302059, ItemClassification.progression),
21+
"Watermelon": ItemData("Items", 0x30205A, ItemClassification.progression),
22+
"Ice Melon": ItemData("Items", 0x30205B, ItemClassification.progression),
23+
"Fire Melon": ItemData("Items", 0x30205C, ItemClassification.progression),
24+
"Super Star": ItemData("Items", 0x30205D, ItemClassification.progression),
25+
"Car Morph": ItemData("Morphs", 0x30205E, ItemClassification.progression),
26+
"Flashing Eggs": ItemData("Items", 0x30205F, ItemClassification.progression),
27+
"Giant Eggs": ItemData("Items", 0x302060, ItemClassification.progression),
28+
"Egg Launcher": ItemData("Items", 0x302061, ItemClassification.progression),
29+
"Egg Plant": ItemData("Items", 0x302062, ItemClassification.progression),
30+
"Submarine Morph": ItemData("Morphs", 0x302063, ItemClassification.progression),
31+
"Chomp Rock": ItemData("Items", 0x302064, ItemClassification.progression),
32+
"Poochy": ItemData("Items", 0x302065, ItemClassification.progression),
33+
"Platform Ghost": ItemData("Items", 0x302066, ItemClassification.progression),
34+
"Skis": ItemData("Items", 0x302067, ItemClassification.progression),
35+
"Train Morph": ItemData("Morphs", 0x302068, ItemClassification.progression),
36+
"Key": ItemData("Items", 0x302069, ItemClassification.progression),
37+
"Middle Ring": ItemData("Items", 0x30206A, ItemClassification.progression),
38+
"Bucket": ItemData("Items", 0x30206B, ItemClassification.progression),
39+
"Tulip": ItemData("Items", 0x30206C, ItemClassification.progression),
40+
"Egg Capacity Upgrade": ItemData("Items", 0x30206D, ItemClassification.progression, 5),
41+
"Secret Lens": ItemData("Items", 0x302081, ItemClassification.progression),
42+
43+
"World 1 Gate": ItemData("Gates", 0x30206E, ItemClassification.progression),
44+
"World 2 Gate": ItemData("Gates", 0x30206F, ItemClassification.progression),
45+
"World 3 Gate": ItemData("Gates", 0x302070, ItemClassification.progression),
46+
"World 4 Gate": ItemData("Gates", 0x302071, ItemClassification.progression),
47+
"World 5 Gate": ItemData("Gates", 0x302072, ItemClassification.progression),
48+
"World 6 Gate": ItemData("Gates", 0x302073, ItemClassification.progression),
49+
50+
"Extra 1": ItemData("Panels", 0x302074, ItemClassification.progression),
51+
"Extra 2": ItemData("Panels", 0x302075, ItemClassification.progression),
52+
"Extra 3": ItemData("Panels", 0x302076, ItemClassification.progression),
53+
"Extra 4": ItemData("Panels", 0x302077, ItemClassification.progression),
54+
"Extra 5": ItemData("Panels", 0x302078, ItemClassification.progression),
55+
"Extra 6": ItemData("Panels", 0x302079, ItemClassification.progression),
56+
"Extra Panels": ItemData("Panels", 0x30207A, ItemClassification.progression),
57+
58+
"Bonus 1": ItemData("Panels", 0x30207B, ItemClassification.progression),
59+
"Bonus 2": ItemData("Panels", 0x30207C, ItemClassification.progression),
60+
"Bonus 3": ItemData("Panels", 0x30207D, ItemClassification.progression),
61+
"Bonus 4": ItemData("Panels", 0x30207E, ItemClassification.progression),
62+
"Bonus 5": ItemData("Panels", 0x30207F, ItemClassification.progression),
63+
"Bonus 6": ItemData("Panels", 0x302080, ItemClassification.progression),
64+
"Bonus Panels": ItemData("Panels", 0x302082, ItemClassification.progression),
65+
66+
"Anytime Egg": ItemData("Consumable", 0x302083, ItemClassification.useful, 0),
67+
"Anywhere Pow": ItemData("Consumable", 0x302084, ItemClassification.filler, 0),
68+
"Winged Cloud Maker": ItemData("Consumable", 0x302085, ItemClassification.filler, 0),
69+
"Pocket Melon": ItemData("Consumable", 0x302086, ItemClassification.filler, 0),
70+
"Pocket Fire Melon": ItemData("Consumable", 0x302087, ItemClassification.filler, 0),
71+
"Pocket Ice Melon": ItemData("Consumable", 0x302088, ItemClassification.filler, 0),
72+
"Magnifying Glass": ItemData("Consumable", 0x302089, ItemClassification.filler, 0),
73+
"+10 Stars": ItemData("Consumable", 0x30208A, ItemClassification.useful, 0),
74+
"+20 Stars": ItemData("Consumable", 0x30208B, ItemClassification.useful, 0),
75+
"1-Up": ItemData("Lives", 0x30208C, ItemClassification.filler, 0),
76+
"2-Up": ItemData("Lives", 0x30208D, ItemClassification.filler, 0),
77+
"3-Up": ItemData("Lives", 0x30208E, ItemClassification.filler, 0),
78+
"10-Up": ItemData("Lives", 0x30208F, ItemClassification.filler, 5),
79+
"Bonus Consumables": ItemData("Events", None, ItemClassification.progression, 0),
80+
"Bandit Consumables": ItemData("Events", None, ItemClassification.progression, 0),
81+
"Bandit Watermelons": ItemData("Events", None, ItemClassification.progression, 0),
82+
83+
"Fuzzy Trap": ItemData("Traps", 0x302090, ItemClassification.trap, 0),
84+
"Reversal Trap": ItemData("Traps", 0x302091, ItemClassification.trap, 0),
85+
"Darkness Trap": ItemData("Traps", 0x302092, ItemClassification.trap, 0),
86+
"Freeze Trap": ItemData("Traps", 0x302093, ItemClassification.trap, 0),
87+
88+
"Boss Clear": ItemData("Events", None, ItemClassification.progression, 0),
89+
"Piece of Luigi": ItemData("Items", 0x302095, ItemClassification.progression, 0),
90+
"Saved Baby Luigi": ItemData("Events", None, ItemClassification.progression, 0)
91+
}
92+
93+
filler_items: Tuple[str, ...] = (
94+
"Anytime Egg",
95+
"Anywhere Pow",
96+
"Winged Cloud Maker",
97+
"Pocket Melon",
98+
"Pocket Fire Melon",
99+
"Pocket Ice Melon",
100+
"Magnifying Glass",
101+
"+10 Stars",
102+
"+20 Stars",
103+
"1-Up",
104+
"2-Up",
105+
"3-Up"
106+
)
107+
108+
trap_items: Tuple[str, ...] = (
109+
"Fuzzy Trap",
110+
"Reversal Trap",
111+
"Darkness Trap",
112+
"Freeze Trap"
113+
)
114+
115+
def get_item_names_per_category() -> Dict[str, Set[str]]:
116+
categories: Dict[str, Set[str]] = {}
117+
118+
for name, data in item_table.items():
119+
if data.category != "Events":
120+
categories.setdefault(data.category, set()).add(name)
121+
122+
return categories

0 commit comments

Comments
 (0)