Files
dockipelago/worlds/cv_dos/Client.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

183 lines
7.5 KiB
Python

from typing import TYPE_CHECKING
from NetUtils import ClientStatus
from .in_game_data import location_ram_table, global_soul_table, world_version, button_item_table
from .static_location_data import location_ids
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
import time
import struct
import asyncio
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
class DoSClient(BizHawkClient):
game = "Castlevania: Dawn of Sorrow"
system = ("NDS")
patch_suffix = ".apcvdos"
most_recent_connect: str = ""
client_version: str = world_version
has_received_death: bool = False
state_is_dying: int = 0
has_reset_from_death: bool = True
def __init__(self) -> None:
super().__init__()
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
try:
# Check ROM name/patch version
validation_data = await bizhawk.read(ctx.bizhawk_ctx, [(0x0, 18, "ROM"), (0x02F6DD7C, 16, "ROM"), (0x02F6DD8D, 1, "ROM")])
base_rom_name = validation_data[0].decode("ascii") # AP ROM name
if not base_rom_name.startswith("CASTLEVANIA1ACVEA4"):
print("A!!!!")
print(base_rom_name)
return False
# This is a DoS ROM
patch_version = validation_data[1].rstrip(b"\x69") # APworld version
patch_version = patch_version.decode("ascii")
if patch_version != self.client_version:
if patch_version != self.most_recent_connect:
# We only want to display this error once
ctx.gui_error("Bad Version", f"Installed Dawn of Sorrow APworld version {self.client_version} does not match patch version {patch_version}")
self.most_recent_connect = patch_version
return False
except UnicodeDecodeError:
return False
except bizhawk.RequestFailedError:
return False # Should verify on the next pass
death_link_flag = int.from_bytes(validation_data[2])
if death_link_flag:
await ctx.update_death_link(True)
ctx.game = self.game
ctx.items_handling = 0b101
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
slot_name_bytes = await bizhawk.read(
ctx.bizhawk_ctx, [(0x2F6DD50, 0x14, "ROM")])
slot_name_bytes = slot_name_bytes[0].rstrip(b'\xFF')
ctx.auth = slot_name_bytes.decode("ascii")
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
if cmd != "Bounced":
return
if "tags" not in args:
return
if "DeathLink" in args["tags"]:
self.has_received_death = True
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from CommonClient import logger
from .in_game_data import location_ram_table
if ctx.server_version.build > 0:
ctx.connected = True
else:
ctx.connected = False
ctx.refresh_connect = True
if ctx.slot_data is not None:
ctx.data_present = True
else:
ctx.data_present = False
if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None:
return
read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x0F7190, 0x10, "Main RAM"), # Check table
(0x0F7257, 0x01, "Main RAM"), # Game Mode
(0x11504C, 0x01, "Main RAM"), # Current Map
(0x0F703C, 0x04, "Main RAM"), # Gameplay timer. Will be 0 if not in game
(0x308930, 0x20, "Main RAM"), # AP data
(0x0F6DFC, 0x01, "Main RAM")]) # Game state, we only care about the Dead flag
location_flag_table = bytearray(read_state[0])
game_mode = int.from_bytes(read_state[1], "little")
cur_map = int.from_bytes(read_state[2], "little")
game_timer = int.from_bytes(read_state[3], "little")
ap_data = bytearray(read_state[4])
death_state = int.from_bytes(read_state[5])
soul_flag_table = list(ap_data[:0x10])
button_items = ap_data[0x13]
current_received_item = ap_data[0x10]
total_items_received = int.from_bytes(ap_data[0x1E:0x20], "little")
if "DeathLink" in ctx.tags:
await self.handle_deathlink(death_state, ctx)
new_checks = []
if game_mode == 1: # Ignore AP handling if the game is in Julius mode
return
if not game_timer: # The in-game itmer is only 0 when not in-game
return
for location_name in location_ids:
loc_id = location_ids[location_name]
if loc_id not in ctx.locations_checked:
if location_name in global_soul_table:
index = global_soul_table.index(location_name)
bit = 1 << (index % 8)
offset = int(index / 8)
location = soul_flag_table[offset]
elif location_name in button_item_table:
bit = 1 << button_item_table.index(location_name)
location = button_items
else:
pointer = location_ram_table[location_name][0]
bit = location_ram_table[location_name][1]
location = location_flag_table[pointer]
if location & bit:
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)
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}])
if total_items_received < len(ctx.items_received) and current_received_item == 0:
item = ctx.items_received[total_items_received]
total_items_received += 1
item_data = struct.pack(">H", item.item)
await bizhawk.write(ctx.bizhawk_ctx, [(0x308940, item_data, "Main RAM")])
await bizhawk.write(ctx.bizhawk_ctx, [(0x30894E, bytes([total_items_received]), "Main RAM")])
if not ctx.finished_game and cur_map == 0x0D: # Map 0x0D is used for the Epilogue. If we're here, trigger goal
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
async def handle_deathlink(self, current_death_state, ctx):
if current_death_state & 0x40: # If the player is currently dead
if self.has_received_death: # This is the death that we just got from the server
self.has_received_death = False
self.has_reset_from_death = False
else: # Received death is false, meaning the player actually died here
if self.has_reset_from_death: # We only want this to run once per death
await ctx.send_death(f"{ctx.player_names[ctx.slot]} died!")
self.has_reset_from_death = False
else:
if self.has_received_death:
# Kill the player
await bizhawk.write(ctx.bizhawk_ctx, [(0x308AAC, int.to_bytes(0x01), "Main RAM")])
else:
# This should be normal gameplay after relaoding
self.has_reset_from_death = True