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

525 lines
18 KiB
Python

import os
import typing
import threading
import pkgutil
from typing import List, Set, Dict, TextIO
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
import settings
from .Items import get_item_names_per_category, soul_filler_table, item_table, consumable_table, money_table
from .Locations import get_locations
from .Regions import init_areas
from .Options import DoSOptions, dos_option_groups, SoulsanityLevel, SoulRandomizer
from .Rules import set_location_rules
from .Client import DoSClient
from .Rom import DoSProcPatch, patch_rom
from .static_location_data import location_ids
from .setup_game import place_static_items, setup_game, place_static_souls
class DoSWeb(WebWorld):
theme = "ocean"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Dawn of Sorrow randomizer"
"and connecting to an Archipelago server.",
"English",
"setup_en.md",
"setup/en",
["Pink Switch"]
)
option_groups = dos_option_groups
tutorials = [setup_en]
class CVDoSItem(Item):
game: str = "Castlevania: Dawn of Sorrow"
class DoSSettings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the Castlevania: Dawn of Sorrow ROM file."""
description = "Dawn of Sorrow ROM File"
copy_to = "CASTLEVANIA1_ACVEA4_00.nds"
md5 = "cc0f25b8783fb83cb4588d1c111bdc18"
rom_file: RomFile = RomFile(RomFile.copy_to)
class DoSWorld(World):
"""One year after the events of Aria, Soma is targetted by a recently emerged cult.
Having rejected his fate, the cult seeks to create a new Dark Lord in his stead.
Explore a new castle and defeat the Dark Lord Candidates!"""
game = "Castlevania: Dawn of Sorrow"
option_definitions = DoSOptions
data_version = 1
required_client_version = (0, 6, 0)
origin_region_name = "Lost Village Upper"
item_name_to_id = {item: item_table[item].code for item in item_table}
location_name_to_id = location_ids
item_name_groups = get_item_names_per_category()
web = DoSWeb()
settings: typing.ClassVar[DoSSettings]
# topology_present = True
ut_can_gen_without_yaml = True
options_dataclass = DoSOptions
options: DoSOptions
locked_locations: List[str]
location_cache: List[Location]
def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
super().__init__(multiworld, player)
self.locked_locations = []
self.location_cache = []
self.extra_item_count = 0
self.has_tried_chaos_ring = False
self.starting_warp_room = None
self.armor_table = [
"Casual Clothes",
"Cloth Tunic",
"Gym Clothes",
"Kung Fu Suit",
"Biker Jacket",
"War Fatigues",
"Ninja Suit",
"Three 7s",
"Justaucorps",
"Army Jacket",
"Pitch Black Suit",
"Olrox's Suit",
"Dracula's Tunic",
"Leather Armor",
"Breastplate",
"Ring Mail",
"Scale Mail",
"Chain Mail",
"Hauberk",
"Cuirass",
"Blocking Mail",
"Eversing",
"Demon's Mail",
"Silk Robe",
"Mage Robe",
"Elfin Robe",
"Wyrm Robe",
"Aquarius",
"Serenity Robe",
"Death's Robe",
"Cape",
"Traveler Cape",
"Crimson Cloak",
"Black Cloak",
"Pendant",
"Heart Pendant",
"Skull Necklace",
"Flame Necklace",
"Rosary",
"Scarf",
"Red Scarf",
"Neck Warmer",
"Power Belt",
"Black Belt",
"Megingiord",
"Hoop Earring",
"Turquoise Stud",
"Silver Stud",
"Gold Stud",
"Bloody Stud",
"Platinum Stud",
"Tear Of Blood",
"Lucky Charm",
"Satan's Ring",
"Rare Ring",
"Soul Eater Ring",
"Rune Ring",
"Shaman Ring",
"Gold Ring"
]
self.weapon_table = [
"Knife",
"Combat Knife",
"Baselard",
"Cutall",
"Cinquedia",
"Rapier",
"Fleuret",
"Main Gauche",
"Small Sword",
"Estoc",
"Whip Sword",
"Garian Sword",
"Kris Naga",
"Nebula",
"Short Sword",
"Cutlass",
"Long Sword",
"Fragarach",
"Hrunting",
"Mystletain",
"Joyeuse",
"Milican's Sword",
"Ice Brand",
"Laevatain",
"Burtgang",
"Kaladbolg",
"Valmanway",
"Claymore",
"Falchion",
"Great Sword",
"Durandal",
"Dainslef",
"Ascalon",
"Balmung",
"Final Sword",
"Claimh Solais",
"Spear",
"Partizan",
"Halberd",
"Lance",
"Trident",
"Brionac",
"Geiborg",
"Longinus",
"Gungner",
"Mace",
"Morgenstern",
"Mjollnjr",
"Axe",
"Battle Axe",
"Bhuj",
"Great Axe",
"Golden Axe",
"Death Scythe",
"Blunt Sword",
"Katana",
"Kotetsu",
"Masamune",
"Osafune",
"Kunitsuna",
"Yasutsuna",
"Muramasa",
"Brass Knuckles",
"Cestus",
"Whip Knuckle",
"Mach Punch",
"Kaiser Knuckle",
"Handgun",
"Silver Gun",
"Boomerang",
"Chakram",
"Tomahawk",
"Throwing Sickle",
"RPG",
"Terror Bear",
"Nunchakus"
]
self.common_souls = {
"Axe Armor Soul",
"Warg Soul",
"Spin Devil Soul",
"Slime Soul",
"Corpseweed Soul",
"Yeti Soul",
"Flying Humanoid Soul",
"Buer Soul",
"Guillotiner Soul",
"Cave Troll Soul",
"Merman Soul",
"Homunculus Soul",
"Decarabia Soul",
"Dead Mate Soul",
"Mothman Soul"
}
self.uncommon_souls = {
"Zombie Soul",
"Bat Soul",
"Skeleton Soul",
"Skull Archer Soul",
"Armor Knight Soul",
"Student Witch Soul",
"Slaughterer Soul",
"Bomber Armor Soul",
"Golem Soul",
"Une Soul",
"Manticore Soul",
"Mollusca Soul",
"Rycuda Soul",
"Mandragora Soul",
"Yorick Soul",
"Catoblepas Soul",
"Ghost Dancer Soul",
"Mini Devil Soul",
"Quetzalcoatl Soul",
"Amalaric Sniper Soul",
"Great Armor Soul",
"Waiter Skeleton Soul",
"Persephone Soul",
"Witch Soul",
"Lilith Soul",
"Killer Clown Soul",
"Skelerang Soul",
"Fleaman Soul",
"Devil Soul",
"Needles Soul",
"Hell Boar Soul",
"White Dragon Soul",
"Wakwak Tree Soul",
"Imp Soul",
"Harpy Soul",
"Malachi Soul",
"Larva Soul",
"Fish Head Soul",
"Ukoback Soul",
"Killer Fish Soul",
"Dead Pirate Soul",
"Frozen Shade Soul",
"Disc Armor Soul",
"Alura Une Soul",
"Mushussu Soul",
"Succubus Soul",
"Werewolf Soul",
"Flame Demon Soul",
"Alastor Soul"
}
self.rare_souls = {
"Ghost Soul",
"Ouija Table Soul",
"Peeping Eye Soul",
"Skeleton Ape Soul",
"Skeleton Farmer Soul",
"The Creature Soul",
"Ghoul Soul",
"Tombstone Soul",
"Treant Soul",
"Valkyrie Soul",
"Killer Doll Soul",
"Draghignazzo Soul",
"Bone Pillar Soul",
"Barbariccia Soul",
"Heart Eater Soul",
"Medusa Head Soul",
"Mimic Soul",
"Bugbear Soul",
"Procel Soul",
"Bone Ark Soul",
"Gorgon Soul",
"Great Axe Armor Soul",
"Dead Crusader Soul",
"Dead Warrior Soul",
"Erinys Soul",
"Tanjelly Soul",
"Final Guard Soul",
"Iron Golem Soul"
}
self.red_soul_walls = []
self.magic_seal_table = []
self.important_souls = {
"Bone Ark Soul",
"Skeleton Ape Soul",
"Mandragora Soul",
"Rycuda Soul",
"Waiter Skeleton Soul"
}
# These souls are always required for movment logic
self.excluded_static_souls = {
"Aguni Soul",
"Abaddon Soul"
}
def generate_early(self) -> None:
if hasattr(self.multiworld, "re_gen_passthrough"): # If UT
if "Castlevania: Dawn of Sorrow" not in self.multiworld.re_gen_passthrough: return
passthrough = self.multiworld.re_gen_passthrough["Castlevania: Dawn of Sorrow"]
self.options.goal = passthrough["goal"]
self.options.soul_randomizer = passthrough["soul_randomizer"]
self.options.soulsanity_level = passthrough["soulsanity_level"]
self.starting_warp_room = passthrough["starting_warp"]
self.options.open_drawbridge = passthrough["open_drawbridge"]
self.options.boost_speed = passthrough["speed_boost"]
self.red_soul_walls = passthrough["soul_walls"]
self.options.gate_items = passthrough["buttonsanity"]
self.magic_seal_table = passthrough["seals"]
self.options.menace_condition.value = passthrough["menace_condition"]
self.options.mine_condition.value = passthrough["mine_condition"]
self.options.garden_condition.value = passthrough["garden_condition"]
setup_game(self)
self.auth_id = self.random.getrandbits(32)
def create_regions(self) -> None:
init_areas(self, get_locations(self))
place_static_items(self)
if self.options.soul_randomizer != SoulRandomizer.option_soulsanity:
place_static_souls(self)
if self.options.soul_randomizer != SoulRandomizer.option_soulsanity or self.options.soulsanity_level < SoulsanityLevel.option_medium:
self.get_location("Imp Soul").place_locked_item(self.create_static_soul("Imp Soul"))
def create_items(self) -> None:
pool = self.get_item_pool(self.get_excluded_items())
self.fill_pool(pool)
self.multiworld.itempool += pool
def set_rules(self) -> None:
set_location_rules(self)
self.multiworld.completion_condition[self.player] = lambda state: state.has("Menace Defeated", self.player)
def generate_output(self, output_directory: str) -> None:
self.has_generated_output = True # Make sure data defined in generate output doesn't get added to spoiler only mode
try:
code_patch = pkgutil.get_data(__name__, "src/overlay_41.bin")
patch = DoSProcPatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_file("dos_base.bsdiff4", pkgutil.get_data(__name__, "src/dos_base.bsdiff4"))
patch_rom(self, patch, self.player, code_patch)
self.rom_name = patch.name
patch.write(os.path.join(output_directory,
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
except Exception:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def fill_slot_data(self) -> Dict[str, typing.Any]:
return {
"goal": self.options.goal.value,
"starting_warp": self.starting_warp_room,
"soul_randomizer": self.options.soul_randomizer.value,
"soulsanity_level": self.options.soulsanity_level.value,
"open_drawbridge": self.options.open_drawbridge.value,
"speed_boost": self.options.boost_speed.value,
"soul_walls": self.red_soul_walls,
"buttonsanity": self.options.gate_items.value,
"seals": self.magic_seal_table,
"menace_condition": self.options.menace_condition.value,
"garden_condition": self.options.garden_condition.value,
"mine_condition": self.options.mine_condition.value
}
def modify_multidata(self, multidata: dict) -> None:
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
if rom_name:
multidata["connect_names"][self.rom_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.shuffle_starting_warp_room:
spoiler_handle.write(f"Default Warp Room: {self.starting_warp_room}\n")
if self.options.randomize_red_soul_walls:
spoiler_handle.write(f"\nSoul Barriers:\n")
spoiler_handle.write(f" Paranoia 1: {self.red_soul_walls[1]}\n")
spoiler_handle.write(f" Paranoia 2: {self.red_soul_walls[0]}\n")
spoiler_handle.write(f" Paranoia 3: {self.red_soul_walls[3]}\n")
spoiler_handle.write(f" Dark Chapel Catacombs: {self.red_soul_walls[2]}\n")
if self.options.boss_shuffle:
spoiler_handle.write(f"\nBosses:\n")
for boss in self.boss_slots:
spoiler_handle.write(f" {boss}: {self.boss_slots[boss].new_boss}\n")
if self.options.seal_shuffle:
spoiler_handle.write(f"\nMagic Seals:\n")
for seal in self.magic_seal_table:
if seal in ["Mine of Judgment", "The Abyss"] and self.mine_status == "Disabled": # Ignore Magic Seals that are past the endgame trigger
continue
else:
spoiler_handle.write(f" {seal}: {self.magic_seal_table[seal]}\n")
def create_item(self, name: str) -> CVDoSItem:
data = self.set_classifications(name)
return CVDoSItem(name, data.classification, data.code, self.player)
def get_filler_item_name(self) -> str:
weights = {"soul": 10, "money": 20, "weapon": 30, "armor": 40, "consumable": 60}
# If these pools have been exhausted, set their weights to 0
if not self.weapon_table:
weights["weapon"] = 0
if not self.armor_table:
weights["armor"] = 0
filler_type = self.random.choices(list(weights), weights=list(weights.values()), k=1)[0]
weight_table = {
"soul": soul_filler_table,
"weapon": self.weapon_table,
"armor": self.armor_table,
"money": money_table,
"consumable": consumable_table,
}
filler_item = self.random.choice(weight_table[filler_type])
if filler_item in self.weapon_table:
self.weapon_table.remove(filler_item)
elif filler_item in self.armor_table:
self.armor_table.remove(filler_item)
if not self.has_tried_chaos_ring:
self.has_tried_chaos_ring = True
if self.random.randint(0, 101) <= 10: # Chaos ring should have a single 10/100 chance to be placed
filler_item = "Chaos Ring"
return filler_item
def get_excluded_items(self) -> Set[str]:
excluded_items: Set[str] = set()
return excluded_items
def set_classifications(self, name: str) -> Item:
data = item_table[name]
item = CVDoSItem(name, data.classification, data.code, self.player)
if name in self.important_souls:
item.classification = ItemClassification.progression
if self.options.soul_randomizer == SoulRandomizer.option_soulsanity:
if name == "Soul Eater Ring" and self.options.soulsanity_level == SoulsanityLevel.option_rare:
item.classification = ItemClassification.progression
return item
def fill_pool(self, pool: List[Item]) -> None:
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool) - self.extra_item_count): # Change to fix event count
item = self.set_classifications(self.get_filler_item_name())
pool.append(item)
def get_item_pool(self, excluded_items: Set[str]) -> List[Item]:
pool: List[Item] = []
for name, data in item_table.items():
if name not in excluded_items:
for _ in range(data.amount):
item = self.set_classifications(name)
pool.append(item)
return pool
def create_static_soul(self, soul):
data = item_table[soul]
item = Item(soul, ItemClassification.progression, None, self.player) # Create an event item of the soul
return item