Archipelago/worlds/lufia2ac/Client.py

361 lines
11 KiB
Python

import logging
import time
import typing
from logging import Logger
from typing import Dict
from NetUtils import ClientStatus, NetworkItem
from worlds.AutoSNIClient import SNIClient
from .Items import start_id as items_start_id
from .Locations import start_id as locations_start_id
if typing.TYPE_CHECKING:
from SNIClient import SNIContext
else:
SNIContext = typing.Any
snes_logger: Logger = logging.getLogger("SNES")
SRAM_START: int = 0xE00000
L2AC_ROMNAME_START: int = 0x007FC0
L2AC_SIGN_ADDR: int = SRAM_START + 0x2000
L2AC_GOAL_ADDR: int = SRAM_START + 0x2030
L2AC_DEATH_ADDR: int = SRAM_START + 0x203D
L2AC_TX_ADDR: int = SRAM_START + 0x2040
L2AC_RX_ADDR: int = SRAM_START + 0x2800
enemy_names: Dict[int, str] = {
0x00: "a Goblin",
0x01: "an Armor goblin",
0x02: "a Regal Goblin",
0x03: "a Goblin Mage",
0x04: "a Troll",
0x05: "an Ork",
0x06: "a Fighter ork",
0x07: "an Ork Mage",
0x08: "a Lizardman",
0x09: "a Skull Lizard",
0x0A: "an Armour Dait",
0x0B: "a Dragonian",
0x0C: "a Cyclops",
0x0D: "a Mega Cyclops",
0x0E: "a Flame genie",
0x0F: "a Well Genie",
0x10: "a Wind Genie",
0x11: "an Earth Genie",
0x12: "a Cobalt",
0x13: "a Merman",
0x14: "an Aqualoi",
0x15: "an Imp",
0x16: "a Fiend",
0x17: "an Archfiend",
0x18: "a Hound",
0x19: "a Doben",
0x1A: "a Winger",
0x1B: "a Serfaco",
0x1C: "a Pug",
0x1D: "a Salamander",
0x1E: "a Brinz Lizard",
0x1F: "a Seahorse",
0x20: "a Seirein",
0x21: "an Earth Viper",
0x22: "a Gnome",
0x23: "a Wispy",
0x24: "a Thunderbeast",
0x25: "a Lunar bear",
0x26: "a Shadowfly",
0x27: "a Shadow",
0x28: "a Lion",
0x29: "a Sphinx",
0x2A: "a Mad horse",
0x2B: "an Armor horse",
0x2C: "a Buffalo",
0x2D: "a Bruse",
0x2E: "a Bat",
0x2F: "a Big Bat",
0x30: "a Red Bat",
0x31: "an Eagle",
0x32: "a Hawk",
0x33: "a Crow",
0x34: "a Baby Frog",
0x35: "a King Frog",
0x36: "a Lizard",
0x37: "a Newt",
0x38: "a Needle Lizard",
0x39: "a Poison Lizard",
0x3A: "a Medusa",
0x3B: "a Ramia",
0x3C: "a Basilisk",
0x3D: "a Cokatoris",
0x3E: "a Scorpion",
0x3F: "an Antares",
0x40: "a Small Crab",
0x41: "a Big Crab",
0x42: "a Red Lobster",
0x43: "a Spider",
0x44: "a Web Spider",
0x45: "a Beetle",
0x46: "a Poison Beetle",
0x47: "a Mosquito",
0x48: "a Coridras",
0x49: "a Spinner",
0x4A: "a Tartona",
0x4B: "an Armour Nail",
0x4C: "a Moth",
0x4D: "a Mega Moth",
0x4E: "a Big Bee",
0x4F: "a Dark Fly",
0x50: "a Stinger",
0x51: "an Armor Bee",
0x52: "a Sentopez",
0x53: "a Cancer",
0x54: "a Garbost",
0x55: "a Bolt Fish",
0x56: "a Moray",
0x57: "a She Viper",
0x58: "an Angler fish",
0x59: "a Unicorn",
0x5A: "an Evil Shell",
0x5B: "a Drill Shell",
0x5C: "a Snell",
0x5D: "an Ammonite",
0x5E: "an Evil Fish",
0x5F: "a Squid",
0x60: "a Kraken",
0x61: "a Killer Whale",
0x62: "a White Whale",
0x63: "a Grianos",
0x64: "a Behemoth",
0x65: "a Perch",
0x66: "a Current",
0x67: "a Vampire Rose",
0x68: "a Desert Rose",
0x69: "a Venus Fly",
0x6A: "a Moray Vine",
0x6B: "a Torrent",
0x6C: "a Mad Ent",
0x6D: "a Crow Kelp",
0x6E: "a Red Plant",
0x6F: "La Fleshia",
0x70: "a Wheel Eel",
0x71: "a Skeleton",
0x72: "a Ghoul",
0x73: "a Zombie",
0x74: "a Specter",
0x75: "a Dark Spirit",
0x76: "a Snatcher",
0x77: "a Jurahan",
0x78: "a Demise",
0x79: "a Leech",
0x7A: "a Necromancer",
0x7B: "a Hade Chariot",
0x7C: "a Hades",
0x7D: "a Dark Skull",
0x7E: "a Hades Skull",
0x7F: "a Mummy",
0x80: "a Vampire",
0x81: "a Nosferato",
0x82: "a Ghost Ship",
0x83: "a Deadly Sword",
0x84: "a Deadly Armor",
0x85: "a T Rex",
0x86: "a Brokion",
0x87: "a Pumpkin Head",
0x88: "a Mad Head",
0x89: "a Snow Gas",
0x8A: "a Great Coca",
0x8B: "a Gargoyle",
0x8C: "a Rogue Shape",
0x8D: "a Bone Gorem",
0x8E: "a Nuborg",
0x8F: "a Wood Gorem",
0x90: "a Mad Gorem",
0x91: "a Green Clay",
0x92: "a Sand Gorem",
0x93: "a Magma Gorem",
0x94: "an Iron Gorem",
0x95: "a Gold Gorem",
0x96: "a Hidora",
0x97: "a Sea Hidora",
0x98: "a High Hidora",
0x99: "a King Hidora",
0x9A: "an Orky",
0x9B: "a Waiban",
0x9C: "a White Dragon",
0x9D: "a Red Dragon",
0x9E: "a Blue Dragon",
0x9F: "a Green Dragon",
0xA0: "a Black Dragon",
0xA1: "a Copper Dragon",
0xA2: "a Silver Dragon",
0xA3: "a Gold Dragon",
0xA4: "a Red Jelly",
0xA5: "a Blue Jelly",
0xA6: "a Bili Jelly",
0xA7: "a Red Core",
0xA8: "a Blue Core",
0xA9: "a Green Core",
0xAA: "a No Core",
0xAB: "a Mimic",
0xAC: "a Blue Mimic",
0xAD: "an Ice Roge",
0xAE: "a Mushroom",
0xAF: "a Big Mushr'm",
0xB0: "a Minataurus",
0xB1: "a Gorgon",
0xB2: "a Ninja",
0xB3: "an Asashin",
0xB4: "a Samurai",
0xB5: "a Dark Warrior",
0xB6: "an Ochi Warrior",
0xB7: "a Sly Fox",
0xB8: "a Tengu",
0xB9: "a Warm Eye",
0xBA: "a Wizard",
0xBB: "a Dark Sum'ner",
0xBC: "the Big Catfish",
0xBD: "a Follower",
0xBE: "the Tarantula",
0xBF: "Pierre",
0xC0: "Daniele",
0xC1: "the Venge Ghost",
0xC2: "the Fire Dragon",
0xC3: "the Tank",
0xC4: "Idura",
0xC5: "Camu",
0xC6: "Gades",
0xC7: "Amon",
0xC8: "Erim",
0xC9: "Daos",
0xCA: "a Lizard Man",
0xCB: "a Goblin",
0xCC: "a Skeleton",
0xCD: "a Regal Goblin",
0xCE: "a Goblin",
0xCF: "a Goblin Mage",
0xD0: "a Slave",
0xD1: "a Follower",
0xD2: "a Groupie",
0xD3: "the Egg Dragon",
0xD4: "a Mummy",
0xD5: "a Troll",
0xD6: "Gades",
0xD7: "Idura",
0xD8: "a Lion",
0xD9: "the Rogue Flower",
0xDA: "a Gargoyle",
0xDB: "a Ghost Ship",
0xDC: "Idura",
0xDD: "a Soldier",
0xDE: "Gades",
0xDF: "the Master",
}
class L2ACSNIClient(SNIClient):
game: str = "Lufia II Ancient Cave"
async def validate_rom(self, ctx: SNIContext) -> bool:
from SNIClient import snes_read
rom_name: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15)
if rom_name is None or rom_name[:4] != b"L2AC":
return False
ctx.game = self.game
ctx.items_handling = 0b111 # fully remote
ctx.rom = rom_name
return True
async def game_watcher(self, ctx: SNIContext) -> None:
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15)
if rom != ctx.rom:
ctx.rom = None
return
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
return
signature: bytes = await snes_read(ctx, L2AC_SIGN_ADDR, 16)
if signature != b"ArchipelagoLufia":
return
# Goal
if not ctx.finished_game:
goal_data: bytes = await snes_read(ctx, L2AC_GOAL_ADDR, 10)
if goal_data is not None and goal_data[goal_data[0]] == 0x01:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
# DeathLink TX
death_data: bytes = await snes_read(ctx, L2AC_DEATH_ADDR, 3)
if death_data is not None:
await ctx.update_death_link(bool(death_data[0]))
if death_data[1] != 0x00:
snes_buffered_write(ctx, L2AC_DEATH_ADDR + 1, b"\x00")
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
player_name: str = ctx.player_names.get(ctx.slot, str(ctx.slot))
enemy_name: str = enemy_names.get(death_data[1] - 1, hex(death_data[1] - 1))
await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.")
# TX
tx_data: bytes = await snes_read(ctx, L2AC_TX_ADDR, 8)
if tx_data is not None:
snes_items_sent = int.from_bytes(tx_data[:2], "little")
client_items_sent = int.from_bytes(tx_data[2:4], "little")
client_ap_items_found = int.from_bytes(tx_data[4:6], "little")
if client_items_sent < snes_items_sent:
location_id: int = locations_start_id + client_items_sent
location: str = ctx.location_names[location_id]
client_items_sent += 1
ctx.locations_checked.add(location_id)
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
snes_logger.info("New Check: %s (%d/%d)" % (
location,
len(ctx.locations_checked),
len(ctx.missing_locations) + len(ctx.checked_locations)))
snes_buffered_write(ctx, L2AC_TX_ADDR + 2, client_items_sent.to_bytes(2, "little"))
ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values())
if client_ap_items_found < ap_items_found:
snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little"))
# RX
rx_data: bytes = await snes_read(ctx, L2AC_RX_ADDR, 4)
if rx_data is not None:
snes_items_received = int.from_bytes(rx_data[:2], "little")
if snes_items_received < len(ctx.items_received):
item: NetworkItem = ctx.items_received[snes_items_received]
item_code: int = item.item - items_start_id
snes_items_received += 1
snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % (
ctx.item_names[item.item],
ctx.player_names[item.player],
ctx.location_names[item.location],
snes_items_received, len(ctx.items_received)))
snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, 'little'))
snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little"))
await snes_flush_writes(ctx)
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes
# DeathLink RX
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x01")
else:
snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x00")
await snes_flush_writes(ctx)
ctx.death_state = DeathState.dead