mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
KDL3: Version 2.0.0 (#3323)
* initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * Update Rom.py * convert KDL3 to APPP * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * initial base for local items, need to finish * coo not clean * handle local items for real, appp cleanup * actually make bosses send their locations * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs * add prefill items * fix kine fill error * Update Rom.py * Update Files.py * mypy and softlock fix * Update Gifting.py * mypy phase 1 * fix rare async client bug * Update __init__.py * typing cleanup * fix stone softlock because of the way Kine's Stone works, you can't clear the stone blocks before clearing the burning blocks, so we have to bring Burning from outside * Update Rom.py * Add option groups * Rename to lowercase * finish rename * whoops broke the world * fix animal duplication bug * overhaul filler generation * add Miku flavor * Update gifting.py * fix issues related to max_hs increase * Update test_locations.py * fix boss shuffle not working if level shuffle is disabled * fix bleeding default levels * Update options.py * thought this would print seed * yay bad merges * forgot options too * yeah lets just break generation while at it * this is probably a problem * cap required heart stars * Revert "cap required heart stars" This reverts commit 759efd3e2b14ec2855082de041ac989cb9c5d500. * fix duplication removal placement, deprecated test option * forgot that we need to account for what we place * move location ids * rewrite trap handling * further stage renumber fixes * forgot one more * basic UT support * fix local heart star checks * fix pattern --------- Co-authored-by: beauxq <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
@@ -1,25 +1,25 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item
|
||||
from Fill import fill_restrictive
|
||||
from Options import PerGameCommonOptions
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
|
||||
trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights
|
||||
from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
|
||||
from .Names.AnimalFriendSpawns import animal_friend_spawns
|
||||
from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive
|
||||
from .Regions import create_levels, default_levels
|
||||
from .Options import KDL3Options
|
||||
from .Presets import kdl3_options_presets
|
||||
from .Names import LocationName
|
||||
from .Room import KDL3Room
|
||||
from .Rules import set_rules
|
||||
from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH
|
||||
from .Client import KDL3SNIClient
|
||||
from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
|
||||
trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\
|
||||
lookup_item_to_id
|
||||
from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
|
||||
from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets
|
||||
from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive
|
||||
from .regions import create_levels, default_levels
|
||||
from .options import KDL3Options, kdl3_option_groups
|
||||
from .presets import kdl3_options_presets
|
||||
from .names import location_name
|
||||
from .room import KDL3Room
|
||||
from .rules import set_rules
|
||||
from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH
|
||||
from .client import KDL3SNIClient
|
||||
|
||||
from typing import Dict, TextIO, Optional, List
|
||||
from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type
|
||||
import os
|
||||
import math
|
||||
import threading
|
||||
@@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld):
|
||||
)
|
||||
]
|
||||
options_presets = kdl3_options_presets
|
||||
option_groups = kdl3_option_groups
|
||||
|
||||
|
||||
class KDL3World(World):
|
||||
@@ -61,35 +62,35 @@ class KDL3World(World):
|
||||
"""
|
||||
|
||||
game = "Kirby's Dream Land 3"
|
||||
options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options
|
||||
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options
|
||||
options: KDL3Options
|
||||
item_name_to_id = {item: item_table[item].code for item in item_table}
|
||||
item_name_to_id = lookup_item_to_id
|
||||
location_name_to_id = {location_table[location]: location for location in location_table}
|
||||
item_name_groups = item_names
|
||||
web = KDL3WebWorld()
|
||||
settings: typing.ClassVar[KDL3Settings]
|
||||
settings: ClassVar[KDL3Settings]
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.rom_name = None
|
||||
self.rom_name: bytes = bytes()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(multiworld, player)
|
||||
self.copy_abilities: Dict[str, str] = vanilla_enemies.copy()
|
||||
self.required_heart_stars: int = 0 # we fill this during create_items
|
||||
self.boss_requirements: Dict[int, int] = dict()
|
||||
self.boss_requirements: List[int] = []
|
||||
self.player_levels = default_levels.copy()
|
||||
self.stage_shuffle_enabled = False
|
||||
self.boss_butch_bosses: List[Optional[bool]] = list()
|
||||
self.rooms: Optional[List[KDL3Room]] = None
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
||||
rom_file: str = get_base_rom_path()
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}")
|
||||
self.boss_butch_bosses: List[Optional[bool]] = []
|
||||
self.rooms: List[KDL3Room] = []
|
||||
|
||||
create_regions = create_levels
|
||||
|
||||
def create_item(self, name: str, force_non_progression=False) -> KDL3Item:
|
||||
def generate_early(self) -> None:
|
||||
if self.options.total_heart_stars != -1:
|
||||
logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. "
|
||||
f"Please use \"max_heart_stars\" instead.")
|
||||
self.options.max_heart_stars.value = self.options.total_heart_stars.value
|
||||
|
||||
def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item:
|
||||
item = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if item.progression and not force_non_progression:
|
||||
@@ -99,7 +100,7 @@ class KDL3World(World):
|
||||
classification = ItemClassification.trap
|
||||
return KDL3Item(name, classification, item.code, self.player)
|
||||
|
||||
def get_filler_item_name(self, include_stars=True) -> str:
|
||||
def get_filler_item_name(self, include_stars: bool = True) -> str:
|
||||
if include_stars:
|
||||
return self.random.choices(list(total_filler_weights.keys()),
|
||||
weights=list(total_filler_weights.values()))[0]
|
||||
@@ -112,8 +113,8 @@ class KDL3World(World):
|
||||
self.options.slow_trap_weight.value,
|
||||
self.options.ability_trap_weight.value])[0]
|
||||
|
||||
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str],
|
||||
level: int, stage: int):
|
||||
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str],
|
||||
level: int, stage: int) -> Optional[str]:
|
||||
valid_rooms = [room for room in self.rooms if (room.level < level)
|
||||
or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge
|
||||
valid_enemies = set()
|
||||
@@ -124,6 +125,10 @@ class KDL3World(World):
|
||||
return None # a valid enemy got placed by a more restrictive placement
|
||||
return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies]))
|
||||
|
||||
def get_pre_fill_items(self) -> List[Item]:
|
||||
return [self.create_item(item)
|
||||
for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]]
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
if self.options.copy_ability_randomization:
|
||||
# randomize copy abilities
|
||||
@@ -207,10 +212,32 @@ class KDL3World(World):
|
||||
# If Kine is ever the last animal friend placed, he will cause fill errors on closed world
|
||||
animal_pool.sort()
|
||||
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
|
||||
items = [self.create_item(animal) for animal in animal_pool]
|
||||
allstate = self.multiworld.get_all_state(False)
|
||||
items: List[Item] = [self.create_item(animal) for animal in animal_pool]
|
||||
allstate = CollectionState(self.multiworld)
|
||||
for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]:
|
||||
self.collect(allstate, self.create_item(item))
|
||||
self.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, allstate, locations, items, True, True)
|
||||
|
||||
# Need to ensure all of these are unique items, and replace them if they aren't
|
||||
for spawns in problematic_sets:
|
||||
placed = [self.get_location(spawn).item for spawn in spawns]
|
||||
placed_names = set([item.name for item in placed])
|
||||
if len(placed_names) != len(placed):
|
||||
# have a duplicate
|
||||
animals = []
|
||||
for spawn in spawns:
|
||||
spawn_location = self.get_location(spawn)
|
||||
if spawn_location.item.name not in animals:
|
||||
animals.append(spawn_location.item.name)
|
||||
else:
|
||||
new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn",
|
||||
"ChuChu Spawn", "Nago Spawn", "Pitch Spawn"]
|
||||
if x not in placed_names and x not in animals])
|
||||
spawn_location.item = None
|
||||
spawn_location.place_locked_item(self.create_item(new_animal))
|
||||
animals.append(new_animal)
|
||||
# logically, this should be sound pre-ER. May need to adjust around it with ER in the future
|
||||
else:
|
||||
animal_friends = animal_friend_spawns.copy()
|
||||
for animal in animal_friends:
|
||||
@@ -225,21 +252,20 @@ class KDL3World(World):
|
||||
remaining_items = len(location_table) - len(itempool)
|
||||
if not self.options.consumables:
|
||||
remaining_items -= len(consumable_locations)
|
||||
remaining_items -= len(star_locations)
|
||||
if self.options.starsanity:
|
||||
# star fill, keep consumable pool locked to consumable and fill 767 stars specifically
|
||||
star_items = list(star_item_weights.keys())
|
||||
star_weights = list(star_item_weights.values())
|
||||
itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights,
|
||||
k=767)])
|
||||
total_heart_stars = self.options.total_heart_stars
|
||||
if not self.options.starsanity:
|
||||
remaining_items -= len(star_locations)
|
||||
max_heart_stars = self.options.max_heart_stars.value
|
||||
if max_heart_stars > remaining_items:
|
||||
max_heart_stars = remaining_items
|
||||
# ensure at least 1 heart star required per world
|
||||
required_heart_stars = max(int(total_heart_stars * required_percentage), 5)
|
||||
filler_items = total_heart_stars - required_heart_stars
|
||||
filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0))
|
||||
trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0))
|
||||
filler_amount -= trap_amount
|
||||
non_required_heart_stars = filler_items - filler_amount - trap_amount
|
||||
required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99)
|
||||
filler_items = remaining_items - required_heart_stars
|
||||
converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0))
|
||||
non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars
|
||||
filler_items -= non_required_heart_stars
|
||||
trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0))
|
||||
|
||||
filler_items -= trap_amount
|
||||
self.required_heart_stars = required_heart_stars
|
||||
# handle boss requirements here
|
||||
requirements = [required_heart_stars]
|
||||
@@ -261,8 +287,8 @@ class KDL3World(World):
|
||||
requirements.insert(i - 1, quotient * i)
|
||||
self.boss_requirements = requirements
|
||||
itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)])
|
||||
itempool.extend([self.create_item(self.get_filler_item_name(False))
|
||||
for _ in range(filler_amount + (remaining_items - total_heart_stars))])
|
||||
itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value)))
|
||||
for _ in range(filler_items)])
|
||||
itempool.extend([self.create_item(self.get_trap_item_name())
|
||||
for _ in range(trap_amount)])
|
||||
itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)])
|
||||
@@ -273,15 +299,15 @@ class KDL3World(World):
|
||||
self.multiworld.get_location(location_table[self.player_levels[level][stage]]
|
||||
.replace("Complete", "Stage Completion"), self.player) \
|
||||
.place_locked_item(KDL3Item(
|
||||
f"{LocationName.level_names_inverse[level]} - Stage Completion",
|
||||
f"{location_name.level_names_inverse[level]} - Stage Completion",
|
||||
ItemClassification.progression, None, self.player))
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.stage_shuffle_enabled = self.options.stage_shuffle > 0
|
||||
goal = self.options.goal
|
||||
goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player)
|
||||
goal = self.options.goal.value
|
||||
goal_location = self.multiworld.get_location(location_name.goals[goal], self.player)
|
||||
goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
|
||||
for level in range(1, 6):
|
||||
self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
|
||||
@@ -300,60 +326,65 @@ class KDL3World(World):
|
||||
else:
|
||||
self.boss_butch_bosses = [False for _ in range(6)]
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
rom_path = ""
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
try:
|
||||
rom = RomData(get_base_rom_path())
|
||||
patch_rom(self, rom)
|
||||
patch = KDL3ProcedurePatch()
|
||||
patch_rom(self, patch)
|
||||
|
||||
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
|
||||
rom.write_to_file(rom_path)
|
||||
self.rom_name = rom.name
|
||||
self.rom_name = patch.name
|
||||
|
||||
patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.multiworld.player_name[self.player], patched_path=rom_path)
|
||||
patch.write()
|
||||
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
|
||||
if os.path.exists(rom_path):
|
||||
os.unlink(rom_path)
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None:
|
||||
# wait for self.rom_name to be available.
|
||||
self.rom_name_available_event.wait()
|
||||
assert isinstance(self.rom_name, bytes)
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
new_name = base64.b64encode(self.rom_name).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
|
||||
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||
# UT support
|
||||
return {"player_levels": self.player_levels}
|
||||
|
||||
def interpret_slot_data(self, slot_data: Mapping[str, Any]):
|
||||
# UT support
|
||||
player_levels = {int(key): value for key, value in slot_data["player_levels"].items()}
|
||||
return {"player_levels": player_levels}
|
||||
|
||||
def write_spoiler(self, spoiler_handle: TextIO) -> None:
|
||||
if self.stage_shuffle_enabled:
|
||||
spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for level in LocationName.level_names:
|
||||
for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)):
|
||||
for level in location_name.level_names:
|
||||
for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)):
|
||||
spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n")
|
||||
if self.options.animal_randomization:
|
||||
spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for level in self.player_levels:
|
||||
for lvl in self.player_levels:
|
||||
for stage in range(6):
|
||||
rooms = [room for room in self.rooms if room.level == level and room.stage == stage]
|
||||
rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage]
|
||||
animals = []
|
||||
for room in rooms:
|
||||
animals.extend([location.item.name.replace(" Spawn", "")
|
||||
for location in room.locations if "Animal" in location.name])
|
||||
spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}"
|
||||
for location in room.locations if "Animal" in location.name
|
||||
and location.item is not None])
|
||||
spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}"
|
||||
f": {', '.join(animals)}\n")
|
||||
if self.options.copy_ability_randomization:
|
||||
spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for enemy in self.copy_abilities:
|
||||
spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n")
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
|
||||
if self.stage_shuffle_enabled:
|
||||
regions = {LocationName.level_names[level]: level for level in LocationName.level_names}
|
||||
regions = {location_name.level_names[level]: level for level in location_name.level_names}
|
||||
level_hint_data = {}
|
||||
for level in regions:
|
||||
for stage in range(7):
|
||||
@@ -361,6 +392,6 @@ class KDL3World(World):
|
||||
self.player).name.replace(" - Complete", "")
|
||||
stage_regions = [room for room in self.rooms if stage_name in room.name]
|
||||
for region in stage_regions:
|
||||
for location in [location for location in region.locations if location.address]:
|
||||
for location in [location for location in list(region.get_locations()) if location.address]:
|
||||
level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}"
|
||||
hint_data[self.player] = level_hint_data
|
||||
|
||||
Reference in New Issue
Block a user