KDL3: Version 2.0.0 (#3323)
* initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * Update Rom.py * convert KDL3 to APPP * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * initial base for local items, need to finish * coo not clean * handle local items for real, appp cleanup * actually make bosses send their locations * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs * add prefill items * fix kine fill error * Update Rom.py * Update Files.py * mypy and softlock fix * Update Gifting.py * mypy phase 1 * fix rare async client bug * Update __init__.py * typing cleanup * fix stone softlock because of the way Kine's Stone works, you can't clear the stone blocks before clearing the burning blocks, so we have to bring Burning from outside * Update Rom.py * Add option groups * Rename to lowercase * finish rename * whoops broke the world * fix animal duplication bug * overhaul filler generation * add Miku flavor * Update gifting.py * fix issues related to max_hs increase * Update test_locations.py * fix boss shuffle not working if level shuffle is disabled * fix bleeding default levels * Update options.py * thought this would print seed * yay bad merges * forgot options too * yeah lets just break generation while at it * this is probably a problem * cap required heart stars * Revert "cap required heart stars" This reverts commit 759efd3e2b14ec2855082de041ac989cb9c5d500. * fix duplication removal placement, deprecated test option * forgot that we need to account for what we place * move location ids * rewrite trap handling * further stage renumber fixes * forgot one more * basic UT support * fix local heart star checks * fix pattern --------- Co-authored-by: beauxq <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
435
worlds/kdl3/client.py
Normal file
435
worlds/kdl3/client.py
Normal file
@@ -0,0 +1,435 @@
|
||||
import logging
|
||||
import struct
|
||||
import time
|
||||
import typing
|
||||
import uuid
|
||||
from struct import unpack, pack
|
||||
from collections import defaultdict
|
||||
import random
|
||||
|
||||
from MultiServer import mark_raw
|
||||
from NetUtils import ClientStatus, color
|
||||
from Utils import async_start
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .locations import boss_locations
|
||||
from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
|
||||
from .client_addrs import consumable_addrs, star_addrs
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIClientCommandProcessor, SNIContext
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
SRAM_1_START = 0xE00000
|
||||
|
||||
# KDL3
|
||||
KDL3_HALKEN = SRAM_1_START + 0x80F0
|
||||
KDL3_NINTEN = SRAM_1_START + 0x8FF0
|
||||
KDL3_ROMNAME = SRAM_1_START + 0x8100
|
||||
KDL3_DEATH_LINK_ADDR = SRAM_1_START + 0x9010
|
||||
KDL3_GOAL_ADDR = SRAM_1_START + 0x9012
|
||||
KDL3_CONSUMABLE_FLAG = SRAM_1_START + 0x9018
|
||||
KDL3_STARS_FLAG = SRAM_1_START + 0x901A
|
||||
KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C
|
||||
KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020
|
||||
KDL3_IS_DEMO = SRAM_1_START + 0x5AD5
|
||||
KDL3_GAME_SAVE = SRAM_1_START + 0x3617
|
||||
KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F
|
||||
KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641
|
||||
KDL3_GAME_STATE = SRAM_1_START + 0x36D0
|
||||
KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF
|
||||
KDL3_KIRBY_HP = SRAM_1_START + 0x39D1
|
||||
KDL3_BOSS_HP = SRAM_1_START + 0x39D5
|
||||
KDL3_STAR_COUNT = SRAM_1_START + 0x39D7
|
||||
KDL3_LIFE_VISUAL = SRAM_1_START + 0x39E3
|
||||
KDL3_HEART_STARS = SRAM_1_START + 0x53A7
|
||||
KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB
|
||||
KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD
|
||||
KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5
|
||||
KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1
|
||||
KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4
|
||||
KDL3_BOSS_BUTCH_STATUS = SRAM_1_START + 0x5EEA
|
||||
KDL3_JUMPING_STATUS = SRAM_1_START + 0x5EF0
|
||||
KDL3_CURRENT_BGM = SRAM_1_START + 0x733E
|
||||
KDL3_SOUND_FX = SRAM_1_START + 0x7F62
|
||||
KDL3_ANIMAL_FRIENDS = SRAM_1_START + 0x8000
|
||||
KDL3_ABILITY_ARRAY = SRAM_1_START + 0x8020
|
||||
KDL3_RECV_COUNT = SRAM_1_START + 0x8050
|
||||
KDL3_HEART_STAR_COUNT = SRAM_1_START + 0x8070
|
||||
KDL3_GOOEY_TRAP = SRAM_1_START + 0x8080
|
||||
KDL3_SLOWNESS_TRAP = SRAM_1_START + 0x8082
|
||||
KDL3_ABILITY_TRAP = SRAM_1_START + 0x8084
|
||||
KDL3_GIFTING_SEND = SRAM_1_START + 0x8086
|
||||
KDL3_COMPLETED_STAGES = SRAM_1_START + 0x8200
|
||||
KDL3_CONSUMABLES = SRAM_1_START + 0xA000
|
||||
KDL3_STARS = SRAM_1_START + 0xB000
|
||||
KDL3_ITEM_QUEUE = SRAM_1_START + 0xC000
|
||||
|
||||
deathlink_messages = defaultdict(lambda: " was defeated.", {
|
||||
0x0200: " was bonked by apples from Whispy Woods.",
|
||||
0x0201: " was out-maneuvered by Acro.",
|
||||
0x0202: " was out-numbered by Pon & Con.",
|
||||
0x0203: " was defeated by Ado's powerful paintings.",
|
||||
0x0204: " was clobbered by King Dedede.",
|
||||
0x0205: " lost their battle against Dark Matter.",
|
||||
0x0300: " couldn't overcome the Boss Butch.",
|
||||
0x0400: " is bad at jumping.",
|
||||
})
|
||||
|
||||
|
||||
@mark_raw
|
||||
def cmd_gift(self: "SNIClientCommandProcessor") -> None:
|
||||
"""Toggles gifting for the current game."""
|
||||
handler = self.ctx.client_handler
|
||||
assert isinstance(handler, KDL3SNIClient)
|
||||
handler.gifting = not handler.gifting
|
||||
self.output(f"Gifting set to {handler.gifting}")
|
||||
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
|
||||
f"{self.ctx.slot}":
|
||||
{
|
||||
"IsOpen": handler.gifting,
|
||||
**kdl3_gifting_options
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
class KDL3SNIClient(SNIClient):
|
||||
game = "Kirby's Dream Land 3"
|
||||
patch_suffix = ".apkdl3"
|
||||
levels: typing.Dict[int, typing.List[int]] = {}
|
||||
consumables: typing.Optional[bool] = None
|
||||
stars: typing.Optional[bool] = None
|
||||
item_queue: typing.List[int] = []
|
||||
initialize_gifting: bool = False
|
||||
gifting: bool = False
|
||||
giftbox_key: str = ""
|
||||
motherbox_key: str = ""
|
||||
client_random: random.Random = random.Random()
|
||||
|
||||
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, KDL3_GAME_STATE, 1)
|
||||
if game_state[0] == 0xFF:
|
||||
return # despite how funny it is, don't try to kill Kirby in a menu
|
||||
|
||||
current_stage = await snes_read(ctx, KDL3_CURRENT_LEVEL, 1)
|
||||
if current_stage[0] == 0x7: # boss stage
|
||||
boss_hp = await snes_read(ctx, KDL3_BOSS_HP, 1)
|
||||
if boss_hp[0] == 0:
|
||||
return # receiving a deathlink after defeating a boss has softlock potential
|
||||
|
||||
current_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
|
||||
if current_hp[0] == 0:
|
||||
return # don't kill Kirby while he's already dead
|
||||
snes_buffered_write(ctx, KDL3_KIRBY_HP, bytes([0x00]))
|
||||
|
||||
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, KDL3_ROMNAME, 0x15)
|
||||
if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3":
|
||||
if "gift" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("gift")
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.rom = rom_name
|
||||
ctx.items_handling = 0b101 # default local items with remote start inventory
|
||||
ctx.allow_collect = True
|
||||
if "gift" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["gift"] = cmd_gift
|
||||
|
||||
death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
|
||||
if death_link:
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled
|
||||
return True
|
||||
|
||||
async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None:
|
||||
from SNIClient import snes_buffered_write, snes_read
|
||||
if len(self.item_queue) > 0:
|
||||
item = self.item_queue.pop()
|
||||
if not in_stage and item & 0xC0:
|
||||
# can't handle this item right now, send it to the back and return to handle the rest
|
||||
self.item_queue.append(item)
|
||||
return
|
||||
ingame_queue = list(unpack("HHHHHHHH", await snes_read(ctx, KDL3_ITEM_QUEUE, 16)))
|
||||
for i in range(len(ingame_queue)):
|
||||
if ingame_queue[i] == 0x00:
|
||||
ingame_queue[i] = item
|
||||
snes_buffered_write(ctx, KDL3_ITEM_QUEUE, pack("HHHHHHHH", *ingame_queue))
|
||||
break
|
||||
else:
|
||||
self.item_queue.append(item) # no more slots, get it next go around
|
||||
|
||||
async def pop_gift(self, ctx: "SNIContext") -> None:
|
||||
if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]:
|
||||
from SNIClient import snes_read, snes_buffered_write
|
||||
key, gift = ctx.stored_data[self.giftbox_key].popitem()
|
||||
await pop_object(ctx, self.giftbox_key, key)
|
||||
# first, special cases
|
||||
traits = [trait["Trait"] for trait in gift["Traits"]]
|
||||
if "Candy" in traits or "Invincible" in traits:
|
||||
# apply invincibility candy
|
||||
self.item_queue.append(0x43)
|
||||
elif "Tomato" in traits or "tomato" in gift["ItemName"].lower():
|
||||
# apply maxim tomato
|
||||
# only want tomatos here, no other vegetable is that good
|
||||
self.item_queue.append(0x42)
|
||||
elif "Life" in traits:
|
||||
# Apply 1-Up
|
||||
self.item_queue.append(0x41)
|
||||
elif "Currency" in traits or "Star" in traits:
|
||||
value = gift["ItemValue"]
|
||||
if value >= 50000:
|
||||
self.item_queue.append(0x46)
|
||||
elif value >= 30000:
|
||||
self.item_queue.append(0x45)
|
||||
else:
|
||||
self.item_queue.append(0x44)
|
||||
elif "Trap" in traits:
|
||||
# find the best trap to apply
|
||||
if "Goo" in traits or "Gel" in traits:
|
||||
self.item_queue.append(0x80)
|
||||
elif "Slow" in traits or "Slowness" in traits:
|
||||
self.item_queue.append(0x81)
|
||||
elif "Eject" in traits or "Removal" in traits:
|
||||
self.item_queue.append(0x82)
|
||||
else:
|
||||
# just deal damage to Kirby
|
||||
kirby_hp = struct.unpack("H", await snes_read(ctx, KDL3_KIRBY_HP, 2))[0]
|
||||
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", max(kirby_hp - 1, 0)))
|
||||
else:
|
||||
# check if it's tasty
|
||||
if any(x in traits for x in ["Consumable", "Food", "Drink", "Heal", "Health"]):
|
||||
# it's tasty!, use quality to decide how much to heal
|
||||
quality = max((trait["Quality"] for trait in gift["Traits"]
|
||||
if trait["Trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"]))
|
||||
quality = min(10, quality * 2)
|
||||
else:
|
||||
# it's not really edible, but he'll eat it anyway
|
||||
quality = self.client_random.choices(range(0, 2), [75, 25])[0]
|
||||
kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
|
||||
gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
|
||||
snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
|
||||
if gooey_hp[0] > 0x00:
|
||||
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality // 2, 8)))
|
||||
snes_buffered_write(ctx, KDL3_KIRBY_HP + 2, struct.pack("H", min(gooey_hp[0] + quality // 2, 8)))
|
||||
else:
|
||||
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10)))
|
||||
|
||||
async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None:
|
||||
assert ctx.slot
|
||||
if gift != 4:
|
||||
gift_base = kdl3_gifts[gift]
|
||||
else:
|
||||
gift_base = kdl3_trap_gifts[self.client_random.randint(0, 3)]
|
||||
most_applicable = -1
|
||||
most_applicable_slot = ctx.slot
|
||||
for slot, info in ctx.stored_data[self.motherbox_key].items():
|
||||
if int(slot) == ctx.slot and len(ctx.stored_data[self.motherbox_key]) > 1:
|
||||
continue
|
||||
desire = len(set(info["DesiredTraits"]).intersection([trait["Trait"] for trait in gift_base["Traits"]]))
|
||||
if desire > most_applicable:
|
||||
most_applicable = desire
|
||||
most_applicable_slot = int(slot)
|
||||
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]:
|
||||
# only send to ourselves if no one else will take it
|
||||
most_applicable_slot = int(slot)
|
||||
# print(most_applicable, most_applicable_slot)
|
||||
item_uuid = uuid.uuid4().hex
|
||||
item = {
|
||||
**gift_base,
|
||||
"ID": item_uuid,
|
||||
"Sender": ctx.player_names[ctx.slot],
|
||||
"Receiver": ctx.player_names[most_applicable_slot],
|
||||
"SenderTeam": ctx.team,
|
||||
"ReceiverTeam": ctx.team, # for the moment
|
||||
"IsRefund": False
|
||||
}
|
||||
# print(item)
|
||||
await update_object(ctx, f"Giftbox;{ctx.team};{most_applicable_slot}", {
|
||||
item_uuid: item,
|
||||
})
|
||||
|
||||
async def game_watcher(self, ctx: "SNIContext") -> None:
|
||||
try:
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
rom = await snes_read(ctx, KDL3_ROMNAME, 0x15)
|
||||
if rom != ctx.rom:
|
||||
ctx.rom = None
|
||||
halken = await snes_read(ctx, KDL3_HALKEN, 6)
|
||||
if halken != b"halken":
|
||||
return
|
||||
ninten = await snes_read(ctx, KDL3_NINTEN, 6)
|
||||
if ninten != b"ninten":
|
||||
return
|
||||
if not ctx.slot:
|
||||
return
|
||||
if not self.initialize_gifting:
|
||||
self.giftbox_key = f"Giftbox;{ctx.team};{ctx.slot}"
|
||||
self.motherbox_key = f"Giftboxes;{ctx.team}"
|
||||
enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
|
||||
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
|
||||
self.initialize_gifting = True
|
||||
# can't check debug anymore, without going and copying the value. might be important later.
|
||||
if not self.levels:
|
||||
self.levels = dict()
|
||||
for i in range(5):
|
||||
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
|
||||
self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little")
|
||||
for idx in range(0, len(level_data), 2)]
|
||||
self.levels[5] = [0x0205, # Hyper Zone
|
||||
0, # MG-5, can't send from here
|
||||
0x0300, # Boss Butch
|
||||
0x0400, # Jumping
|
||||
0, 0, 0]
|
||||
|
||||
if self.consumables is None:
|
||||
consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1)
|
||||
self.consumables = consumables[0] == 0x01
|
||||
if self.stars is None:
|
||||
stars = await snes_read(ctx, KDL3_STARS_FLAG, 1)
|
||||
self.stars = stars[0] == 0x01
|
||||
is_demo = await snes_read(ctx, KDL3_IS_DEMO, 1)
|
||||
# 1 - recording a demo, 2 - playing back recorded, 3+ is a demo
|
||||
if is_demo[0] > 0x00:
|
||||
return
|
||||
current_save = await snes_read(ctx, KDL3_GAME_SAVE, 1)
|
||||
goal = await snes_read(ctx, KDL3_GOAL_ADDR, 1)
|
||||
boss_butch_status = await snes_read(ctx, KDL3_BOSS_BUTCH_STATUS + (current_save[0] * 2), 1)
|
||||
mg5_status = await snes_read(ctx, KDL3_MG5_STATUS + (current_save[0] * 2), 1)
|
||||
jumping_status = await snes_read(ctx, KDL3_JUMPING_STATUS + (current_save[0] * 2), 1)
|
||||
if boss_butch_status[0] == 0xFF:
|
||||
return # save file is not created, ignore
|
||||
if (goal[0] == 0x00 and boss_butch_status[0] == 0x01) \
|
||||
or (goal[0] == 0x01 and boss_butch_status[0] == 0x03) \
|
||||
or (goal[0] == 0x02 and mg5_status[0] == 0x03) \
|
||||
or (goal[0] == 0x03 and jumping_status[0] == 0x03):
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
current_bgm = await snes_read(ctx, KDL3_CURRENT_BGM, 1)
|
||||
if current_bgm[0] in (0x00, 0x21, 0x22, 0x23, 0x25, 0x2A, 0x2B):
|
||||
return # null, title screen, opening, save select, true and false endings
|
||||
game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
|
||||
if "DeathLink" in ctx.tags and game_state[0] == 0x00 and ctx.last_death_link + 1 < time.time():
|
||||
current_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
|
||||
current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0]
|
||||
current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0]
|
||||
currently_dead = current_hp[0] == 0x00
|
||||
message = deathlink_messages[self.levels[current_world][current_level]]
|
||||
await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}")
|
||||
|
||||
recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2)
|
||||
recv_amount = unpack("H", recv_count)[0]
|
||||
if recv_amount < len(ctx.items_received):
|
||||
item = ctx.items_received[recv_amount]
|
||||
recv_amount += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount))
|
||||
item_idx = item.item & 0x00000F
|
||||
if item.item & 0x000070 == 0:
|
||||
self.item_queue.append(item_idx | 0x10)
|
||||
elif item.item & 0x000010 > 0:
|
||||
self.item_queue.append(item_idx | 0x20)
|
||||
elif item.item & 0x000020 > 0:
|
||||
# Positive
|
||||
self.item_queue.append(item_idx | 0x40)
|
||||
elif item.item & 0x000040 > 0:
|
||||
self.item_queue.append(item_idx | 0x80)
|
||||
|
||||
# handle gifts here
|
||||
gifting_status = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
|
||||
if hasattr(ctx, "gifting") and ctx.gifting:
|
||||
if gifting_status[0]:
|
||||
gift = await snes_read(ctx, KDL3_GIFTING_SEND, 0x01)
|
||||
if gift[0]:
|
||||
# we have a gift to send
|
||||
await self.pick_gift_recipient(ctx, gift[0])
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_SEND, bytes([0x00]))
|
||||
else:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01]))
|
||||
else:
|
||||
if gifting_status[0]:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
new_checks = []
|
||||
# level completion status
|
||||
world_unlocks = await snes_read(ctx, KDL3_WORLD_UNLOCK, 1)
|
||||
if world_unlocks[0] > 0x06:
|
||||
return # save is not loaded, ignore
|
||||
stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
|
||||
stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
|
||||
for i in range(30):
|
||||
loc_id = 0x770000 + i
|
||||
if stages[i] == 1 and loc_id not in ctx.checked_locations:
|
||||
new_checks.append(loc_id)
|
||||
elif loc_id in ctx.checked_locations:
|
||||
snes_buffered_write(ctx, KDL3_COMPLETED_STAGES + (i * 2), struct.pack("H", 1))
|
||||
|
||||
# heart star status
|
||||
heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
|
||||
for i in range(5):
|
||||
start_ind = i * 7
|
||||
for j in range(6):
|
||||
level_ind = start_ind + j
|
||||
loc_id = 0x770100 + (6 * i) + j
|
||||
if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
|
||||
new_checks.append(loc_id)
|
||||
elif loc_id in ctx.checked_locations:
|
||||
snes_buffered_write(ctx, KDL3_HEART_STARS + level_ind, bytes([0x01]))
|
||||
if self.consumables:
|
||||
consumables = await snes_read(ctx, KDL3_CONSUMABLES, 1920)
|
||||
for consumable in consumable_addrs:
|
||||
# TODO: see if this can be sped up in any way
|
||||
loc_id = 0x770300 + consumable
|
||||
if loc_id not in ctx.checked_locations and consumables[consumable_addrs[consumable]] == 0x01:
|
||||
new_checks.append(loc_id)
|
||||
if self.stars:
|
||||
stars = await snes_read(ctx, KDL3_STARS, 1920)
|
||||
for star in star_addrs:
|
||||
if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
|
||||
new_checks.append(star)
|
||||
|
||||
if not game_state:
|
||||
return
|
||||
|
||||
if game_state[0] != 0xFF:
|
||||
await self.pop_gift(ctx)
|
||||
await self.pop_item(ctx, game_state[0] != 0xFF)
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
# boss status
|
||||
boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2)
|
||||
boss_flag = int.from_bytes(boss_flag_bytes, "little")
|
||||
for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()):
|
||||
if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
|
||||
new_checks.append(boss)
|
||||
|
||||
for new_check_id in new_checks:
|
||||
ctx.locations_checked.add(new_check_id)
|
||||
location = ctx.location_names.lookup_in_game(new_check_id)
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/'
|
||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
||||
except Exception as ex:
|
||||
# we crashed, so print log and clean up
|
||||
snes_logger.error("", exc_info=ex)
|
||||
if "gift" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("gift")
|
||||
ctx.rom = None
|
||||
ctx.game = None
|
||||
Reference in New Issue
Block a user