From edd1fff4b79e94dea3c6a1691872faf032e7aba9 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Wed, 16 Nov 2022 10:32:33 -0600 Subject: [PATCH] Core: make early_items internal only (#1177) Co-authored-by: beauxq --- BaseClasses.py | 5 ++- Fill.py | 46 ++++++++++++++++----- Options.py | 6 --- test/general/TestFill.py | 6 +-- worlds/generic/docs/advanced_settings_en.md | 9 +--- worlds/rogue_legacy/__init__.py | 24 +++-------- worlds/sm/__init__.py | 17 +------- worlds/subnautica/__init__.py | 3 +- worlds/zillion/docs/setup_en.md | 9 ---- worlds/zillion/options.py | 12 +++++- 10 files changed, 61 insertions(+), 76 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5840d5c8..c51eaf9a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -48,7 +48,8 @@ class MultiWorld(): state: CollectionState accessibility: Dict[int, Options.Accessibility] - early_items: Dict[int, Options.EarlyItems] + early_items: Dict[int, Dict[str, int]] + local_early_items: Dict[int, Dict[str, int]] local_items: Dict[int, Options.LocalItems] non_local_items: Dict[int, Options.NonLocalItems] progression_balancing: Dict[int, Options.ProgressionBalancing] @@ -94,6 +95,8 @@ class MultiWorld(): self.customitemarray = [] self.shuffle_ganon = True self.spoiler = Spoiler(self) + self.early_items = {player: {} for player in self.player_ids} + self.local_early_items = {player: {} for player in self.player_ids} self.indirect_connections = {} self.fix_trock_doors = self.AttributeProxy( lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') diff --git a/Fill.py b/Fill.py index b9888962..f32d0ced 100644 --- a/Fill.py +++ b/Fill.py @@ -254,14 +254,17 @@ def distribute_early_items(world: MultiWorld, """ returns new fill_locations and itempool """ early_items_count: typing.Dict[typing.Tuple[str, int], int] = {} for player in world.player_ids: - for item, count in world.early_items[player].value.items(): - early_items_count[(item, player)] = count + items = itertools.chain(world.early_items[player], world.local_early_items[player]) + for item in items: + early_items_count[(item, player)] = [world.early_items[player].get(item, 0), world.local_early_items[player].get(item, 0)] if early_items_count: early_locations: typing.List[Location] = [] early_priority_locations: typing.List[Location] = [] loc_indexes_to_remove: typing.Set[int] = set() + base_state = world.state.copy() + base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None)) for i, loc in enumerate(fill_locations): - if loc.can_reach(world.state): + if loc.can_reach(base_state): if loc.progress_type == LocationProgressType.PRIORITY: early_priority_locations.append(loc) else: @@ -271,27 +274,48 @@ def distribute_early_items(world: MultiWorld, early_prog_items: typing.List[Item] = [] early_rest_items: typing.List[Item] = [] + early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids} + early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids} item_indexes_to_remove: typing.Set[int] = set() for i, item in enumerate(itempool): if (item.name, item.player) in early_items_count: if item.advancement: - early_prog_items.append(item) + if early_items_count[(item.name, item.player)][1]: + early_local_prog_items[item.player].append(item) + early_items_count[(item.name, item.player)][1] -= 1 + else: + early_prog_items.append(item) + early_items_count[(item.name, item.player)][0] -= 1 else: - early_rest_items.append(item) + if early_items_count[(item.name, item.player)][1]: + early_local_rest_items[item.player].append(item) + early_items_count[(item.name, item.player)][1] -= 1 + else: + early_rest_items.append(item) + early_items_count[(item.name, item.player)][0] -= 1 item_indexes_to_remove.add(i) - early_items_count[(item.name, item.player)] -= 1 - if early_items_count[(item.name, item.player)] == 0: + if early_items_count[(item.name, item.player)] == [0, 0]: del early_items_count[(item.name, item.player)] if len(early_items_count) == 0: break itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove] - fill_restrictive(world, world.state, early_locations, early_rest_items, lock=True) + for player in world.player_ids: + fill_restrictive(world, base_state, + [loc for loc in early_locations if loc.player == player], + early_local_rest_items[player], lock=True) + early_locations = [loc for loc in early_locations if not loc.item] + fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True) early_locations += early_priority_locations - fill_restrictive(world, world.state, early_locations, early_prog_items, lock=True) + for player in world.player_ids: + fill_restrictive(world, base_state, + [loc for loc in early_locations if loc.player == player], + early_local_prog_items[player], lock=True) + early_locations = [loc for loc in early_locations if not loc.item] + fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True) unplaced_early_items = early_rest_items + early_prog_items if unplaced_early_items: - logging.warning(f"Ran out of early locations for early items. Failed to place \ - {len(unplaced_early_items)} items early.") + logging.warning("Ran out of early locations for early items. Failed to place " + f"{len(unplaced_early_items)} items early.") itempool += unplaced_early_items fill_locations.extend(early_locations) diff --git a/Options.py b/Options.py index e13e1d7b..16deb90d 100644 --- a/Options.py +++ b/Options.py @@ -883,11 +883,6 @@ class NonLocalItems(ItemSet): display_name = "Not Local Items" -class EarlyItems(ItemDict): - """Force the specified items to be in locations that are reachable from the start.""" - display_name = "Early Items" - - class StartInventory(ItemDict): """Start with these items.""" verify_item_name = True @@ -986,7 +981,6 @@ per_game_common_options = { **common_options, # can be overwritten per-game "local_items": LocalItems, "non_local_items": NonLocalItems, - "early_items": EarlyItems, "start_inventory": StartInventory, "start_hints": StartHints, "start_location_hints": StartLocationHints, diff --git a/test/general/TestFill.py b/test/general/TestFill.py index c5130eed..102e1fd6 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -628,9 +628,9 @@ class TestDistributeItemsRestrictive(unittest.TestCase): mw = generate_multi_world(2) player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5) player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5) - mw.early_items[1].value[player1.basic_items[0].name] = 1 - mw.early_items[2].value[player2.basic_items[2].name] = 1 - mw.early_items[2].value[player2.basic_items[3].name] = 1 + mw.early_items[1][player1.basic_items[0].name] = 1 + mw.early_items[2][player2.basic_items[2].name] = 1 + mw.early_items[2][player2.basic_items[3].name] = 1 early_items = [ player1.basic_items[0], diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 45f653e8..a96598a8 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -106,7 +106,7 @@ settings. If a game can be rolled it **must** have a settings section even if it Some options in Archipelago can be used by every game but must still be placed within the relevant game's section. -Currently, these options are `start_inventory`, `early_items`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints` +Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints` , `exclude_locations`, and various plando options. See the plando guide for more info on plando options. Plando @@ -115,8 +115,6 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which will give you 30 rupees. -* `early_items` is formatted in the same way as `start_inventory` and will force the number of each item specified to be -forced into locations that are reachable from the start, before obtaining any items. * `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for the location without using any hint points. * `local_items` will force any items you want to be in your world instead of being in another world. @@ -174,8 +172,6 @@ A Link to the Past: - Quake non_local_items: - Moon Pearl - early_items: - Flute: 1 start_location_hints: - Spike Cave priority_locations: @@ -239,9 +235,6 @@ Timespinner: * `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we have to find it ourselves. * `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it. -* `early_items` forces the `Flute` to be placed in a location that is available from the beginning of the game ("Sphere -1"). Since it is not specified in `local_items` or `non_local_items`, it can be placed one of these locations in any -world. * `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld that can be used for no cost. * `priority_locations` forces a progression item to be placed on the `Link's House` location. diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 91107c75..93334a97 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -41,7 +41,6 @@ class RLWorld(World): location_name_to_id = {name: data.code for name, data in location_table.items()} item_pool: List[RLItem] = [] - prefill_items: List[RLItem] = [] def setting(self, name: str): return getattr(self.multiworld, name)[self.player] @@ -50,8 +49,6 @@ class RLWorld(World): return {option_name: self.setting(option_name).value for option_name in rl_options} def generate_early(self): - self.prefill_items = [] - # Check validation of names. additional_lady_names = len(self.setting("additional_lady_names").value) additional_sir_names = len(self.setting("additional_sir_names").value) @@ -68,10 +65,11 @@ class RLWorld(World): f"Expected {int(self.setting('number_of_children'))}, Got {additional_sir_names}") if self.setting("vendors") == "early": - self.prefill_items += [self.create_item("Blacksmith"), self.create_item("Enchantress")] + self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 + self.multiworld.local_early_items[self.player]["Enchantress"] = 1 if self.setting("architect") == "early": - self.prefill_items += [self.create_item("Architect")] + self.multiworld.local_early_items[self.player]["Architect"] = 1 def generate_basic(self): self.item_pool = [] @@ -83,7 +81,7 @@ class RLWorld(World): # Architect if name == "Architect": - if self.setting("architect") == "disabled" or self.setting("architect") == "early": + if self.setting("architect") == "disabled": continue if self.setting("architect") == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) @@ -94,8 +92,6 @@ class RLWorld(World): if self.setting("vendors") == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.setting("vendors") == "early": - continue # Haggling if name == "Haggling" and self.setting("disable_charon"): @@ -192,21 +188,11 @@ class RLWorld(World): self.item_pool += [self.create_item(name) for _ in range(0, quantity)] # Fill any empty locations with filler items. - while len(self.item_pool) + len(self.prefill_items) < total_locations: + while len(self.item_pool) < total_locations: self.item_pool.append(self.create_item(self.get_filler_item_name())) self.multiworld.itempool += self.item_pool - def pre_fill(self) -> None: - reachable = [loc for loc in self.multiworld.get_reachable_locations(player=self.player) if not loc.item] - self.multiworld.random.shuffle(reachable) - items = self.prefill_items.copy() - for item in items: - reachable.pop().place_locked_item(item) - - def get_pre_fill_items(self) -> List[RLItem]: - return self.prefill_items - def get_filler_item_name(self) -> str: fillers = get_items_by_category("Filler") weights = [data.weight for data in fillers.values()] diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 1cb59325..b7310be1 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -127,7 +127,7 @@ class SMWorld(World): self.multiworld.local_items[self.player].value.add('No Energy') if (self.variaRando.args.morphPlacement == "early"): - self.multiworld.local_items[self.player].value.add('Morph') + self.multiworld.local_early_items[self.player]['Morph Ball'] = 1 self.remote_items = self.multiworld.remote_items[self.player] @@ -658,21 +658,6 @@ class SMWorld(World): return "Nothing" def pre_fill(self): - if (self.variaRando.args.morphPlacement == "early") and next((item for item in self.multiworld.itempool if item.player == self.player and item.name == "Morph Ball"), False): - viable = [] - for location in self.multiworld.get_locations(): - if location.player == self.player \ - and location.item is None \ - and location.can_reach(self.multiworld.state): - viable.append(location) - self.multiworld.random.shuffle(viable) - key = self.multiworld.create_item("Morph Ball", self.player) - loc = viable.pop() - loc.place_locked_item(key) - self.multiworld.itempool[:] = [item for item in self.multiworld.itempool if - item.player != self.player or - item.name != "Morph Ball"] - if len(self.NothingPool) > 0: nonChozoLoc = [] chozoLoc = [] diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index d421fe49..12359d40 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -47,8 +47,7 @@ class SubnauticaWorld(World): creatures_to_scan: List[str] def generate_early(self) -> None: - if "Seaglide Fragment" not in self.multiworld.early_items[self.player]: - self.multiworld.early_items[self.player].value["Seaglide Fragment"] = 2 + self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2 scan_option: Options.AggressiveScanLogic = self.multiworld.creature_scan_logic[self.player] creature_pool = scan_option.get_pool() diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md index 63a0ec5a..16000dbe 100644 --- a/worlds/zillion/docs/setup_en.md +++ b/worlds/zillion/docs/setup_en.md @@ -49,15 +49,6 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from them. -### Advanced settings - -The [advanced settings page](/tutorial/Archipelago/advanced_settings/en) describes more options you can put in your configuration file. - - A recommended setting for Zillion is: -``` - early_items: - Scope: 1 -``` - ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck). diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index d24b5fd5..2c5a9dd8 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -192,6 +192,11 @@ class ZillionRedIDCardCount(Range): display_name = "Red ID Card count" +class ZillionEarlyScope(Toggle): + """ make sure Scope is available early """ + display_name = "early scope" + + class ZillionSkill(Range): """ the difficulty level of the game """ range_start = 0 @@ -236,6 +241,7 @@ zillion_options: Dict[str, AssembleOptions] = { "floppy_disk_count": ZillionFloppyDiskCount, "scope_count": ZillionScopeCount, "red_id_card_count": ZillionRedIDCardCount, + "early_scope": ZillionEarlyScope, "skill": ZillionSkill, "starting_cards": ZillionStartingCards, "room_gen": ZillionRoomGen, @@ -352,6 +358,10 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": room_gen = cast(ZillionRoomGen, wo.room_gen[p]) + early_scope = cast(ZillionEarlyScope, wo.early_scope[p]) + if early_scope: + world.early_items[p]["Scope"] = 1 + zz_item_counts = convert_item_counts(item_counts) zz_op = ZzOptions( zz_item_counts, @@ -365,7 +375,7 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": floppy_req.value, wo.continues[p].value, wo.randomize_alarms[p].value, - False, # early scope can be done with AP early_items + False, # early scope is done with AP early_items API True, # balance defense starting_cards.value, bool(room_gen.value)