Files
dockipelago/worlds/banjo_tooie/BTClient.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

1265 lines
52 KiB
Python

import asyncio, hashlib, json, os, multiprocessing, copy, sys, time, atexit, pkgutil
import bsdiff4 # pyright: ignore[reportMissingTypeStubs]
from typing import TYPE_CHECKING, Any, List, TypedDict, cast
from collections import Counter
from dataclasses import dataclass
# CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
import Utils
from Utils import async_start
from . import BanjoTooieWorld
if TYPE_CHECKING:
from kvui import UILog
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart banjoTooie_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure banjoTooie_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart banjoTooie_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
# Payload: lua -> client
# {
# playerName: string,
# locations: dict,
# deathlinkActive: bool,
# taglinkActive: bool,
# isDead: bool,
# isTag: bool,
# gameComplete: bool
# }
#
# Payload: client -> lua
# {
# items: list,
# playerNames: list,
# triggerDeath: bool,
# triggerTag: bool,
# messages: string
# }
#
# Deathlink logic:
# "Dead" is true <-> Banjo is at 0 hp.
#
# deathlink_pending: we need to kill the player
# deathlink_sent_this_death: we interacted with the multiworld on this death, waiting to reset with living link
bt_loc_name_to_id = BanjoTooieWorld.location_name_to_id
bt_itm_name_to_id = BanjoTooieWorld.item_name_to_id
script_version: int = 5
version: str = BanjoTooieWorld.world_version.as_simple_string()
patch_md5: str = "7509ee3bccf29337b819873228d27042"
bt_options = BanjoTooieWorld.settings
program = None
def read_file(path: str) -> bytes:
with open(path, "rb") as fi:
data = fi.read()
return data
def write_file(path: str, data: bytes):
with open(path, "wb") as fi:
fi.write(data)
def open_world_file(resource: str) -> bytes:
assert __package__ is not None, "BTClient needs to be loaded as a package."
ret = pkgutil.get_data(__package__, resource)
assert ret is not None, f"Unable to find: {resource}"
return ret
def patch_rom(rom_path: str, dst_path: str, patch_path: str):
rom: bytes = read_file(rom_path)
md5 = hashlib.md5(rom).hexdigest()
if md5 == "ca0df738ae6a16bfb4b46d3860c159d9": # byte swapped
swapped = bytearray(b'\0'*len(rom))
for i in range(0, len(rom), 2):
swapped[i] = rom[i+1]
swapped[i+1] = rom[i]
rom = bytes(swapped)
elif md5 != "40e98faa24ac3ebe1d25cb5e5ddf49e4":
logger.error(f"Unknown ROM! Please use /patch or restart the Banjo-Tooie Client to try again.")
return False
patch = open_world_file(patch_path)
patched_rom = cast(bytes, bsdiff4.patch(rom, patch)) # pyright: ignore[reportUnknownMemberType]
write_file(dst_path, patched_rom)
return True
async def patch_and_run(show_path: bool):
global program
game_name = "Banjo-Tooie"
patch_path = bt_options.get("patch_path", "")
patch_name = f"{game_name} AP {version}.z64"
if patch_path and os.access(patch_path, os.W_OK):
patch_path = os.path.join(patch_path, patch_name)
elif os.access(Utils.user_path(), os.W_OK):
patch_path = Utils.user_path(patch_name)
elif os.access(Utils.cache_path(), os.W_OK):
patch_path = Utils.cache_path(patch_name)
else:
patch_path = None
existing_md5 = None
if patch_path and os.path.isfile(patch_path):
rom = read_file(patch_path)
existing_md5 = hashlib.md5(rom).hexdigest()
await asyncio.sleep(0.01)
patch_successful = True
if not patch_path or existing_md5 != patch_md5:
rom = bt_options.get("rom_path", "")
if not rom or not os.path.isfile(rom):
rom = Utils.open_filename(f"Open your {game_name} US ROM", (("Rom Files", (".z64", ".n64")), ("All Files", "*"),))
if not rom:
logger.error(f"No ROM selected. Please use /patch or restart the {game_name} Client to try again.")
return
if not patch_path:
patch_path = os.path.split(rom)[0]
if os.access(patch_path, os.W_OK):
patch_path = os.path.join(patch_path, patch_name)
else:
logger.error(f"Unable to find writable path... Please use /patch or restart the {game_name} Client to try again.")
return
logger.info("Patching...")
patch_successful = patch_rom(rom, patch_path, "assets/Banjo-Tooie.patch")
if patch_successful:
bt_options.rom_path = rom
bt_options.patch_path = os.path.split(patch_path)[0]
else:
bt_options.rom_path = None
bt_options._changed = True
if patch_successful:
if show_path:
logger.info(f"Patched {game_name} is located here: {patch_path}")
program_path = bt_options.get("program_path", "")
if program_path and os.path.isfile(program_path) and (not program or program.poll() != None):
import shlex, subprocess
logger.info(f"Automatically starting {program_path}")
args = [*shlex.split(program_path)]
program_args = bt_options.program_args
if program_args:
if program_args == "--lua=":
lua = Utils.local_path("data", "lua", "connector_banjo_tooie_bizhawk.lua")
program_args = f'--lua={lua}'
if os.access(os.path.split(lua)[0], os.W_OK):
with open(lua, "w") as to:
to.write(open_world_file("assets/connector_banjo_tooie_bizhawk.lua").decode())
args.append(program_args)
args.append(patch_path)
program = subprocess.Popen(
args,
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
class BanjoTooieClientTab:
content: "UILog"
class BanjoTooieItemTracker:
def __init__(self, ctx: "BanjoTooieContext"):
self.ctx = ctx
self.items: Counter[str] = Counter()
self.refresh_items()
def refresh_items(self):
self.items.clear()
for item in self.ctx.items_received:
self.items[self.ctx.item_names.lookup_in_game(item.item)] += 1
data: list[dict[str, str]] = []
for item_name, amount in sorted(self.items.items()):
if amount > 1: data.append({"text":f"{item_name}: {amount}"})
else: data.append({"text":f"{item_name}"})
self.ctx.tab_items.content.data = data
class BanjoTooieCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: "BanjoTooieContext"):
super().__init__(ctx)
def _cmd_patch(self):
"""Reruns the patcher."""
asyncio.create_task(patch_and_run(True))
return True
def _cmd_autostart(self):
"""Allows configuring a program to automatically start with the client.
This allows you to, for example, automatically start Bizhawk with the patched ROM and lua.
If already configured, disables the configuration."""
program_path = bt_options.get("program_path", "")
if program_path == "" or not os.path.isfile(program_path):
program_path = Utils.open_filename(f"Select your program to automatically start", (("All Files", "*"),))
if program_path:
bt_options.program_path = program_path
bt_options._changed = True
logger.info(f"Autostart configured for: {program_path}")
if not program or program.poll() != None:
asyncio.create_task(patch_and_run(False))
else:
logger.error("No file selected...")
return False
else:
bt_options.program_path = ""
bt_options._changed = True
logger.info("Autostart disabled.")
return True
def _cmd_rom_path(self, path: str = ""):
"""Sets (or unsets) the file path of the vanilla ROM used for patching."""
bt_options.rom_path = path
bt_options._changed = True
if path:
logger.info("rom_path set!")
else:
logger.info("rom_path unset!")
return True
def _cmd_patch_path(self, path: str = ""):
"""Sets (or unsets) the folder path of where to save the patched ROM."""
bt_options.patch_path = path
bt_options._changed = True
if path:
logger.info("patch_path set!")
else:
logger.info("patch_path unset!")
return True
def _cmd_program_args(self, path: str = ""):
"""Sets (or unsets) the arguments to pass to the automatically run program. Defaults to passing the lua to Bizhawk."""
bt_options.program_args = path
bt_options._changed = True
if path:
logger.info("program_args set!")
else:
logger.info("program_args unset!")
return True
def _cmd_n64(self):
"""Check N64 Connection State"""
if isinstance(self.ctx, BanjoTooieContext):
logger.info(f"N64 Status: {self.ctx.n64_status}")
def _cmd_deathlink(self):
"""Toggle deathlink from client. Overrides default setting."""
if isinstance(self.ctx, BanjoTooieContext):
self.ctx.deathlink_client_override = True
self.ctx.deathlink_enabled = not self.ctx.deathlink_enabled
async_start(self.ctx.update_death_link(self.ctx.deathlink_enabled), name="Update Deathlink")
def _cmd_taglink(self):
"""Toggle taglink from client. Overrides default setting."""
if isinstance(self.ctx, BanjoTooieContext):
self.ctx.taglink_client_override = True
self.ctx.taglink_enabled = not self.ctx.taglink_enabled
async_start(self.ctx.update_tag_link(self.ctx.taglink_enabled), name="Update Taglink")
# def _cmd_tag(self):
# """Toggle a tag for Taglink."""
# if isinstance(self.ctx, BanjoTooieContext):
# async_start(self.ctx.send_tag_link(), name="Send Taglink")
@dataclass
class CreateHintsParams:
location: int
player: int
class BanjoTooieContext(CommonContext):
command_processor = BanjoTooieCommandProcessor
items_handling = 0b111 #full
tab_items: BanjoTooieClientTab
def __init__(self, server_address: str | None, password: str | None):
super().__init__(server_address, password)
self.game = "Banjo-Tooie"
self.n64_streams: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None = None
self.n64_sync_task = None
self.n64_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.location_table = {}
self.movelist_table: dict[str, bool] = {}
self.cheatorewardslist_table: dict[str, bool] = {}
self.honeybrewardslist_table: dict[str, bool] = {}
self.treblelist_table: dict[str, bool] = {}
self.stationlist_table: dict[str, bool] = {}
self.jinjofamlist_table: dict[str, bool] = {}
self.jinjolist_table: dict[str, bool] = {}
self.pages_table: dict[str, bool] = {}
self.honeycomb_table: dict[str, bool] = {}
self.glowbo_table: dict[str, bool] = {}
self.doubloon_table: dict[str, bool] = {}
self.notes_table: dict[str, bool] = {}
self.worldlist_table: dict[str, bool] = {}
self.chuffy_table: dict[str, bool] = {}
self.mystery_table: dict[str, bool] = {}
self.roystenlist_table: dict[str, bool] = {}
self.jiggychunks_table: dict[str, bool] = {}
self.goggles_table = False
self.dino_kids_table: dict[str, bool] = {}
self.boggy_kids_table: dict[str, bool] = {}
self.alien_kids_table: dict[str, bool] = {}
self.skivvies_table: dict[str, bool] = {}
self.mr_fit_table: dict[str, bool] = {}
self.bt_tickets_table: dict[str, bool] = {}
self.green_relics_table: dict[str, bool] = {}
self.beans_table: dict[str, bool] = {}
self.signpost_table = {}
self.warppads_table: dict[str, bool] = {}
self.silos_table: dict[str, bool] = {}
self.nests_table: dict[str, bool] = {}
self.roar = False
self.jiggy_table: dict[str, bool] = {}
self.current_map = 0
self.deathlink_enabled = False
self.deathlink_pending = False
self.deathlink_sent_this_death = False
self.deathlink_client_override = False
self.taglink_enabled = False
self.pending_tag_link = False
self.taglink_sent_this_tag = False
self.taglink_client_override = False
self.version_warning = False
self.messages: dict[int, dict[str, str | int] | str] = {}
self.slot_data = {}
self.sendSlot = False
self.sync_ready = False
self.startup = False
self.handled_scouts: list[CreateHintsParams] = []
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(BanjoTooieContext, self).server_auth(password_requested)
if not self.auth:
await self.get_username()
await self.send_connect()
self.awaiting_rom = True
return
return
def set_message(self, msg: dict[str, str | int] | str):
self.messages[len(self.messages)+1] = msg
def on_deathlink(self, data: dict[str, Any]):
self.deathlink_pending = True
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
async def send_death(self, death_text: str = ""):
if self.server and self.server.socket and self.slot is not None:
logger.info(f"(DeathLink: Sending death to your friends...)")
self.last_death_link = time.time()
await self.send_msgs([{
"cmd": "Bounce", "tags": ["DeathLink"],
"data": {
"time": self.last_death_link,
"source": self.player_names[self.slot],
"cause": death_text
}
}])
async def update_tag_link(self, tag_link: bool):
"""Helper function to set tag Link connection tag on/off and update the connection if already connected."""
old_tags = self.tags.copy()
if tag_link:
self.tags.add("TagLink")
else:
self.tags -= {"TagLink"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
async def send_tag_link(self):
"""Send a tag link message."""
if "TagLink" not in self.tags or self.slot is None:
return
if not hasattr(self, "instance_id"):
self.instance_id = time.time()
await self.send_msgs([{"cmd": "Bounce", "tags": ["TagLink"],
"data": {
"time": time.time(),
"source": self.instance_id,
"tag": True}}])
def run_gui(self):
from kvui import GameManager, Window, UILog
Window.bind(on_request_close=self.on_request_close) # pyright: ignore[reportArgumentType]
asyncio.create_task(patch_and_run(True))
class BanjoTooieManager(GameManager):
ctx: BanjoTooieContext
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Banjo-Tooie Client "+ version + " for AP"
def build(self):
ret = super().build()
self.ctx.tab_items = cast(BanjoTooieClientTab, self.add_client_tab("Items", UILog()))
return ret
self.ui = BanjoTooieManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_request_close(self, *args: Any):
from kvui import MessageBox
title = "Warning: Autostart program still running!"
message = "Attempting to close this window again will forcibly close it."
def cleanup(messagebox: MessageBox):
self._messagebox = None
if self._messagebox and self._messagebox.title == title: # pyright: ignore[reportUnknownMemberType]
return False
if program and program.poll() == None:
self.gui_error(title, message)
if self._messagebox:
self._messagebox.bind(on_dismiss=cleanup) # pyright: ignore[reportCallIssue]
return True
return False
def on_package(self, cmd: str, args: dict[Any, Any]):
if cmd == "Connected":
self.tracker = BanjoTooieItemTracker(self)
self.slot_data: dict[str, Any] = args.get("slot_data", {})
assert self.slot_data, "This slot doesn't have any data..."
slot_version = self.slot_data.get("custom_bt_data", {}).get("version", "N/A")
assert version == slot_version, (
"Your Banjo-Tooie AP does not match with the generated world.\n" +
f"Your version: {version} | Generated version: {self.slot_data['custom_bt_data']['version']}"
)
self.deathlink_enabled = bool(self.slot_data["options"]["death_link"])
self.taglink_enabled = bool(self.slot_data["options"]["tag_link"])
self.n64_sync_task = asyncio.create_task(n64_sync_task(self), name="N64 Sync")
elif cmd == "ReceivedItems":
self.tracker.refresh_items()
if self.startup == False:
for item in args["items"]:
player = ""
item_name = ""
for (i, name) in self.player_names.items():
if i == item.player:
player = name
break
for (name, i) in bt_itm_name_to_id.items():
if item.item == i:
item_name = name
break
logger.info(player + " sent " + item_name)
logger.info("The above items will be sent when Banjo-Tooie is loaded.")
self.startup = True
if isinstance(args.get("data", {}), dict):
source_name = args.get("data", {}).get("source", None)
if not hasattr(self, "instance_id"):
self.instance_id = time.time()
if "TagLink" in self.tags and source_name != self.instance_id and "TagLink" in args.get("tags", []):
if not hasattr(self, "pending_tag_link"):
self.pending_tag_link = False
else:
self.pending_tag_link = True
def on_print_json(self, args: dict[Any, Any]):
if self.ui:
self.ui.print_json(copy.deepcopy(args["data"]))
relevant = args.get("type") == "ItemSend"
if relevant:
relevant = False
item = args["item"]
if self.slot_concerns_self(args["receiving"]): # pyright: ignore[reportUnknownMemberType]
relevant = True
elif self.slot_concerns_self(item.player): # pyright: ignore[reportUnknownMemberType]
relevant = True
if relevant == True:
player = self.player_names[int(args["data"][0]["text"])]
to_player = self.player_names[int(args["data"][0]["text"])]
for id, data in enumerate(args["data"]):
if id == 0:
continue
if "type" in data and data["type"] == "player_id":
to_player = self.player_names[int(data["text"])]
break
item_name = self.item_names.lookup_in_slot(int(args["data"][2]["text"]))
self.set_message({"player":player, "item":item_name, "item_id":int(args["data"][2]["text"]), "to_player":to_player })
else:
text = self.jsontotextparser(copy.deepcopy(args["data"]))
logger.info(text)
relevant = args.get("type") == "ItemSend"
if relevant:
player = self.player_names[int(args["data"][0]["text"])]
to_player = self.player_names[int(args["data"][0]["text"])]
for id, data in enumerate(args["data"]):
if id == 0:
continue
if "type" in data and data["type"] == "player_id":
to_player = self.player_names[int(data["text"])]
break
item_name = self.item_names.lookup_in_slot(int(args["data"][2]["text"]))
self.set_message({"player":player, "item":item_name, "item_id":int(args["data"][2]["text"]), "to_player":to_player})
def get_payload(ctx: BanjoTooieContext):
if ctx.deathlink_enabled and ctx.deathlink_pending:
trigger_death = True
ctx.deathlink_sent_this_death = True
ctx.deathlink_pending = False
else:
trigger_death = False
if ctx.taglink_enabled and ctx.pending_tag_link:
trigger_tag = True
ctx.taglink_sent_this_tag = True
ctx.pending_tag_link = False
else:
trigger_tag = False
# if(len(ctx.items_received) > 0) and ctx.sync_ready == True:
# print("Receiving Item")
if ctx.sync_ready == True:
ctx.startup = True
payload = json.dumps({
"items": [item.item for item in ctx.items_received],
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
"triggerDeath": trigger_death,
"triggerTag": trigger_tag,
"messages": [message for (i, message) in ctx.messages.items() if i != 0],
})
else:
payload = json.dumps({
"items": [],
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
"triggerDeath": trigger_death,
"triggerTag": trigger_tag,
"messages": [message for (i, message) in ctx.messages.items() if i != 0],
})
if len(ctx.messages) > 0:
ctx.messages = {}
# if len(ctx.items_received) > 0 and ctx.sync_ready == True:
# ctx.items_received = []
return payload
def get_slot_payload(ctx: BanjoTooieContext):
payload = json.dumps({
"slot_player": ctx.slot_data["custom_bt_data"]["player_name"],
"slot_seed": ctx.slot_data["custom_bt_data"]["seed"],
"slot_deathlink": ctx.deathlink_enabled,
"slot_taglink": ctx.taglink_enabled,
"slot_tower_of_tragedy": ctx.slot_data["options"]["tower_of_tragedy"],
"slot_randomize_bk_moves": ctx.slot_data["options"]["randomize_bk_moves"],
"slot_speed_up_minigames": ctx.slot_data["options"]["speed_up_minigames"],
"slot_skip_puzzles": ctx.slot_data["options"]["skip_puzzles"],
"slot_backdoors": ctx.slot_data["options"]["backdoors"],
"slot_open_hag1": ctx.slot_data["options"]["open_hag1"],
"slot_randomize_chuffy": ctx.slot_data["options"]["randomize_chuffy"],
"slot_world_requirements": ctx.slot_data["custom_bt_data"]["world_requirements"], #??
"slot_world_order": ctx.slot_data["custom_bt_data"]["world_order"], #??
"slot_skip_klungo": ctx.slot_data["options"]["skip_klungo"],
"slot_victory_condition": ctx.slot_data["options"]["victory_condition"],
"slot_minigame_hunt_length": ctx.slot_data["options"]["minigame_hunt_length"],
"slot_boss_hunt_length": ctx.slot_data["options"]["boss_hunt_length"],
"slot_jinjo_family_rescue_length": ctx.slot_data["options"]["jinjo_family_rescue_length"],
"slot_token_hunt_length": ctx.slot_data["options"]["token_hunt_length"],
"slot_version": version,
"slot_silo_costs": ctx.slot_data["custom_bt_data"]["jamjars_silo_costs"],
"slot_preopened_silo": ctx.slot_data["custom_bt_data"]["preopened_silos_ids"],
"slot_randomize_warp_pads": ctx.slot_data["options"]["randomize_warp_pads"],
"slot_randomize_silos": ctx.slot_data["options"]["randomize_silos"],
"slot_zones": ctx.slot_data["custom_bt_data"]["loading_zones"],
"slot_dialog_character": ctx.slot_data["options"]["dialog_character"],
"slot_nestsanity": ctx.slot_data["options"]["nestsanity"],
"slot_hints": ctx.slot_data["custom_bt_data"]["hints"],
"slot_hints_activated": ctx.slot_data["options"]["signpost_hints"],
"slot_extra_cheats": ctx.slot_data["options"]["extra_cheats"],
"slot_easy_canary": ctx.slot_data["options"]["easy_canary"],
"slot_randomize_signposts": ctx.slot_data["options"]["randomize_signposts"],
"slot_auto_enable_cheats": ctx.slot_data["options"]["auto_enable_cheats"],
"slot_cheato_rewards": ctx.slot_data["options"]["cheato_rewards"],
"slot_honeyb_rewards": ctx.slot_data["options"]["honeyb_rewards"],
"slot_open_gi_entrance": ctx.slot_data["options"]["open_gi_frontdoor"],
"slot_randomize_tickets": ctx.slot_data["options"]["randomize_tickets"],
"slot_randomize_green_relics": ctx.slot_data["options"]["randomize_green_relics"],
"slot_randomize_beans": ctx.slot_data["options"]["randomize_beans"]
})
ctx.sendSlot = False
return payload
class Payload(TypedDict):
playerName: str
deathlinkActive: bool
taglinkActive: bool
DEMO: Any
chuffy: dict[str, bool]
treble: dict[str, bool]
stations: dict[str, bool]
mystery: dict[str, bool]
roysten: dict[str, bool]
jinjofam: dict[str, bool]
jinjos: dict[str, bool]
pages: dict[str, bool]
honeycomb: dict[str, bool]
glowbo: dict[str, bool]
doubloon: dict[str, bool]
notes: dict[str, bool]
unlocked_moves: dict[str, bool]
hag: Any
cheato_rewards: dict[str, bool]
honeyb_rewards: dict[str, bool]
jiggy_chunks: dict[str, bool]
goggles: Any
jiggies: dict[str, bool]
dino_kids: dict[str, bool]
boggy_kids: dict[str, bool]
alien_kids: dict[str, bool]
skivvies: dict[str, bool]
fit_events: dict[str, bool]
nests: dict[str, bool]
roar: Any
signposts: dict[str, bool]
warppads: dict[str, bool]
silos: dict[str, bool]
worlds: dict[str, bool]
banjo_map: Any
bt_tickets: dict[str, bool]
green_relics: dict[str, bool]
beans: dict[str, bool]
isDead: bool
isTag: bool
async def parse_payload(payload: Payload, ctx: BanjoTooieContext, force: bool):
# Refuse to do anything if ROM is detected as changed
if ctx.auth and payload["playerName"] != ctx.auth:
logger.warning("ROM change detected. Disconnecting and reconnecting...")
ctx.deathlink_enabled = False
ctx.deathlink_client_override = False
ctx.deathlink_pending = False
ctx.deathlink_sent_this_death = False
ctx.taglink_enabled = False
ctx.taglink_client_override = False
ctx.pending_tag_link = False
ctx.taglink_sent_this_tag = False
ctx.finished_game = False
ctx.location_table = {}
ctx.chuffy_table = {}
ctx.movelist_table = {}
ctx.auth = payload["playerName"]
await ctx.send_connect()
return
# Turn on deathlink if it is on, and if the client hasn't overriden it
if payload["deathlinkActive"] and ctx.deathlink_enabled and not ctx.deathlink_client_override:
await ctx.update_death_link(True)
ctx.deathlink_enabled = True
# Turn on taglink if it is on, and if the client hasn't overriden it
if payload["taglinkActive"] and ctx.taglink_enabled and not ctx.taglink_client_override:
await ctx.update_tag_link(True)
ctx.taglink_enabled = True
# Locations handling
demo = payload["DEMO"]
chuffy = payload["chuffy"]
treblelist = payload["treble"]
stationlist = payload["stations"]
mystery = payload["mystery"]
roystenlist = payload["roysten"]
jinjofamlist = payload["jinjofam"]
jinjolist = payload["jinjos"]
pageslist = payload["pages"]
honeycomblist = payload["honeycomb"]
glowbolist = payload["glowbo"]
doubloonlist = payload["doubloon"]
noteslist = payload["notes"]
movelist = payload["unlocked_moves"]
hag = payload["hag"]
cheatorewardslist = payload["cheato_rewards"]
honeybrewardslist = payload["honeyb_rewards"]
jiggychunklist = payload["jiggy_chunks"]
goggles = payload["goggles"]
jiggylist = payload["jiggies"]
dino_kids = payload["dino_kids"]
boggy_kids = payload["boggy_kids"]
alien_kids = payload["alien_kids"]
skivvies = payload["skivvies"]
mr_fit = payload["fit_events"]
nests = payload["nests"]
roar_obtain = payload["roar"]
signposts = payload["signposts"]
warp_pads = payload["warppads"]
silos = payload["silos"]
worldslist = payload["worlds"]
banjo_map = payload["banjo_map"]
bt_tickets = payload["bt_tickets"]
green_relics = payload["green_relics"]
beans = payload["beans"]
# The Lua JSON library serializes an empty table into a list instead of a dict. Verify types for safety:
# if isinstance(locations, list):
# locations = {}
if isinstance(chuffy, list):
chuffy = {}
if isinstance(treblelist, list):
treblelist = {}
if isinstance(stationlist, list):
stationlist = {}
if isinstance(mystery, list):
mystery = {}
if isinstance(roystenlist, list):
roystenlist = {}
if isinstance(jinjofamlist, list):
jinjofamlist = {}
if isinstance(cheatorewardslist, list):
cheatorewardslist = {}
if isinstance(honeybrewardslist, list):
honeybrewardslist = {}
if isinstance(jiggychunklist, list):
jiggychunklist = {}
if isinstance(worldslist, list):
worldslist = {}
if isinstance(dino_kids, list):
dino_kids = {}
if isinstance(boggy_kids, list):
boggy_kids = {}
if isinstance(alien_kids, list):
alien_kids = {}
if isinstance(skivvies, list):
skivvies = {}
if isinstance(mr_fit, list):
mr_fit = {}
if isinstance(nests, list):
nests = {}
if isinstance(signposts, list):
signposts = {}
if not isinstance(goggles, bool):
goggles = False
if not isinstance(demo, bool):
demo = True
if not isinstance(banjo_map, int):
banjo_map = 0
if isinstance(jiggylist, list):
jiggylist = {}
if isinstance(jinjolist, list):
jinjolist = {}
if isinstance(movelist, list):
movelist = {}
if isinstance(pageslist, list):
pageslist = {}
if isinstance(honeycomblist, list):
honeycomblist = {}
if isinstance(glowbolist, list):
glowbolist = {}
if isinstance(doubloonlist, list):
doubloonlist = {}
if isinstance(noteslist, list):
noteslist = {}
if isinstance(warp_pads, list):
warp_pads = {}
if isinstance(silos, list):
silos = {}
if not isinstance(hag, bool):
hag = False
if not isinstance(roar_obtain, bool):
roar_obtain = False
if isinstance(bt_tickets, list):
bt_tickets = {}
if isinstance(green_relics, list):
green_relics = {}
if isinstance(beans, list):
beans = {}
if demo == False and ctx.sync_ready == True:
locs1: list[int] = []
create_hints_params: List[CreateHintsParams] = []
if ctx.chuffy_table != chuffy:
ctx.chuffy_table = chuffy
for locationId, value in chuffy.items():
if value == True:
locs1.append(int(locationId))
if ctx.treblelist_table != treblelist:
ctx.treblelist_table = treblelist
for locationId, value in treblelist.items():
if value == True:
locs1.append(int(locationId))
if ctx.stationlist_table != stationlist:
ctx.stationlist_table = stationlist
for locationId, value in stationlist.items():
if value == True:
locs1.append(int(locationId))
if ctx.mystery_table != mystery:
ctx.mystery_table = mystery
for locationId, value in mystery.items():
if locationId == "REMOVE": #Don't need to handle this here
continue
if value == True:
locs1.append(int(locationId))
if ctx.roystenlist_table != roystenlist:
ctx.roystenlist_table = roystenlist
for locationId, value in roystenlist.items():
if value == True:
locs1.append(int(locationId))
if ctx.jiggychunks_table != jiggychunklist:
ctx.jiggychunks_table = jiggychunklist
for locationId, value in jiggychunklist.items():
if value == True:
locs1.append(int(locationId))
if ctx.goggles_table != goggles:
ctx.goggles_table = goggles
if goggles == True:
locs1.append(1231005)
if ctx.dino_kids_table != dino_kids:
ctx.dino_kids_table = dino_kids
for locationId, value in dino_kids.items():
if value == True:
locs1.append(int(locationId))
if ctx.boggy_kids_table != boggy_kids:
ctx.boggy_kids_table = boggy_kids
for locationId, value in boggy_kids.items():
if value == True:
locs1.append(int(locationId))
if ctx.alien_kids_table != alien_kids:
ctx.alien_kids_table = alien_kids
for locationId, value in alien_kids.items():
if value == True:
locs1.append(int(locationId))
if ctx.skivvies_table != skivvies:
ctx.skivvies_table = skivvies
for locationId, value in skivvies.items():
if value == True:
locs1.append(int(locationId))
if ctx.mr_fit_table != mr_fit:
ctx.mr_fit_table = mr_fit
for locationId, value in mr_fit.items():
if value == True:
locs1.append(int(locationId))
if ctx.nests_table != nests:
ctx.nests_table = nests
for locationId, value in nests.items():
if value == True:
locs1.append(int(locationId))
if ctx.current_map != banjo_map: #Fix for not resending activated Hints
ctx.signpost_table = signposts #sets only when transistioning on a new map
if ctx.signpost_table != signposts:
ctx.signpost_table = signposts
actual_hints = ctx.slot_data["custom_bt_data"]["hints"]
for locationId, value in signposts.items():
if value == True:
locs1.append(int(locationId))
hint = actual_hints.get(str(locationId), None)
if not hint is None and hint.get('should_add_hint')\
and not hint.get('location_id') is None\
and not hint.get('location_player_id') is None:
player_id = hint['location_player_id']
location_id = hint['location_id']
params = CreateHintsParams(location_id, player_id)
if not params in ctx.handled_scouts:
create_hints_params.append(params)
if ctx.warppads_table != warp_pads:
ctx.warppads_table = warp_pads
for locationId, value in warp_pads.items():
if value == True:
locs1.append(int(locationId))
if ctx.silos_table != silos:
ctx.silos_table = silos
for locationId, value in silos.items():
if value == True:
locs1.append(int(locationId))
if ctx.bt_tickets_table != bt_tickets:
ctx.bt_tickets_table = bt_tickets
for locationId, value in bt_tickets.items():
if value == True:
locs1.append(int(locationId))
if ctx.green_relics_table != green_relics:
ctx.green_relics_table = green_relics
for locationId, value in green_relics.items():
if value == True:
locs1.append(int(locationId))
if ctx.beans_table != beans:
ctx.beans_table = beans
for locationId, value in beans.items():
if value == True:
locs1.append(int(locationId))
if ctx.roar != roar_obtain:
ctx.roar = roar_obtain
if roar_obtain == True:
locs1.append(1231009)
if ctx.jiggy_table != jiggylist:
ctx.jiggy_table = jiggylist
for locationId, value in jiggylist.items():
if value == True:
locs1.append(int(locationId))
if ctx.jinjolist_table != jinjolist:
ctx.jinjolist_table = jinjolist
for locationId, value in jinjolist.items():
if value == True:
locs1.append(int(locationId))
if ctx.pages_table != pageslist:
ctx.pages_table = pageslist
for locationId, value in pageslist.items():
if value == True:
locs1.append(int(locationId))
if ctx.honeycomb_table != honeycomblist:
ctx.honeycomb_table = honeycomblist
for locationId, value in honeycomblist.items():
if value == True:
locs1.append(int(locationId))
if ctx.glowbo_table != glowbolist:
ctx.glowbo_table = glowbolist
for locationId, value in glowbolist.items():
if value == True:
locs1.append(int(locationId))
if ctx.doubloon_table != doubloonlist:
ctx.doubloon_table = doubloonlist
for locationId, value in doubloonlist.items():
if value == True:
locs1.append(int(locationId))
if ctx.notes_table != noteslist:
ctx.notes_table = noteslist
for locationId, value in noteslist.items():
if value == True:
locs1.append(int(locationId))
if ctx.movelist_table != movelist:
ctx.movelist_table = movelist
for locationId, value in movelist.items():
if value == True:
locs1.append(int(locationId))
if ctx.jinjofamlist_table != jinjofamlist:
ctx.jinjofamlist_table = jinjofamlist
for locationId, value in jinjofamlist.items():
if value == True:
locs1.append(int(locationId))
if ctx.cheatorewardslist_table != cheatorewardslist:
ctx.cheatorewardslist_table = cheatorewardslist
for locationId, value in cheatorewardslist.items():
if value == True:
locs1.append(int(locationId))
if ctx.honeybrewardslist_table != honeybrewardslist:
ctx.honeybrewardslist_table = honeybrewardslist
for locationId, value in honeybrewardslist.items():
if value == True:
locs1.append(int(locationId))
if ctx.slot_data["options"]["skip_puzzles"] == 1:
if ctx.worldlist_table != worldslist:
ctx.worldlist_table = worldslist
for locationId, value in worldslist.items():
if value == True:
locs1.append(int(locationId))
#Mumbo Tokens
if ctx.slot_data["options"]["victory_condition"] == 1 or ctx.slot_data["options"]["victory_condition"] == 2 or \
ctx.slot_data["options"]["victory_condition"] == 3 or ctx.slot_data["options"]["victory_condition"] == 4 or \
ctx.slot_data["options"]["victory_condition"] == 6:
locs1 = mumbo_tokens_loc(locs1, ctx.slot_data["options"]["victory_condition"])
if len(locs1) > 0:
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": locs1
}])
for params in create_hints_params:
await ctx.send_msgs([{
"cmd": "CreateHints",
"locations": [params.location],
"player": params.player
}])
ctx.handled_scouts.append(params)
#GAME VICTORY
#Beat Hag-1
if hag == True and (ctx.slot_data["options"]["victory_condition"] == 0 or ctx.slot_data["options"]["victory_condition"] == 4 or\
ctx.slot_data["options"]["victory_condition"] == 6) and not ctx.finished_game:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": 30
}])
ctx.finished_game = True
ctx.set_message("You have completed your goal")
#Mumbo Tokens
if (ctx.slot_data["options"]["victory_condition"] == 1 or ctx.slot_data["options"]["victory_condition"] == 2) and not ctx.finished_game:
mumbo_tokens = 0
for networkItem in ctx.items_received:
if networkItem.item == 1230798:
mumbo_tokens += 1
if ((ctx.slot_data["options"]["victory_condition"] == 1 and mumbo_tokens >= ctx.slot_data["options"]["minigame_hunt_length"]) or
(ctx.slot_data["options"]["victory_condition"] == 2 and mumbo_tokens >= ctx.slot_data["options"]["boss_hunt_length"]) or
(ctx.slot_data["options"]["victory_condition"] == 3 and mumbo_tokens >= ctx.slot_data["options"]["jinjo_family_rescue_length"])):
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": 30
}])
ctx.finished_game = True
ctx.set_message("You have completed your goal")
if (ctx.current_map == 401 and ctx.slot_data["options"]["victory_condition"] == 5 and not ctx.finished_game):
mumbo_tokens = 0
for networkItem in ctx.items_received:
if networkItem.item == 1230798:
mumbo_tokens += 1
if (mumbo_tokens >= ctx.slot_data["options"]["token_hunt_length"]):
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": 30
}])
ctx.finished_game = True
if (ctx.current_map == 401 and ctx.slot_data["options"]["victory_condition"] == 3 and not ctx.finished_game):
mumbo_tokens = 0
for networkItem in ctx.items_received:
if networkItem.item == 1230798:
mumbo_tokens += 1
if (mumbo_tokens >= ctx.slot_data["options"]["jinjo_family_rescue_length"]):
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": 30
}])
ctx.finished_game = True
# Ozone & Mia's Banjo-Tooie Tracker
if ctx.current_map != banjo_map:
ctx.current_map = banjo_map
await ctx.send_msgs([{
"cmd": "Set",
"key": f"Banjo_Tooie_{ctx.team}_{ctx.slot}_map",
"default": hex(0),
"want_reply": False,
"operations": [{"operation": "replace",
"value": hex(banjo_map)}]
}])
#Send Sync Data.
if "sync_ready" in payload and payload["sync_ready"] == "true" and ctx.sync_ready == False:
# ctx.items_handling = 0b101
# await ctx.send_connect()
ctx.sync_ready = True
# Deathlink handling
if ctx.deathlink_enabled:
if payload["isDead"]: #Banjo died
ctx.deathlink_pending = False
if not ctx.deathlink_sent_this_death:
ctx.deathlink_sent_this_death = True
await ctx.send_death()
else: # Banjo is somehow still alive
ctx.deathlink_sent_this_death = False
if ctx.taglink_enabled:
if payload["isTag"]: #Banjo tagged
ctx.pending_tag_link = False
if not ctx.taglink_sent_this_tag:
ctx.taglink_sent_this_tag = True
await ctx.send_tag_link()
else:
ctx.taglink_sent_this_tag = False
def mumbo_tokens_loc(locs: list[int], goaltype: int) -> list[int]:
for locationId in locs:
if goaltype == 1 or goaltype == 4:
if locationId == 1230598: #MT
locs.append(1230968)
if locationId == 1230610: #GM
locs.append(1230969)
if locationId == 1230616: #WW
locs.append(1230970)
if locationId == 1230617: #WW
locs.append(1230971)
if locationId == 1230619: #WW
locs.append(1230972)
if locationId == 1230620: #WW
locs.append(1230973)
if locationId == 1230626: #JRL
locs.append(1230974)
if locationId == 1230641: #TDL
locs.append(1230975)
if locationId == 1230648: #GI
locs.append(1230976)
if locationId == 1230654: #GI
locs.append(1230977)
if locationId == 1230663: #HFP
locs.append(1230978)
if locationId == 1230668: #CCL
locs.append(1230979)
if locationId == 1230670: #CCL
locs.append(1230980)
if locationId == 1230673: #CCL
locs.append(1230981)
if locationId == 1230749: #CCL
locs.append(1230982)
if goaltype == 2 or goaltype == 4 or goaltype == 6:
if locationId == 1230596: #MT
locs.append(1230960)
if locationId == 1230606: #GGM
locs.append(1230961)
if locationId == 1230618: #WW
locs.append(1230962)
if locationId == 1230632: #JRL
locs.append(1230963)
if locationId == 1230639: #TDL
locs.append(1230964)
if locationId == 1230745: #GI
locs.append(1230965)
if locationId == 1230656: #HFP
locs.append(1230966)
if locationId == 1230666: #CC
locs.append(1230967)
if goaltype == 3 or goaltype == 4:
if locationId == 1230676: #JINJOFAM
locs.append(1230983)
if locationId == 1230677: #JINJOFAM
locs.append(1230984)
if locationId == 1230678: #JINJOFAM
locs.append(1230985)
if locationId == 1230679: #JINJOFAM
locs.append(1230986)
if locationId == 1230680: #JINJOFAM
locs.append(1230987)
if locationId == 1230681: #JINJOFAM
locs.append(1230988)
if locationId == 1230682: #JINJOFAM
locs.append(1230989)
if locationId == 1230683: #JINJOFAM
locs.append(1230990)
if locationId == 1230684: #JINJOFAM
locs.append(1230991)
return locs
async def n64_sync_task(ctx: BanjoTooieContext):
logger.info("Starting n64 connector. Use /n64 for status information.")
while not ctx.exit_event.is_set():
error_status = None
if ctx.n64_streams:
(reader, writer) = ctx.n64_streams
if ctx.sendSlot == True:
msg = get_slot_payload(ctx).encode()
else:
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
reported_version = data_decoded.get("scriptVersion", 0)
getSlotData = data_decoded.get("getSlot", 0)
if getSlotData == True:
ctx.sendSlot = True
elif reported_version >= script_version:
if ctx.game is not None and "jiggies" in data_decoded:
# Not just a keep alive ping, parse
async_start(parse_payload(data_decoded, ctx, False))
if not ctx.auth:
ctx.auth = data_decoded["playerName"]
if ctx.awaiting_rom:
await ctx.server_auth(False)
else:
if not ctx.version_warning:
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
"Please update to the latest version. "
"Your connection to the Archipelago server will not be accepted.")
ctx.version_warning = True
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.n64_streams = None
except ConnectionResetError:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.n64_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.n64_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.n64_streams = None
if ctx.n64_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to N64")
ctx.n64_status = CONNECTION_CONNECTED_STATUS
else:
ctx.n64_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.n64_status = error_status
logger.info("Lost connection to N64 and attempting to reconnect. Use /n64 for status updates")
else:
try:
logger.debug("Attempting to connect to N64")
ctx.n64_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 21221), timeout=10)
ctx.n64_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.n64_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.n64_status = CONNECTION_REFUSED_STATUS
continue
except OSError:
logger.debug("Connection Failed, Trying Again")
ctx.n64_status = CONNECTION_REFUSED_STATUS
continue
except Exception as error:
logger.info("Unknown Error: %r", error)
ctx.n64_status = CONNECTION_REFUSED_STATUS
break
@atexit.register
def close_program():
global program
if program and program.poll() == None:
program.kill()
program = None
def main():
Utils.init_logging("Banjo-Tooie Client")
parser = get_base_parser()
args = sys.argv[1:] # the default for parse_args()
if "Banjo-Tooie Client" in args:
args.remove("Banjo-Tooie Client")
args = parser.parse_args(args)
async def _main():
multiprocessing.freeze_support()
ctx = BanjoTooieContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.n64_sync_task:
await ctx.n64_sync_task
import colorama
colorama.init()
asyncio.run(_main())
colorama.deinit()
if __name__ == "__main__":
main()