mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 07:03:44 -08:00
* Add the world * doc update * docs * Fix Blast/Missile not clearing Reflect * Update worlds/earthbound/__init__.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/__init__.py remove unused import Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/__init__.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/modules/dungeon_er.py make bool optional Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/modules/boss_shuffle.py typing update Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/modules/boss_shuffle.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Filter events out of item name to id * we call it a glorp * Update worlds/earthbound/Regions.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/__init__.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/Items.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Update worlds/earthbound/Regions.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Fix missing optional import * hint stuff * -Fix Apple Kid text being wrong -Fix Slimy Pile text being wrong * -Fix some sprite corruption if PSI was used when an enemy loaded another enemy -Fixed a visible artifact tile during some cutscenes * Update ver * Update docs * Fix some money scripting issues * Add argument to PSI fakeout attack * Updated monkey caves shop description * Remove closing markdown from doc * Add new flavors * Make flavors actually work * Update platforms * Fix common gear getting duplicated * Split region initialization * Condense checks for start inventory + some other junk * Fix some item groups - change receiver phone to warp pad * wow that one was really bad :glorp: * blah * Fix cutoff option text * switch start inventory concatenation to itertools * Fix sky runner scripting bug - added some new comm suggestions * Fix crash when generating with spoiler_only * Fix happy-happy teleport not unlocking after beating carpainter * Hint man hints can now use CreateHint packets to create hints in other games * Adjust some filler rarity * Update world to use CreateHints and deprecate old method * Fix epilogue skip being offset * Rearrange a couple regions * Fix tendapants getting deleted in battle * update doc * i got scared and forgot i had multiple none checks and am worried about this triggering but tested and it works * Fix mostly typing errors from silvris * More type checks * More typing * Typema * Type * Fix enemy levels overwriting music * Fix gihugic blunder * Fix Lumine Hall enabling OSS * del world * Rel 4.2.7 * Remove some debug logs * Fix vanilla bug with weird ambush detection * Fix Starman Junior having an unscaled Freeze * Change shop scaling * Fix shops using the wrong thankful script * Update some bosses in boss shuffle * Loc group adjustment * Update some boss shuffle stuff | Fix Enemizer attacks getting overwritten by Shuffle data | Fix flunkies not updating and still being used with enemizer * Get rid of some debug stuff * Get boss shuffle running, dont merge * Fix json and get boss shuffle no plando back up * Fix Magicant Boost not initializing to Ness if party count = 4 * Fix belch shop using wrong logic * Don't re-send goal status * EBitem * remove : * idk if this is whatvi wanted * All client messagesnow only send when relevant instead of constantly * Patch up the rest of boss plando * Fix Giygas being not excluded from enemizer * Fix epilogue again * adjust the sphere scaling name * add the things * Fix Ness being placed onto monotoli when monotoli was in sea of eden * Fix prefill properly * Fix boss shuffle on vanilla slots. * rename this, apparently * Update archipelago.json --------- Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
517 lines
24 KiB
Python
517 lines
24 KiB
Python
import logging
|
|
import struct
|
|
import typing
|
|
import time
|
|
import uuid
|
|
from struct import pack
|
|
from .game_data.local_data import client_specials, world_version, hint_bits, item_id_table, money_id_table
|
|
from .game_data.text_data import text_encoder
|
|
from .gifting.gift_tags import gift_properties
|
|
from .gifting.trait_parser import wanted_traits, trait_interpreter, gift_exclusions
|
|
|
|
from NetUtils import ClientStatus, color
|
|
from worlds.AutoSNIClient import SNIClient
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from SNIClient import SNIContext
|
|
else:
|
|
SNIContext = typing.Any
|
|
|
|
snes_logger = logging.getLogger("SNES")
|
|
|
|
ROM_START = 0x000000
|
|
WRAM_START = 0xF50000
|
|
WRAM_SIZE = 0x20000
|
|
SRAM_START = 0xE00000
|
|
|
|
EB_ROMHASH_START = 0x00FFC0
|
|
WORLD_VERSION = 0x3FF0A0
|
|
ROMHASH_SIZE = 0x15
|
|
|
|
ITEM_MODE = ROM_START + 0x04FD76
|
|
|
|
ITEMQUEUE_HIGH = WRAM_START + 0xB576
|
|
ITEM_RECEIVED = WRAM_START + 0xB570
|
|
SPECIAL_RECEIVED = WRAM_START + 0xB572
|
|
MONEY_RECIVED = WRAM_START + 0xB5F1
|
|
SAVE_FILE = WRAM_START + 0xB4A1
|
|
GIYGAS_CLEAR = WRAM_START + 0x9C11
|
|
GAME_CLEAR = WRAM_START + 0x9C85
|
|
OPEN_WINDOW = WRAM_START + 0x8958
|
|
MELODY_TABLE = WRAM_START + 0x9C1E
|
|
EARTH_POWER_FLAG = WRAM_START + 0x9C82
|
|
CUR_SCENE = WRAM_START + 0x97B8
|
|
IS_IN_BATTLE = WRAM_START + 0x9643
|
|
DEATHLINK_ENABLED = ROM_START + 0x04FD74
|
|
DEATHLINK_TYPE = ROM_START + 0x04FD75
|
|
IS_CURRENTLY_DEAD = WRAM_START + 0xB582
|
|
GOT_DEATH_FROM_SERVER = WRAM_START + 0xB583
|
|
PLAYER_JUST_DIED_SEND_DEATHLINK = WRAM_START + 0xB584
|
|
IS_ABLE_TO_RECEIVE_DEATHLINKS = WRAM_START + 0xB585
|
|
CHAR_COUNT = WRAM_START + 0x98A4
|
|
OSS_FLAG = WRAM_START + 0x5D98
|
|
HINT_SCOUNT_IDS = ROM_START + 0x310250
|
|
SCOUTED_HINT_FLAGS = WRAM_START + 0xB621
|
|
MONEY_IN_BANK = WRAM_START + 0x9835
|
|
IS_ENERGYLINK_ENABLED = ROM_START + 0x04FD78
|
|
already_tried_to_connect = False
|
|
|
|
|
|
class EarthBoundClient(SNIClient):
|
|
game = "EarthBound"
|
|
patch_suffix = ".apeb"
|
|
most_recent_connect: str = ""
|
|
client_version: str = world_version
|
|
hint_list: list[int] = []
|
|
hinted_shop_locations: list[int] = []
|
|
|
|
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
|
|
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
|
battle_hp = {
|
|
1: WRAM_START + 0x9FBF,
|
|
2: WRAM_START + 0xA00D,
|
|
3: WRAM_START + 0xA05B,
|
|
4: WRAM_START + 0xA0A9,
|
|
}
|
|
|
|
active_hp = {
|
|
1: WRAM_START + 0x9A15,
|
|
2: WRAM_START + 0x9A74,
|
|
3: WRAM_START + 0x9AD3,
|
|
4: WRAM_START + 0x9B32,
|
|
}
|
|
|
|
scrolling_hp = {
|
|
1: WRAM_START + 0x9A13,
|
|
2: WRAM_START + 0x9A72,
|
|
3: WRAM_START + 0x9AD1,
|
|
4: WRAM_START + 0x9B30,
|
|
}
|
|
|
|
deathlink_mode = await snes_read(ctx, DEATHLINK_TYPE, 1)
|
|
oss_flag = await snes_read(ctx, OSS_FLAG, 1)
|
|
is_currently_dead = await snes_read(ctx, IS_CURRENTLY_DEAD, 1)
|
|
can_receive_deathlinks = await snes_read(ctx, IS_ABLE_TO_RECEIVE_DEATHLINKS, 1)
|
|
is_in_battle = await snes_read(ctx, IS_IN_BATTLE, 1)
|
|
char_count = await snes_read(ctx, CHAR_COUNT, 1)
|
|
snes_buffered_write(ctx, GOT_DEATH_FROM_SERVER, bytes([0x01]))
|
|
text_open = await snes_read(ctx, OPEN_WINDOW, 1)
|
|
|
|
if text_open is None: #Catch None reads from client jank????????
|
|
return
|
|
|
|
if is_currently_dead[0] != 0x00 or can_receive_deathlinks[0] == 0x00:
|
|
return
|
|
|
|
# If suppression is set and we're not in a battle dont do deathlinks
|
|
if oss_flag[0] != 0x00 and is_in_battle[0] == 0x00:
|
|
return
|
|
|
|
# Prevent overworld deaths while a menu is open
|
|
if not is_in_battle[0] and text_open[0] != 0xFF:
|
|
return
|
|
|
|
for i in range(char_count[0]):
|
|
w_cur_char = WRAM_START + 0x986F + i
|
|
current_char = await snes_read(ctx, w_cur_char, 1)
|
|
snes_buffered_write(ctx, active_hp[current_char[0]], bytes([0x00, 0x00]))
|
|
snes_buffered_write(ctx, battle_hp[i + 1], bytes([0x00, 0x00]))
|
|
if deathlink_mode[0] == 0 or is_in_battle[0] == 0:
|
|
# This should be the check for instant or mercy. Write the value, call it here
|
|
snes_buffered_write(ctx, scrolling_hp[current_char[0]], bytes([0x00, 0x00]))
|
|
await snes_flush_writes(ctx)
|
|
ctx.death_state = DeathState.dead
|
|
ctx.last_death_link = time.time()
|
|
|
|
def on_package(self, ctx, cmd: str, args: dict[str, typing.Any]) -> None:
|
|
super().on_package(ctx, cmd, args)
|
|
|
|
if cmd == "Connected":
|
|
self.slot_data = args.get("slot_data", None)
|
|
|
|
async def validate_rom(self, ctx: "SNIContext") -> bool:
|
|
from SNIClient import snes_read
|
|
|
|
rom_name = await snes_read(ctx, EB_ROMHASH_START, ROMHASH_SIZE)
|
|
apworld_version = await snes_read(ctx, WORLD_VERSION, 16)
|
|
|
|
item_handling = await snes_read(ctx, ITEM_MODE, 1)
|
|
if rom_name is None or rom_name[:6] != b"MOM2AP":
|
|
return False
|
|
|
|
apworld_version = apworld_version.decode("utf-8").strip("\x00")
|
|
if apworld_version != self.most_recent_connect and apworld_version != self.client_version:
|
|
ctx.gui_error("Bad Version", f"EarthBound APWorld version {self.client_version} does not match generated version {apworld_version}")
|
|
self.most_recent_connect = apworld_version
|
|
return False
|
|
|
|
ctx.game = self.game
|
|
if item_handling[0] == 0x00:
|
|
ctx.items_handling = 0b001
|
|
else:
|
|
ctx.items_handling = 0b111
|
|
ctx.rom = rom_name
|
|
|
|
death_link = await snes_read(ctx, DEATHLINK_ENABLED, 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, snes_write
|
|
giygas_clear = await snes_read(ctx, GIYGAS_CLEAR, 0x1)
|
|
game_clear = await snes_read(ctx, GAME_CLEAR, 0x1)
|
|
item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1)
|
|
special_received = await snes_read(ctx, SPECIAL_RECEIVED, 0x1)
|
|
money_received = await snes_read(ctx, MONEY_RECIVED, 0x2)
|
|
save_num = await snes_read(ctx, SAVE_FILE, 0x1)
|
|
text_open = await snes_read(ctx, OPEN_WINDOW, 1)
|
|
melody_table = await snes_read(ctx, MELODY_TABLE, 2)
|
|
earth_power_absorbed = await snes_read(ctx, EARTH_POWER_FLAG, 1)
|
|
cur_script = await snes_read(ctx, CUR_SCENE, 1)
|
|
rom = await snes_read(ctx, EB_ROMHASH_START, ROMHASH_SIZE)
|
|
scouted_hint_flags = await snes_read(ctx, SCOUTED_HINT_FLAGS, 1)
|
|
gift_target = await snes_read(ctx, WRAM_START + 0xB5E7, 2)
|
|
outbound_gifts = await snes_read(ctx, WRAM_START + 0x31D0, 1)
|
|
shop_scout = await snes_read(ctx, WRAM_START + 0x0770, 1)
|
|
shop_scouts_enabled = await snes_read(ctx, ROM_START + 0x04FD77, 1)
|
|
outgoing_energy = await snes_read(ctx, MONEY_IN_BANK, 4)
|
|
if rom != ctx.rom:
|
|
ctx.rom = None
|
|
return
|
|
|
|
if giygas_clear[0] & 0x01 == 0x01: # Are we in the epilogue
|
|
return
|
|
|
|
if save_num[0] == 0x00: # If on the title screen
|
|
return
|
|
|
|
if ctx.slot is None:
|
|
return
|
|
|
|
if outgoing_energy is None: #None Catcher
|
|
return
|
|
|
|
|
|
if f"GiftBoxes;{ctx.team}" not in ctx.stored_data:
|
|
await ctx.send_msgs([{
|
|
"cmd": "SetNotify",
|
|
"keys": [f"GiftBoxes;{ctx.team}"]
|
|
}])
|
|
|
|
# GIFTING DATA
|
|
if f"GiftBox;{ctx.team};{ctx.slot}" not in ctx.stored_data:
|
|
local_giftbox = {
|
|
str(ctx.slot): {
|
|
"is_open": True,
|
|
"accepts_any_gift": False,
|
|
"desired_traits": wanted_traits,
|
|
"minimum_gift_data_version": 2,
|
|
"maximum_gift_data_version": 3}}
|
|
await ctx.send_msgs([{
|
|
"cmd": "Set",
|
|
"key": f"GiftBoxes;{ctx.team}",
|
|
"want_reply": False,
|
|
"default": {},
|
|
"operations": [{"operation": "update", "value": local_giftbox}]
|
|
}])
|
|
|
|
await ctx.send_msgs([{
|
|
"cmd": "Get",
|
|
"keys": [f"GiftBox;{ctx.team};{ctx.slot}"]
|
|
}])
|
|
|
|
await ctx.send_msgs([{
|
|
"cmd": "SetNotify",
|
|
"keys": [f"GiftBox;{ctx.team};{ctx.slot}", f"GiftBoxes;{ctx.team}"]
|
|
}])
|
|
|
|
inbox = ctx.stored_data.get(f"GiftBox;{ctx.team};{ctx.slot}")
|
|
motherbox = ctx.stored_data.get(f"GiftBoxes;{ctx.team}")
|
|
if inbox:
|
|
gift_item_name = "None"
|
|
key, gift = next(iter(inbox.items()))
|
|
if "item_name" in gift or "ItemName" in gift:
|
|
gift_item_name = gift.get("item_name", gift.get("ItemName"))
|
|
if gift_item_name in item_id_table and gift_item_name not in gift_exclusions:
|
|
# If the name matches an EB item, convert it to one (even if not coming from EB)
|
|
item = item_id_table[gift_item_name]
|
|
else:
|
|
item = trait_interpreter(gift)
|
|
|
|
inbox_queue = await snes_read(ctx, WRAM_START + 0x3200, 1)
|
|
# Pause if the receiver queue is full
|
|
if not inbox_queue[0]:
|
|
await snes_write(ctx, [(WRAM_START + 0x3200, bytes([item]))])
|
|
inbox.pop(key)
|
|
await ctx.send_msgs([{
|
|
"cmd": "Set",
|
|
"key": f"GiftBox;{ctx.team};{ctx.slot}",
|
|
"want_reply": False,
|
|
"default": {},
|
|
"operations": [{"operation": "pop", "value": key}]
|
|
}])
|
|
|
|
# We're in the Gift selection menu. This should write the selected player's name into RAM
|
|
# for parsing.
|
|
# TODO; CHECK A SETNOTIFY HERE
|
|
gift_target = int.from_bytes(gift_target, byteorder="little")
|
|
|
|
# Giftbox checking for the gift menu UI
|
|
if gift_target != 0x00 and motherbox is not None:
|
|
gift_recipient = str(gift_target)
|
|
recip_name = ctx.player_names[gift_target]
|
|
recip_name = get_alias(recip_name, ctx.slot_info[gift_target].name)
|
|
recip_name = text_encoder(recip_name, 20)
|
|
if gift_recipient in motherbox:
|
|
if "IsOpen" in motherbox[gift_recipient]:
|
|
motherbox[gift_recipient]["is_open"] = motherbox[gift_recipient].pop("IsOpen")
|
|
|
|
if gift_recipient in motherbox and motherbox[gift_recipient]["is_open"]:
|
|
recip_name.extend(text_encoder(" (Open)", 20))
|
|
else:
|
|
recip_name.extend(text_encoder(" (Closed)", 20))
|
|
recip_name.append(0x00)
|
|
await snes_write(ctx, [(WRAM_START + 0xFF80, recip_name)])
|
|
await snes_write(ctx, [(WRAM_START + 0xB5E7, bytes([0x00, 0x00]))])
|
|
await snes_write(ctx, [(WRAM_START + 0xB573, bytes([0x00, 0x00]))])
|
|
|
|
gift_flag_byte = await snes_read(ctx, WRAM_START + 0xB622, 1)
|
|
gift_flag_byte = gift_flag_byte[0] | 0x04
|
|
await snes_write(ctx, [(WRAM_START + 0xB622, bytes([gift_flag_byte]))])
|
|
|
|
if outbound_gifts[0] != 0x00 and motherbox is not None:
|
|
gift_buffer = await snes_read(ctx, WRAM_START + 0x31D1, 3)
|
|
gift_item_id = gift_buffer[0]
|
|
gift = gift_properties[gift_item_id]
|
|
recipient = struct.unpack("H", gift_buffer[-2:])
|
|
if str(recipient[0]) in motherbox:
|
|
# Check if the player's box is open, refund if not
|
|
if "IsOpen" in motherbox[str(recipient[0])]:
|
|
# Does the recipient 0 thing work if > 255? Will need some testing.
|
|
motherbox[str(recipient[0])]["is_open"] = motherbox[str(recipient[0])].pop("IsOpen")
|
|
|
|
if "AcceptsAnyGift" in motherbox[str(recipient[0])]:
|
|
motherbox[str(recipient[0])]["accepts_any_gift"] = motherbox[str(recipient[0])].pop("AcceptsAnyGift")
|
|
|
|
if "DesiredTraits" in motherbox[str(recipient[0])]:
|
|
motherbox[str(recipient[0])]["desired_traits"] = motherbox[str(recipient[0])].pop("DesiredTraits")
|
|
|
|
if "Trait" in motherbox[str(recipient[0])]["desired_traits"]:
|
|
motherbox[str(recipient[0])]["desired_traits"]["trait"] = motherbox[str(recipient[0])]["desired_traits"].pop("Trait")
|
|
|
|
if str(recipient[0]) in motherbox and motherbox[str(recipient[0])]["is_open"] and (any(
|
|
motherbox[str(recipient[0])]["accepts_any_gift"] or
|
|
trait["trait"] in motherbox[str(recipient[0])]["desired_traits"] for trait in gift.traits)):
|
|
was_refunded = False
|
|
recipient = recipient[0]
|
|
else:
|
|
was_refunded = True
|
|
recipient = ctx.slot
|
|
guid = str(uuid.uuid4())
|
|
outgoing_gift = {
|
|
guid: {
|
|
"id": guid,
|
|
"item_name": gift.name,
|
|
"amount": 1,
|
|
"item_value": gift.value,
|
|
"traits": gift.traits,
|
|
"sender_slot": ctx.slot,
|
|
"receiver_slot": recipient,
|
|
"sender_team": ctx.team,
|
|
"receiver_team": ctx.team, # ??? Should be Receive slot team?
|
|
"is_refund": was_refunded}}
|
|
|
|
await ctx.send_msgs([{
|
|
"cmd": "Set",
|
|
"key": f"GiftBox;{ctx.team};{recipient}", # Receiver team here too
|
|
"want_reply": True,
|
|
"default": {},
|
|
"operations": [{"operation": "update", "value": outgoing_gift}]
|
|
}])
|
|
|
|
gift_queue = await snes_read(ctx, WRAM_START + 0x31D4, 0x21)
|
|
# shuffle the entire queue down 3 bytes
|
|
outbox_full_byte = await snes_read(ctx, WRAM_START + 0xB622, 1)
|
|
|
|
await snes_write(ctx, [(WRAM_START + 0x31D1, gift_queue)])
|
|
await snes_write(ctx, [(WRAM_START + 0x31D0, bytes([outbound_gifts[0] - 1]))])
|
|
outbox_full_byte = outbox_full_byte[0] & ~0x08
|
|
await snes_write(ctx, [(WRAM_START + 0xB622, bytes([outbox_full_byte]))])
|
|
|
|
if (game_clear[0] & 0x01 == 0x01) and not ctx.finished_game: # Goal should ignore the item queue and textbox check
|
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
|
ctx.finished_game = True
|
|
|
|
for i in range(6):
|
|
if scouted_hint_flags[0] & hint_bits[i]:
|
|
if i not in self.hint_list:
|
|
scoutable_hint = await snes_read(ctx, HINT_SCOUNT_IDS + (i * 3), 3)
|
|
if not scoutable_hint[2]:
|
|
scoutable_hint = (int.from_bytes(scoutable_hint[:2], byteorder="little") + 0xEB0000)
|
|
self.hint_list.append(i)
|
|
await ctx.send_msgs([{"cmd": "CreateHints", "locations": [scoutable_hint], "player": ctx.player}])
|
|
else:
|
|
hint = self.slot_data['hint_man_hints'][i]
|
|
await ctx.send_msgs([{"cmd": "CreateHints", "locations": [hint[0]], "player": hint[1]}])
|
|
self.hint_list.append(i)
|
|
|
|
if shop_scout[0] and shop_scouts_enabled[0]:
|
|
shop_slots = []
|
|
for i in range(7):
|
|
slot_id = (0xEB0FF9 + (shop_scout[0] * 7) + i)
|
|
if slot_id in ctx.server_locations and slot_id not in self.hinted_shop_locations:
|
|
shop_slots.append(slot_id)
|
|
|
|
if shop_slots:
|
|
if shop_scouts_enabled[0] == 2:
|
|
await ctx.send_msgs([{"cmd": "CreateHints", "locations": shop_slots, "player": ctx.slot}])
|
|
await snes_write(ctx, [(WRAM_START + 0x0770, bytes([0x00]))])
|
|
else:
|
|
prog_shops = []
|
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": shop_slots, "create_as_hint": 0}])
|
|
for location in shop_slots:
|
|
if location in ctx.locations_info:
|
|
self.hinted_shop_locations.append(location)
|
|
if ctx.locations_info[location].flags & 0x01:
|
|
prog_shops.append(location)
|
|
if prog_shops:
|
|
await ctx.send_msgs([{"cmd": "CreateHints", "locations": prog_shops, "player": ctx.slot}])
|
|
|
|
melody_data = f"{ctx.team}_{ctx.slot}_melody_status"
|
|
earth_power_data = f"{ctx.team}_{ctx.slot}_earthpower"
|
|
current_melodies = int.from_bytes(melody_table, "little")
|
|
earth_power_state = int.from_bytes(earth_power_absorbed, "little")
|
|
|
|
if melody_data not in ctx.stored_data or (ctx.stored_data[melody_data] != current_melodies) or (ctx.stored_data[earth_power_data] != earth_power_state):
|
|
await ctx.send_msgs([{
|
|
"cmd": "Set",
|
|
"key": melody_data,
|
|
"default": None,
|
|
"want_reply": True,
|
|
"operations": [{"operation": "replace", "value": int.from_bytes(melody_table, "little")}]},
|
|
{
|
|
"cmd": "Set",
|
|
"key": earth_power_data,
|
|
"default": None,
|
|
"want_reply": True,
|
|
"operations": [{"operation": "replace", "value": int.from_bytes(earth_power_absorbed, "little")}]
|
|
}])
|
|
|
|
# death link handling goes here
|
|
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
|
|
send_deathlink = await snes_read(ctx, PLAYER_JUST_DIED_SEND_DEATHLINK, 1)
|
|
currently_dead = send_deathlink[0] != 0x00
|
|
if send_deathlink[0] != 0x00:
|
|
snes_buffered_write(ctx, PLAYER_JUST_DIED_SEND_DEATHLINK, bytes([0x00]))
|
|
await ctx.handle_deathlink_state(currently_dead)
|
|
|
|
new_checks = []
|
|
from .game_data.local_data import check_table
|
|
|
|
location_ram_data = await snes_read(ctx, WRAM_START + 0x9C00, 0x88)
|
|
shop_location_flags = await snes_read(ctx, WRAM_START + 0xB721, 0x41)
|
|
for loc_id, loc_data in check_table.items():
|
|
if loc_id not in ctx.locations_checked:
|
|
if loc_id >= 0xEB1000:
|
|
data = shop_location_flags[loc_data[0]]
|
|
else:
|
|
data = location_ram_data[loc_data[0]]
|
|
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 and loc_id in ctx.server_locations:
|
|
if text_open[0] == 0xFF or shop_scout[0]: # Don't check locations while in a textbox
|
|
new_checks.append(loc_id)
|
|
|
|
for new_check_id in new_checks:
|
|
ctx.locations_checked.add(new_check_id)
|
|
location = ctx.location_names.lookup_in_slot(new_check_id)
|
|
snes_logger.info(
|
|
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]}])
|
|
await snes_write(ctx, [(WRAM_START + 0x0770, bytes([0]))])
|
|
|
|
if item_received[0] or special_received[0] != 0x00 or money_received[0] != 0x00: # If processing any item from the server
|
|
return
|
|
|
|
is_energylink_enabled = await snes_read(ctx, IS_ENERGYLINK_ENABLED, 1)
|
|
is_requesting_energy = await snes_read(ctx, WRAM_START + 0x0790, 1)
|
|
energy_withdrawal = await snes_read(ctx, WRAM_START + 0x0796, 4)
|
|
ctx.set_notify(f"EnergyLink{ctx.team}")
|
|
energy = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
|
exchange_rate = 1000000
|
|
if is_energylink_enabled[0]:
|
|
|
|
deposited_energy = int.from_bytes(outgoing_energy, byteorder="little")
|
|
if deposited_energy:
|
|
deposited_energy *= exchange_rate
|
|
await snes_write(ctx, [(MONEY_IN_BANK, (0x00).to_bytes(4, byteorder="little"))])
|
|
await ctx.send_msgs([{
|
|
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
|
[{"operation": "add", "value": deposited_energy},
|
|
{"operation": "max", "value": 0}]}])
|
|
|
|
if is_requesting_energy[0] and energy: # This is just to pull the current number for a display.
|
|
energy //= exchange_rate
|
|
if energy > 9999999:
|
|
energy = 9999999
|
|
cap_flag = await snes_read(ctx, WRAM_START + 0xB623, 1)
|
|
cap_flag = int.from_bytes(cap_flag)
|
|
cap_flag |= 0x20
|
|
await snes_write(ctx, [(WRAM_START + 0xB623, cap_flag.to_bytes(1, byteorder="little"))])
|
|
|
|
await snes_write(ctx, [(WRAM_START + 0x0792, int(energy).to_bytes(4, byteorder="little"))])
|
|
await snes_write(ctx, [(WRAM_START + 0x0790, (0x00).to_bytes(1, byteorder="little"))])
|
|
|
|
if any(energy_withdrawal) and energy:
|
|
withdrawal = int.from_bytes(energy_withdrawal, byteorder="little")
|
|
withdrawal *= exchange_rate
|
|
energy = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) # Refresh the value
|
|
|
|
if withdrawal > energy:
|
|
energy_success = 2
|
|
withdrawal = energy
|
|
else:
|
|
energy_success = 1
|
|
|
|
await snes_write(ctx, [(WRAM_START + 0x97D0, (withdrawal // exchange_rate).to_bytes(4, byteorder="little"))])
|
|
await snes_write(ctx, [(WRAM_START + 0x0796, (0x00).to_bytes(4, byteorder="little"))])
|
|
await ctx.send_msgs([{
|
|
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
|
[{"operation": "add", "value": (withdrawal * -1)},
|
|
{"operation": "max", "value": 0}]}])
|
|
await snes_write(ctx, [(WRAM_START + 0x079A, energy_success.to_bytes(1, byteorder="little"))]) # Signal the game to continue
|
|
|
|
if cur_script[0]: # Stop items during cutscenes
|
|
return
|
|
|
|
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]
|
|
item_id = (item.item - 0xEB0000)
|
|
recv_index += 1
|
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
|
color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"),
|
|
color(ctx.player_names[item.player], 'yellow'),
|
|
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
|
|
|
snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index))
|
|
if item_id <= 0xFD:
|
|
snes_buffered_write(ctx, WRAM_START + 0xB570, bytes([item_id]))
|
|
elif item_id in money_id_table:
|
|
snes_buffered_write(ctx, WRAM_START + 0xB5F1, bytes([list(money_id_table).index(item_id) + 1]))
|
|
else:
|
|
snes_buffered_write(ctx, WRAM_START + 0xB572, bytes([client_specials[item_id]]))
|
|
|
|
await snes_flush_writes(ctx)
|
|
|
|
|
|
def get_alias(alias: str, slot_name: str) -> str:
|
|
try:
|
|
index = alias.index(f" ({slot_name}")
|
|
except ValueError:
|
|
return alias
|
|
return alias[:index]
|