SMW: v2.0 Content Update (#2762)

Changelog:

Features:
- New optional Location Checks
  - 3-Up Moons
  - Hidden 1-Ups
  - Bonus Blocks
  - Blocksanity
    - All blocks that contain coins or items are included, with the exception of:
      - Blocks in Top Secret Area & Front Door/Bowser Castle
      - Blocks that are unreachable without glitches/unreasonable movement
- New Items
  - Special Zone Clear
  - New Filler Items
    - 1 Coin
    - 5 Coins
    - 10 Coins
    - 50 Coins
  - New Trap Items
    - Reverse Trap
    - Thwimp Trap
- SFX Shuffle
- Palette Shuffle Overhaul
  - New Curated Palette can now be used for the Overworld and Level Palette Shuffle options
  - Foreground and Background Shuffle options have been merged into a single setting
- Max possible Yoshi Egg value is 255
  - UI in-game is updated to handle 3-digits
  - New `Display Received Item Popups` option: `progression_minus_yoshi_eggs`

Quality of Life:
- In-Game Indicators are now displayed on the map screen for location checks and received items
- In-level sprites are displayed upon receiving certain items
- The Camera Scroll unlocking is now only enabled on levels where it needs to be
- SMW can now handle receiving more than 255 items
- Significant World Code cleanup
  - New Options API
  - Removal of `world: MultiWorld` across the world
- The PopTracker pack now has tabs for every level/sublevel, and can automatically swap tabs while playing if connected to the server

Bug Fixes:
- Several logic tweaks/fixes

"Major credit to @TheLX5 for being the driving force for almost all of this update. We've been collaborating on design and polish of the features for the last few months, but all of the heavy lifting was all @TheLX5."
This commit is contained in:
PoryGone
2024-03-12 17:00:13 -04:00
committed by GitHub
parent b6b88070be
commit f8d5fe0e1e
395 changed files with 8433 additions and 775 deletions

View File

@@ -1,5 +1,4 @@
import logging
import asyncio
import time
from NetUtils import ClientStatus, color
@@ -17,11 +16,19 @@ SRAM_START = 0xE00000
SMW_ROMHASH_START = 0x7FC0
ROMHASH_SIZE = 0x15
SMW_PROGRESS_DATA = WRAM_START + 0x1F02
SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F
SMW_PATH_DATA = WRAM_START + 0x1EA2
SMW_EVENT_ROM_DATA = ROM_START + 0x2D608
SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70
SMW_PROGRESS_DATA = WRAM_START + 0x1F02
SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F
SMW_PATH_DATA = WRAM_START + 0x1EA2
SMW_EVENT_ROM_DATA = ROM_START + 0x2D608
SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70
SMW_MOON_DATA = WRAM_START + 0x1FEE
SMW_HIDDEN_1UP_DATA = WRAM_START + 0x1F3C
SMW_BONUS_BLOCK_DATA = WRAM_START + 0x1A000
SMW_BLOCKSANITY_DATA = WRAM_START + 0x1A400
SMW_BLOCKSANITY_FLAGS = WRAM_START + 0x1A010
SMW_LEVEL_CLEAR_FLAGS = WRAM_START + 0x1A200
SMW_SPECIAL_WORLD_CLEAR = WRAM_START + 0x1F1E
SMW_GOAL_DATA = ROM_START + 0x01BFA0
SMW_REQUIRED_BOSSES_DATA = ROM_START + 0x01BFA1
@@ -31,22 +38,32 @@ SMW_RECEIVE_MSG_DATA = ROM_START + 0x01BFA4
SMW_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x01BFA5
SMW_DRAGON_COINS_ACTIVE_ADDR = ROM_START + 0x01BFA6
SMW_SWAMP_DONUT_GH_ADDR = ROM_START + 0x01BFA7
SMW_MOON_ACTIVE_ADDR = ROM_START + 0x01BFA8
SMW_HIDDEN_1UP_ACTIVE_ADDR = ROM_START + 0x01BFA9
SMW_BONUS_BLOCK_ACTIVE_ADDR = ROM_START + 0x01BFAA
SMW_BLOCKSANITY_ACTIVE_ADDR = ROM_START + 0x01BFAB
SMW_GAME_STATE_ADDR = WRAM_START + 0x100
SMW_MARIO_STATE_ADDR = WRAM_START + 0x71
SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B
SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC
SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF
SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426
SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48
SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24
SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26
SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E
SMW_SFX_ADDR = WRAM_START + 0x1DFC
SMW_PAUSE_ADDR = WRAM_START + 0x13D4
SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391
SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x1F2B
SMW_GAME_STATE_ADDR = WRAM_START + 0x100
SMW_MARIO_STATE_ADDR = WRAM_START + 0x71
SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B
SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC
SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF
SMW_CURRENT_SUBLEVEL_ADDR = WRAM_START + 0x10B
SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426
SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48
SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24
SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26
SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E
SMW_SFX_ADDR = WRAM_START + 0x1DFC
SMW_PAUSE_ADDR = WRAM_START + 0x13D4
SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391
SMW_ACTIVE_THWIMP_ADDR = WRAM_START + 0x0F3C
SMW_GOAL_ITEM_COUNT = WRAM_START + 0x1A01E
SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x01F2B
SMW_BLOCKSANITY_BLOCK_COUNT = 582
SMW_GOAL_LEVELS = [0x28, 0x31, 0x32]
SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D]
@@ -115,6 +132,9 @@ class SMWSNIClient(SNIClient):
if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1))
if ctx.rom != rom_name:
ctx.current_sublevel_value = 0
ctx.rom = rom_name
return True
@@ -176,6 +196,11 @@ class SMWSNIClient(SNIClient):
self.trap_queue.append((trap_item, trap_msg))
def should_show_message(self, ctx, next_item):
return ctx.receive_option == 1 or \
(ctx.receive_option == 2 and ((next_item.flags & 1) != 0)) or \
(ctx.receive_option == 3 and ((next_item.flags & 1) != 0 and next_item.item != 0xBC0002))
async def handle_trap_queue(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
@@ -217,6 +242,13 @@ class SMWSNIClient(SNIClient):
self.add_trap_to_queue(next_trap, message)
return
else:
if next_trap.item == 0xBC001D:
# Special case thwimp trap
# Do not fire if the previous thwimp hasn't reached the player's Y pos
active_thwimp = await snes_read(ctx, SMW_ACTIVE_THWIMP_ADDR, 0x1)
if active_thwimp[0] != 0xFF:
self.add_trap_to_queue(next_trap, message)
return
verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
if verify_game_state[0] == 0x14 and len(trap_rom_data[next_trap.item]) > 2:
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([trap_rom_data[next_trap.item][2]]))
@@ -236,13 +268,14 @@ class SMWSNIClient(SNIClient):
if active_boss[0] != 0x00:
return
if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((next_trap.flags & 1) != 0)):
if self.should_show_message(ctx, next_trap):
self.add_message_to_queue(message)
async def game_watcher(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1)
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
if game_state is None:
@@ -259,6 +292,7 @@ class SMWSNIClient(SNIClient):
elif game_state[0] < 0x0B:
# We haven't loaded a save file
ctx.message_queue = []
ctx.current_sublevel_value = 0
return
elif mario_state[0] in SMW_INVALID_MARIO_STATES:
# Mario can't come to the phone right now
@@ -304,8 +338,18 @@ class SMWSNIClient(SNIClient):
progress_data = bytearray(await snes_read(ctx, SMW_PROGRESS_DATA, 0x0F))
dragon_coins_data = bytearray(await snes_read(ctx, SMW_DRAGON_COINS_DATA, 0x0C))
dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1)
from worlds.smw.Rom import item_rom_data, ability_rom_data, trap_rom_data
from worlds.smw.Levels import location_id_to_level_id, level_info_dict
moon_data = bytearray(await snes_read(ctx, SMW_MOON_DATA, 0x0C))
moon_active = await snes_read(ctx, SMW_MOON_ACTIVE_ADDR, 0x1)
hidden_1up_data = bytearray(await snes_read(ctx, SMW_HIDDEN_1UP_DATA, 0x0C))
hidden_1up_active = await snes_read(ctx, SMW_HIDDEN_1UP_ACTIVE_ADDR, 0x1)
bonus_block_data = bytearray(await snes_read(ctx, SMW_BONUS_BLOCK_DATA, 0x0C))
bonus_block_active = await snes_read(ctx, SMW_BONUS_BLOCK_ACTIVE_ADDR, 0x1)
blocksanity_data = bytearray(await snes_read(ctx, SMW_BLOCKSANITY_DATA, SMW_BLOCKSANITY_BLOCK_COUNT))
blocksanity_flags = bytearray(await snes_read(ctx, SMW_BLOCKSANITY_FLAGS, 0xC))
blocksanity_active = await snes_read(ctx, SMW_BLOCKSANITY_ACTIVE_ADDR, 0x1)
level_clear_flags = bytearray(await snes_read(ctx, SMW_LEVEL_CLEAR_FLAGS, 0x60))
from worlds.smw.Rom import item_rom_data, ability_rom_data, trap_rom_data, icon_rom_data
from worlds.smw.Levels import location_id_to_level_id, level_info_dict, level_blocks_data
from worlds import AutoWorldRegister
for loc_name, level_data in location_id_to_level_id.items():
loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name]
@@ -327,6 +371,54 @@ class SMWSNIClient(SNIClient):
if bit_set:
new_checks.append(loc_id)
elif level_data[1] == 3:
# Moon Check
if not moon_active or moon_active[0] == 0:
continue
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = moon_data[progress_byte]
masked_data = data & (1 << progress_bit)
bit_set = (masked_data != 0)
if bit_set:
new_checks.append(loc_id)
elif level_data[1] == 4:
# Hidden 1-Up Check
if not hidden_1up_active or hidden_1up_active[0] == 0:
continue
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = hidden_1up_data[progress_byte]
masked_data = data & (1 << progress_bit)
bit_set = (masked_data != 0)
if bit_set:
new_checks.append(loc_id)
elif level_data[1] == 5:
# Bonus Block Check
if not bonus_block_active or bonus_block_active[0] == 0:
continue
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = bonus_block_data[progress_byte]
masked_data = data & (1 << progress_bit)
bit_set = (masked_data != 0)
if bit_set:
new_checks.append(loc_id)
elif level_data[1] >= 100:
if not blocksanity_active or blocksanity_active[0] == 0:
continue
block_index = level_data[1] - 100
if blocksanity_data[block_index] != 0:
new_checks.append(loc_id)
else:
event_id_value = event_id + level_data[1]
@@ -360,12 +452,48 @@ class SMWSNIClient(SNIClient):
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
# Send Current Room for Tracker
current_sublevel_data = await snes_read(ctx, SMW_CURRENT_SUBLEVEL_ADDR, 2)
current_sublevel_value = current_sublevel_data[0] + (current_sublevel_data[1] << 8)
if game_state[0] != 0x14:
current_sublevel_value = 0
if ctx.current_sublevel_value != current_sublevel_value:
ctx.current_sublevel_value = current_sublevel_value
# Send level id data to tracker
await ctx.send_msgs(
[
{
"cmd": "Set",
"key": f"smw_curlevelid_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [
{
"operation": "replace",
"value": ctx.current_sublevel_value,
}
],
}
]
)
if game_state[0] != 0x14:
# Don't receive items or collect locations outside of in-level mode
ctx.current_sublevel_value = 0
return
if boss_state[0] in SMW_BOSS_STATES:
# Don't receive items or collect locations inside boss battles
return
recv_count = await snes_read(ctx, SMW_RECV_PROGRESS_ADDR, 1)
recv_index = recv_count[0]
recv_count = await snes_read(ctx, SMW_RECV_PROGRESS_ADDR, 2)
if recv_count is None:
# Add a small failsafe in case we get a None. Other SNI games do this...
return
recv_index = recv_count[0] | (recv_count[1] << 8)
if recv_index < len(ctx.items_received):
item = ctx.items_received[recv_index]
@@ -375,7 +503,7 @@ class SMWSNIClient(SNIClient):
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((item.flags & 1) != 0)):
if self.should_show_message(ctx, item):
if item.item != 0xBC0012 and item.item not in trap_rom_data:
# Don't send messages for Boss Tokens
item_name = ctx.item_names[item.item]
@@ -384,7 +512,7 @@ class SMWSNIClient(SNIClient):
receive_message = generate_received_text(item_name, player_name)
self.add_message_to_queue(receive_message)
snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index]))
snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF]))
if item.item in trap_rom_data:
item_name = ctx.item_names[item.item]
player_name = ctx.player_names[item.player]
@@ -405,6 +533,15 @@ class SMWSNIClient(SNIClient):
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([item_rom_data[item.item][2]]))
snes_buffered_write(ctx, WRAM_START + item_rom_data[item.item][0], bytes([new_item_count]))
elif item.item in icon_rom_data:
queue_addr = await snes_read(ctx, WRAM_START + icon_rom_data[item.item][0], 2)
queue_addr = queue_addr[0] + (queue_addr[1] << 8)
queue_addr += 1
snes_buffered_write(ctx, WRAM_START + icon_rom_data[item.item][0], bytes([queue_addr&0xFF, (queue_addr>>8)&0xFF]))
if (goal[0] == 0 and item.item == 0xBC0012) or (goal[0] == 1 and item.item == 0xBC0002):
goal_item_count = await snes_read(ctx, SMW_GOAL_ITEM_COUNT, 1)
snes_buffered_write(ctx, SMW_GOAL_ITEM_COUNT, bytes([goal_item_count[0] + 1]))
elif item.item in ability_rom_data:
# Handle Upgrades
for rom_data in ability_rom_data[item.item]:
@@ -449,6 +586,12 @@ class SMWSNIClient(SNIClient):
path_data = bytearray(await snes_read(ctx, SMW_PATH_DATA, 0x60))
donut_gh_swapped = await snes_read(ctx, SMW_SWAMP_DONUT_GH_ADDR, 0x1)
new_dragon_coin = False
new_moon = False
new_hidden_1up = False
new_bonus_block = False
new_blocksanity = False
new_blocksanity_flags = False
for loc_id in ctx.checked_locations:
if loc_id not in ctx.locations_checked:
ctx.locations_checked.add(loc_id)
@@ -470,10 +613,64 @@ class SMWSNIClient(SNIClient):
dragon_coins_data[progress_byte] = new_data
new_dragon_coin = True
elif level_data[1] == 3:
# Moon Check
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = moon_data[progress_byte]
new_data = data | (1 << progress_bit)
moon_data[progress_byte] = new_data
new_moon = True
elif level_data[1] == 4:
# Hidden 1-Up Check
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = hidden_1up_data[progress_byte]
new_data = data | (1 << progress_bit)
hidden_1up_data[progress_byte] = new_data
new_hidden_1up = True
elif level_data[1] == 5:
# Bonus block prize Check
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = bonus_block_data[progress_byte]
new_data = data | (1 << progress_bit)
bonus_block_data[progress_byte] = new_data
new_bonus_block = True
elif level_data[1] >= 100:
# Blocksanity flag Check
block_index = level_data[1] - 100
blocksanity_data[block_index] = 1
new_blocksanity = True
# All blocksanity blocks flag
new_blocksanity_flags = True
for block_id in level_blocks_data[level_data[0]]:
if blocksanity_data[block_id] != 1:
new_blocksanity_flags = False
continue
if new_blocksanity_flags is True:
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
data = blocksanity_flags[progress_byte]
new_data = data | (1 << progress_bit)
blocksanity_flags[progress_byte] = new_data
else:
if level_data[0] in SMW_UNCOLLECTABLE_LEVELS:
continue
# Handle map indicators
flag = 1 if level_data[1] == 0 else 2
level_clear_flags[level_data[0]] |= flag
event_id = event_data[level_data[0]]
event_id_value = event_id + level_data[1]
@@ -514,7 +711,18 @@ class SMWSNIClient(SNIClient):
if new_dragon_coin:
snes_buffered_write(ctx, SMW_DRAGON_COINS_DATA, bytes(dragon_coins_data))
if new_moon:
snes_buffered_write(ctx, SMW_MOON_DATA, bytes(moon_data))
if new_hidden_1up:
snes_buffered_write(ctx, SMW_HIDDEN_1UP_DATA, bytes(hidden_1up_data))
if new_bonus_block:
snes_buffered_write(ctx, SMW_BONUS_BLOCK_DATA, bytes(bonus_block_data))
if new_blocksanity:
snes_buffered_write(ctx, SMW_BLOCKSANITY_DATA, bytes(blocksanity_data))
if new_blocksanity_flags:
snes_buffered_write(ctx, SMW_BLOCKSANITY_FLAGS, bytes(blocksanity_flags))
if new_events > 0:
snes_buffered_write(ctx, SMW_LEVEL_CLEAR_FLAGS, bytes(level_clear_flags))
snes_buffered_write(ctx, SMW_PROGRESS_DATA, bytes(progress_data))
snes_buffered_write(ctx, SMW_PATH_DATA, bytes(path_data))
old_events = await snes_read(ctx, SMW_NUM_EVENTS_ADDR, 0x1)