Merge branch 'ArchipelagoMW:main' into Satisfactory

This commit is contained in:
Jarno
2025-06-14 16:25:28 +02:00
committed by GitHub
19 changed files with 165 additions and 108 deletions

View File

@@ -6,6 +6,8 @@ on:
permissions:
contents: read
pull-requests: write
env:
GH_REPO: ${{ github.repository }}
jobs:
labeler:

11
Fill.py
View File

@@ -890,7 +890,7 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
worlds = set()
for listed_world in target_world:
if listed_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
failed(f"Cannot place item to {listed_world}'s world as that world does not exist.",
block.force)
continue
worlds.add(world_name_lookup[listed_world])
@@ -923,9 +923,9 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
if isinstance(locations, str):
locations = [locations]
locations_from_groups: list[str] = []
resolved_locations: list[Location] = []
for target_player in worlds:
locations_from_groups: list[str] = []
world_locations = multiworld.get_unfilled_locations(target_player)
for group in multiworld.worlds[target_player].location_name_groups:
if group in locations:
@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
count = block.count
if not count:
count = len(new_block.items)
count = (min(len(new_block.items), len(new_block.resolved_locations))
if new_block.resolved_locations else len(new_block.items))
if isinstance(count, int):
count = {"min": count, "max": count}
if "min" not in count:
count["min"] = 0
if "max" not in count:
count["max"] = len(new_block.items)
count["max"] = (min(len(new_block.items), len(new_block.resolved_locations))
if new_block.resolved_locations else len(new_block.items))
new_block.count = count
plando_blocks[player].append(new_block)

View File

@@ -196,7 +196,8 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
def launch(exe, in_terminal=False):
if in_terminal:
if is_windows:
subprocess.Popen(['start', *exe], shell=True)
# intentionally using a window title with a space so it gets quoted and treated as a title
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
return
elif is_linux:
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')

View File

@@ -548,10 +548,12 @@ def set_up_take_anys(multiworld, world, player):
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
multiworld.shops.append(old_man_take_any.shop)
swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword']
if swords:
sword = multiworld.random.choice(swords)
multiworld.itempool.remove(sword)
sword_indices = [
index for index, item in enumerate(multiworld.itempool) if item.player == player and item.type == 'Sword'
]
if sword_indices:
sword_index = multiworld.random.choice(sword_indices)
sword = multiworld.itempool.pop(sword_index)
multiworld.itempool.append(item_factory('Rupees (20)', world))
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0)
loc_name = "Old Man Sword Cave"

View File

@@ -38,7 +38,7 @@ class DungeonFillTestBase(TestCase):
def test_original_dungeons(self):
self.generate_with_options(DungeonItem.option_original_dungeon)
for location in self.multiworld.get_filled_locations():
with (self.subTest(location=location)):
with (self.subTest(location_name=location.name)):
if location.parent_region.dungeon is None:
self.assertIs(location.item.dungeon, None)
else:
@@ -52,7 +52,7 @@ class DungeonFillTestBase(TestCase):
def test_own_dungeons(self):
self.generate_with_options(DungeonItem.option_own_dungeons)
for location in self.multiworld.get_filled_locations():
with self.subTest(location=location):
with self.subTest(location_name=location.name):
if location.parent_region.dungeon is None:
self.assertIs(location.item.dungeon, None)
else:

View File

@@ -4,7 +4,7 @@ Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Used to manage Regions in the Aquaria game multiworld randomizer
"""
from typing import Dict, Optional
from typing import Dict, Optional, Iterable
from BaseClasses import MultiWorld, Region, Entrance, Item, ItemClassification, CollectionState
from .Items import AquariaItem, ItemNames
from .Locations import AquariaLocations, AquariaLocation, AquariaLocationNames
@@ -34,10 +34,15 @@ def _has_li(state: CollectionState, player: int) -> bool:
return state.has(ItemNames.LI_AND_LI_SONG, player)
def _has_damaging_item(state: CollectionState, player: int) -> bool:
"""`player` in `state` has the shield song item"""
return state.has_any({ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, ItemNames.LI_AND_LI_SONG,
ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, ItemNames.BABY_BLASTER}, player)
DAMAGING_ITEMS:Iterable[str] = [
ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
ItemNames.BABY_BLASTER
]
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
"""`player` in `state` has the an item that do damage other than the ones in `to_remove`"""
return state.has_any(damaging_items, player)
def _has_energy_attack_item(state: CollectionState, player: int) -> bool:
@@ -566,9 +571,11 @@ class AquariaRegions:
self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_turtle,
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
self.__connect_one_way_regions(self.openwater_tr_turtle, self.openwater_tr)
damaging_items_minus_nature_form = [item for item in DAMAGING_ITEMS if item != ItemNames.NATURE_FORM]
self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_urns,
lambda state: _has_bind_song(state, self.player) or
_has_damaging_item(state, self.player))
_has_damaging_item(state, self.player,
damaging_items_minus_nature_form))
self.__connect_regions(self.openwater_tr, self.openwater_br)
self.__connect_regions(self.openwater_tr, self.mithalas_city)
self.__connect_regions(self.openwater_tr, self.veil_b)

View File

@@ -75,6 +75,13 @@ class DarkSouls3World(World):
"""The pool of all items within this particular world. This is a subset of
`self.multiworld.itempool`."""
missable_dupe_prog_locs: Set[str] = {"PC: Storm Ruler - Siegward",
"US: Pyromancy Flame - Cornyx",
"US: Tower Key - kill Irina"}
"""Locations whose vanilla item is a missable duplicate of a non-missable progression item.
If vanilla, these locations shouldn't be expected progression, so they aren't created and don't get rules.
"""
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.all_excluded_locations = set()
@@ -258,10 +265,7 @@ class DarkSouls3World(World):
new_location.progress_type = LocationProgressType.EXCLUDED
else:
# Don't allow missable duplicates of progression items to be expected progression.
if location.name in {"PC: Storm Ruler - Siegward",
"US: Pyromancy Flame - Cornyx",
"US: Tower Key - kill Irina"}:
continue
if location.name in self.missable_dupe_prog_locs: continue
# Replace non-randomized items with events that give the default item
event_item = (
@@ -1286,8 +1290,9 @@ class DarkSouls3World(World):
data = location_dictionary[location]
if data.dlc and not self.options.enable_dlc: continue
if data.ngp and not self.options.enable_ngp: continue
# Don't add rules to missable duplicates of progression items
if location in self.missable_dupe_prog_locs and not self._is_location_available(location): continue
if not self._is_location_available(location): continue
if isinstance(rule, str):
assert item_dictionary[rule].classification == ItemClassification.progression
rule = lambda state, item=rule: state.has(item, self.player)

View File

@@ -34,7 +34,7 @@ class KH2Context(CommonContext):
self.growthlevel = None
self.kh2connected = False
self.kh2_finished_game = False
self.serverconneced = False
self.serverconnected = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
self.kh2_data_package = {}
@@ -47,6 +47,8 @@ class KH2Context(CommonContext):
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
self.sending = []
self.slot_name = None
self.disconnect_from_server = False
# list used to keep track of locations+items player has. Used for disoneccting
self.kh2_seed_save_cache = {
"itemIndex": -1,
@@ -185,11 +187,20 @@ class KH2Context(CommonContext):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
# if slot name != first time login or previous name
# and seed name is none or saved seed name
if not self.slot_name and not self.kh2seedname:
await self.send_connect()
elif self.slot_name == self.auth and self.kh2seedname:
await self.send_connect()
else:
logger.info(f"You are trying to connect with data still cached in the client. Close client or connect to the correct slot: {self.slot_name}")
self.serverconnected = False
self.disconnect_from_server = True
async def connection_closed(self):
self.kh2connected = False
self.serverconneced = False
self.serverconnected = False
if self.kh2seedname is not None and self.auth is not None:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
@@ -197,7 +208,8 @@ class KH2Context(CommonContext):
async def disconnect(self, allow_autoreconnect: bool = False):
self.kh2connected = False
self.serverconneced = False
self.serverconnected = False
self.locations_checked = []
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
@@ -239,7 +251,15 @@ class KH2Context(CommonContext):
def on_package(self, cmd: str, args: dict):
if cmd == "RoomInfo":
self.kh2seedname = args['seed_name']
if not self.kh2seedname:
self.kh2seedname = args['seed_name']
elif self.kh2seedname != args['seed_name']:
self.disconnect_from_server = True
self.serverconnected = False
self.kh2connected = False
logger.info("Connection to the wrong seed, connect to the correct seed or close the client.")
return
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
@@ -338,7 +358,7 @@ class KH2Context(CommonContext):
},
},
}
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced:
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconnected:
self.kh2_seed_save_cache["itemIndex"] = start_index
for item in args['items']:
asyncio.create_task(self.give_item(item.item, item.location))
@@ -370,12 +390,14 @@ class KH2Context(CommonContext):
if not self.kh2:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
self.get_addresses()
#
except Exception as e:
if self.kh2connected:
self.kh2connected = False
logger.info("Game is not open.")
self.serverconneced = True
self.serverconnected = True
self.slot_name = self.auth
def data_package_kh2_cache(self, loc_to_id, item_to_id):
self.kh2_loc_name_to_id = loc_to_id
@@ -493,23 +515,38 @@ class KH2Context(CommonContext):
async def give_item(self, item, location):
try:
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
# sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
while not self.lookup_id_to_item:
await asyncio.sleep(0.5)
itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname]
if itemdata.ability:
if location in self.all_weapon_location_id:
return
# growth have reserved ability slots because of how the goa handles them
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
return
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
# appending the slot that the ability should be in
# appending the slot that the ability should be in
# abilities have a limit amount of slots.
# we start from the back going down to not mess with stuff.
# Front of Invo
# Sora: Save+24F0+0x54 : 0x2546
# Donald: Save+2604+0x54 : 0x2658
# Goofy: Save+2718+0x54 : 0x276C
# Back of Invo. Sora has 6 ability slots that are reserved
# Sora: Save+24F0+0x54+0x92 : 0x25D8
# Donald: Save+2604+0x54+0x9C : 0x26F4
# Goofy: Save+2718+0x54+0x9C : 0x2808
# seed has 2 scans in sora's abilities
# recieved second scan
# if len(seed_save(Scan:[ability slot 52]) < (2)amount of that ability they should have from slot data
# ability_slot = back of inventory that isnt taken
# add ability_slot to seed_save(Scan[]) so now its Scan:[ability slot 52,50]
# decrease back of inventory since its ability_slot is already taken
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set:
@@ -528,18 +565,21 @@ class KH2Context(CommonContext):
if ability_slot in self.front_ability_slots:
self.front_ability_slots.remove(ability_slot)
# if itemdata in {bitmask} all the forms,summons and a few other things are bitmasks
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
# if memaddr is in a bitmask location in memory
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
# if itemdata in {magic}
elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
# if memaddr is in magic addresses
self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1
# equipment is a list instead of dict because you can only have 1 currently
elif itemname in self.all_equipment:
self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname)
# weapons are done differently since you can only have one and has to check it differently
elif itemname in self.all_weapons:
if itemname in self.keyblade_set:
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
@@ -548,9 +588,11 @@ class KH2Context(CommonContext):
else:
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname)
# TODO: this can just be removed and put into the else below it
elif itemname in self.stat_increase_set:
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
else:
# "normal" items. They have a unique byte reserved for how many they have
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
else:
@@ -930,7 +972,7 @@ def finishedGame(ctx: KH2Context):
async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set():
try:
if ctx.kh2connected and ctx.serverconneced:
if ctx.kh2connected and ctx.serverconnected:
ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels())
@@ -944,13 +986,19 @@ async def kh2_watcher(ctx: KH2Context):
if ctx.sending:
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
elif not ctx.kh2connected and ctx.serverconnected:
logger.info("Game Connection lost. trying to reconnect.")
ctx.kh2 = None
while not ctx.kh2connected and ctx.serverconneced:
await asyncio.sleep(15)
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
ctx.get_addresses()
while not ctx.kh2connected and ctx.serverconnected:
try:
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
ctx.get_addresses()
logger.info("Game Connection Established.")
except Exception as e:
await asyncio.sleep(5)
if ctx.disconnect_from_server:
ctx.disconnect_from_server = False
await ctx.disconnect()
except Exception as e:
if ctx.kh2connected:
ctx.kh2connected = False

View File

@@ -277,9 +277,7 @@ class KH2World(World):
if self.options.FillerItemsLocal:
for item in filler_items:
self.options.local_items.value.add(item)
# By imitating remote this doesn't have to be plandoded filler anymore
# for location in {LocationName.JunkMedal, LocationName.JunkMedal}:
# self.plando_locations[location] = random_stt_item
if not self.options.SummonLevelLocationToggle:
self.total_locations -= 6
@@ -400,6 +398,8 @@ class KH2World(World):
# plando goofy get bonuses
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"]
if len(goofy_get_bonus_location_pool) > len(self.goofy_get_bonus_abilities):
raise Exception(f"Too little abilities to fill goofy get bonus locations for player {self.player_name}.")
for location in goofy_get_bonus_location_pool:
self.random.choice(self.goofy_get_bonus_abilities)
random_ability = self.random.choice(self.goofy_get_bonus_abilities)
@@ -416,11 +416,12 @@ class KH2World(World):
random_ability = self.random.choice(self.donald_weapon_abilities)
location.place_locked_item(random_ability)
self.donald_weapon_abilities.remove(random_ability)
# if option is turned off
if not self.options.DonaldGoofyStatsanity:
# plando goofy get bonuses
donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"]
if len(donald_get_bonus_location_pool) > len(self.donald_get_bonus_abilities):
raise Exception(f"Too little abilities to fill donald get bonus locations for player {self.player_name}.")
for location in donald_get_bonus_location_pool:
random_ability = self.random.choice(self.donald_get_bonus_abilities)
location.place_locked_item(random_ability)

View File

@@ -1,6 +1,7 @@
import typing
from BaseClasses import MultiWorld
from Options import OptionError
from worlds.AutoWorld import World
from .Names import LocationName
@@ -99,8 +100,9 @@ def get_gate_bosses(world: World):
pass
if boss in plando_bosses:
# TODO: Raise error here. Duplicates not allowed
pass
raise OptionError(f"Invalid input for option `plando_bosses`: "
f"No Duplicate Bosses permitted ({boss}) - for "
f"{world.player_name}")
plando_bosses[boss_num] = boss
@@ -108,13 +110,14 @@ def get_gate_bosses(world: World):
available_bosses.remove(boss)
for x in range(world.options.number_of_level_gates):
if ("king boom boo" not in selected_bosses) and ("king boom boo" not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
available_bosses.extend(gate_bosses_with_requirements_table)
if (10 not in selected_bosses) and (king_boom_boo not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
available_bosses.extend(gate_bosses_with_requirements_table.keys())
world.random.shuffle(available_bosses)
chosen_boss = available_bosses[0]
if plando_bosses[x] != "None":
available_bosses.append(plando_bosses[x])
if plando_bosses[x] not in available_bosses:
available_bosses.append(plando_bosses[x])
chosen_boss = plando_bosses[x]
selected_bosses.append(all_gate_bosses_table[chosen_boss])

View File

@@ -324,7 +324,8 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
add_rule_safe(multiworld, LocationName.iron_gate_5, player,
lambda state: state.has(ItemName.eggman_large_cannon, player))
add_rule_safe(multiworld, LocationName.dry_lagoon_5, player,
lambda state: state.has(ItemName.rouge_treasure_scope, player))
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_treasure_scope, player))
add_rule_safe(multiworld, LocationName.sand_ocean_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule_safe(multiworld, LocationName.egg_quarters_5, player,
@@ -407,8 +408,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
@@ -1402,8 +1402,6 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
state.has(ItemName.eggman_large_cannon, player)))
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
lambda state: (state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_treasure_scope, player)))
@@ -1724,6 +1722,9 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.white_jungle_itembox_8, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
add_rule(multiworld.get_location(LocationName.sky_rail_itembox_8, player),
lambda state: (state.has(ItemName.shadow_air_shoes, player) and
state.has(ItemName.shadow_mystic_melody, player)))
add_rule(multiworld.get_location(LocationName.mad_space_itembox_8, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_itembox_8, player),
@@ -2308,8 +2309,7 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
lambda state: state.has(ItemName.tails_booster, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
@@ -2980,8 +2980,6 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
state.has(ItemName.eggman_jet_engine, player)))
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
lambda state: (state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_treasure_scope, player)))
@@ -3593,8 +3591,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
lambda state: state.has(ItemName.tails_booster, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
@@ -3643,9 +3640,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
add_rule(multiworld.get_location(LocationName.prison_lane_pipe_3, player),
lambda state: state.has(ItemName.tails_bazooka, player))
add_rule(multiworld.get_location(LocationName.mission_street_pipe_3, player),
@@ -3771,10 +3765,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
add_rule(multiworld.get_location(LocationName.cosmic_wall_beetle, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_beetle, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Animal Upgrade Requirements
if world.options.animalsanity:
add_rule(multiworld.get_location(LocationName.hidden_base_animal_2, player),
@@ -3839,8 +3829,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
add_rule(multiworld.get_location(LocationName.weapons_bed_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.security_hall_animal_8, player),
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
lambda state: state.has(ItemName.rouge_iron_boots, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
@@ -3976,8 +3965,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
state.has(ItemName.tails_bazooka, player))
add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_16, player),
lambda state: state.has(ItemName.sonic_flame_ring, player))
add_rule(multiworld.get_location(LocationName.final_rush_animal_16, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
add_rule(multiworld.get_location(LocationName.final_chase_animal_17, player),
lambda state: state.has(ItemName.shadow_flame_ring, player))
@@ -4035,8 +4022,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))

View File

@@ -12,7 +12,7 @@ from .Constants import *
def launch_client(*args: str):
from .Client import launch
launch_subprocess(launch(*args), name=CLIENT_NAME)
launch_subprocess(launch, name=CLIENT_NAME, args=args)
components.append(

View File

@@ -261,13 +261,13 @@ class ShiversWorld(World):
data.type == ItemType.POT_DUPLICATE]
elif self.options.full_pots == "complete":
return [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPELTE_DUPLICATE]
data.type == ItemType.POT_COMPLETE_DUPLICATE]
else:
pool = []
pieces = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_DUPLICATE]
complete = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPELTE_DUPLICATE]
data.type == ItemType.POT_COMPLETE_DUPLICATE]
for i in range(10):
if self.pot_completed_list[i] == 0:
pool.append(pieces[i])

View File

@@ -271,11 +271,11 @@ solar_essence = BundleItem(Loot.solar_essence)
void_essence = BundleItem(Loot.void_essence)
petrified_slime = BundleItem(Mineral.petrified_slime)
blue_slime_egg = BundleItem(Loot.blue_slime_egg)
red_slime_egg = BundleItem(Loot.red_slime_egg)
purple_slime_egg = BundleItem(Loot.purple_slime_egg)
green_slime_egg = BundleItem(Loot.green_slime_egg)
tiger_slime_egg = BundleItem(Loot.tiger_slime_egg, source=BundleItem.Sources.island)
blue_slime_egg = BundleItem(AnimalProduct.slime_egg_blue)
red_slime_egg = BundleItem(AnimalProduct.slime_egg_red)
purple_slime_egg = BundleItem(AnimalProduct.slime_egg_purple)
green_slime_egg = BundleItem(AnimalProduct.slime_egg_green)
tiger_slime_egg = BundleItem(AnimalProduct.slime_egg_tiger, source=BundleItem.Sources.island)
cherry_bomb = BundleItem(Bomb.cherry_bomb, 5)
bomb = BundleItem(Bomb.bomb, 2)

View File

@@ -168,15 +168,16 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep),
AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime),
AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3),
AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6),
AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9),
AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond),
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
AnimalProduct.slime_egg_tiger: self.can_fish_pond(Fish.lionfish, *(Forageable.ginger, Fruit.pineapple, Fruit.mango)) & self.time.has_lived_months(12) &
self.building.has_building(Building.slime_hutch) & self.monster.can_kill(Monster.tiger_slime),
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
AnimalProduct.egg_starter: self.logic.false_, # It could be purchased at the Desert Festival, but festival logic is quite a mess, so not considering it yet...
AnimalProduct.golden_egg_starter: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
AnimalProduct.void_egg_starter: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
@@ -233,7 +234,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), #
Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton),
Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe),
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe),
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe) & self.received("Open Professor Snail Cave"),
Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut),
Fossil.fossilized_spine: self.fishing.can_fish_at(Region.dig_site),
Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper),
@@ -288,9 +289,9 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))),
MetalBar.radioactive: self.can_smelt(Ore.radioactive),
Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
Ore.iridium: self.count(2, *(self.mine.can_mine_in_the_skull_cavern(), self.can_fish_pond(Fish.super_cucumber), self.tool.has_tool(Tool.pan, ToolMaterial.iridium))),
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room),
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
@@ -381,5 +382,8 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
def can_use_obelisk(self, obelisk: str) -> StardewRule:
return self.region.can_reach(Region.farm) & self.received(obelisk)
def can_fish_pond(self, fish: str) -> StardewRule:
return self.building.has_building(Building.fish_pond) & self.has(fish)
def can_fish_pond(self, fish: str, *items: str) -> StardewRule:
rule = self.building.has_building(Building.fish_pond) & self.has(fish)
if items:
rule = rule & self.has_all(*items)
return rule

View File

@@ -1,9 +1,4 @@
class Loot:
blue_slime_egg = "Blue Slime Egg"
red_slime_egg = "Red Slime Egg"
purple_slime_egg = "Purple Slime Egg"
green_slime_egg = "Green Slime Egg"
tiger_slime_egg = "Tiger Slime Egg"
slime = "Slime"
bug_meat = "Bug Meat"
bat_wing = "Bat Wing"

View File

@@ -11,7 +11,7 @@ class EntranceRandomizationAssertMixin:
non_progression_connections = [connection for connection in all_connections.values() if RandomizationFlag.BIT_NON_PROGRESSION in connection.flag]
for non_progression_connections in non_progression_connections:
with self.subTest(connection=non_progression_connections):
with self.subTest(connection=non_progression_connections.name):
self.assert_can_reach_entrance(non_progression_connections.name)

View File

@@ -12,14 +12,14 @@ from ...regions.regions import create_all_regions, create_all_connections
class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_data.regions_with_ginger_island_by_name.values():
with self.subTest(region=region):
with self.subTest(region=region.name):
for exit_ in region.exits:
self.assertIn(exit_, vanilla_data.connections_with_ginger_island_by_name,
f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_data.connections_with_ginger_island_by_name.values():
with self.subTest(connection=connection):
with self.subTest(connection=connection.name):
self.assertIn(connection.destination, vanilla_data.regions_with_ginger_island_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")
@@ -27,14 +27,14 @@ class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
class TestVanillaRegionsConnectionsWithoutGingerIsland(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_data.regions_without_ginger_island_by_name.values():
with self.subTest(region=region):
with self.subTest(region=region.name):
for exit_ in region.exits:
self.assertIn(exit_, vanilla_data.connections_without_ginger_island_by_name,
f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_data.connections_without_ginger_island_by_name.values():
with self.subTest(connection=connection):
with self.subTest(connection=connection.name):
self.assertIn(connection.destination, vanilla_data.regions_without_ginger_island_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")

View File

@@ -8,7 +8,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
SkillProgression.internal_name: SkillProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_progressive,
Fishsanity.internal_name: Fishsanity.option_all,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
@@ -18,7 +18,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
fish_and_items = {
Fish.crimsonfish: ["Beach Bridge"],
Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"],
Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades
Fish.woodskip: ["Progressive Axe", "Progressive Axe", "Progressive Weapon"], # For the ores to get the axe upgrades
Fish.mutant_carp: ["Rusty Key"],
Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"],
Fish.lionfish: ["Boat Repair"],
@@ -26,8 +26,8 @@ class TestNeedRegionToCatchFish(SVTestBase):
Fish.stingray: ["Boat Repair", "Island Resort"],
Fish.ghostfish: ["Progressive Weapon"],
Fish.stonefish: ["Progressive Weapon"],
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"],
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"],
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe"],
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe", "Progressive Pickaxe"],
Fish.sandfish: ["Bus Repair"],
Fish.scorpion_carp: ["Desert Obelisk"],
# Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary
@@ -37,6 +37,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
}
self.collect("Progressive Fishing Rod", 4)
self.original_state = self.multiworld.state.copy()
for fish in fish_and_items:
with self.subTest(f"Region rules for {fish}"):