From 31c550d4102983c06a7308a6de71e928bfb23ef7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 12 Jul 2021 13:54:47 +0200 Subject: [PATCH] AutoWorld: basic Item handling --- BaseClasses.py | 3 ++- Main.py | 6 +++--- Mystery.py | 8 ++++---- worlds/AutoWorld.py | 15 ++++++++++++--- worlds/alttp/Items.py | 18 +++++++++++------- worlds/alttp/__init__.py | 17 ++++++++++++----- worlds/factorio/__init__.py | 29 ++++++++++++++++++----------- worlds/generic/__init__.py | 5 +---- worlds/hk/__init__.py | 8 ++++++-- worlds/minecraft/Items.py | 5 +++-- worlds/minecraft/__init__.py | 21 ++++++++++++--------- 11 files changed, 84 insertions(+), 51 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d48ec418..91438987 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -272,6 +272,8 @@ class MultiWorld(): return next(location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player) + def create_item(self, item_name: str, player: int) -> Item: + return self.worlds[player].create_item(item_name) def push_precollected(self, item: Item): item.world = self @@ -859,7 +861,6 @@ class CollectionState(object): (self.has('Progressive Weapons', player, 1) and self.has('Bed', player))) return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) - def collect(self, item: Item, event: bool = False, location: Location = None) -> bool: if location: self.locations_checked.add(location) diff --git a/Main.py b/Main.py index 70857341..7f89ea90 100644 --- a/Main.py +++ b/Main.py @@ -9,7 +9,7 @@ import pickle from typing import Dict, Tuple from BaseClasses import MultiWorld, CollectionState, Region, Item -from worlds.alttp.Items import ItemFactory, item_name_groups +from worlds.alttp.Items import item_name_groups from worlds.alttp.Regions import create_regions, mark_light_world_regions, \ lookup_vanilla_location_to_entrance from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions @@ -453,8 +453,8 @@ def main(args, seed=None): for index, take_any in enumerate(takeanyregions): for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]: - item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], - region.player) + item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], + region.player) player = region.player location_id = SHOP_ID_START + total_shop_slots + index diff --git a/Mystery.py b/Mystery.py index ec65753f..9a85c131 100644 --- a/Mystery.py +++ b/Mystery.py @@ -516,10 +516,11 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b ret.game = get_choice("game", weights) if ret.game not in weights: raise Exception(f"No game options for selected game \"{ret.game}\" found.") + world_type = AutoWorldRegister.world_types[ret.game] game_weights = weights[ret.game] ret.local_items = set() for item_name in game_weights.get('local_items', []): - items = item_name_groups.get(item_name, {item_name}) + items = world_type.item_name_groups.get(item_name, {item_name}) for item in items: if item in lookup_any_item_name_to_id: ret.local_items.add(item) @@ -528,7 +529,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b ret.non_local_items = set() for item_name in game_weights.get('non_local_items', []): - items = item_name_groups.get(item_name, {item_name}) + items = world_type.item_name_groups.get(item_name, {item_name}) for item in items: if item in lookup_any_item_name_to_id: ret.non_local_items.add(item) @@ -556,7 +557,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b else: setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) except Exception as e: - raise Exception(f"Error generating option {option_name} in {ret.game}") + raise Exception(f"Error generating option {option_name} in {ret.game}") from e else: setattr(ret, option_name, option(option.default)) if ret.game == "Minecraft": @@ -778,7 +779,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): get_choice("direction", placement, "both") )) - ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 58462a8d..35c3c29d 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,4 +1,6 @@ -from BaseClasses import MultiWorld +from typing import Dict, Set, Tuple + +from BaseClasses import MultiWorld, Item, CollectionState class AutoWorldRegister(type): @@ -29,6 +31,9 @@ class World(metaclass=AutoWorldRegister): player: int options: dict = {} topology_present: bool = False # indicate if world type has any meaningful layout/pathing + item_names: Set[str] = frozenset() + # maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"} + item_name_groups: Dict[str, Set[str]] = {} def __init__(self, world: MultiWorld, player: int): self.world = world @@ -49,15 +54,19 @@ class World(metaclass=AutoWorldRegister): If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" pass - def get_required_client_version(self) -> tuple: + def get_required_client_version(self) -> Tuple[int, int, int]: return 0, 0, 3 # end of Main.py calls - def collect(self, state, item) -> bool: + def collect(self, state: CollectionState, item: Item) -> bool: """Collect an item into state. For speed reasons items that aren't logically useful get skipped.""" if item.advancement: state.prog_items[item.name, item.player] += 1 return True # indicate that a logical state change has occured return False + def create_item(self, name: str) -> Item: + """Create an item for this world type and player. + Warning: this may be called with self.world = None, for example by MultiServer""" + raise NotImplementedError diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index 21bfe38c..849977b8 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -6,17 +6,19 @@ def GetBeemizerItem(world, player, item): if world.beemizer[player] and item_name in trap_replaceable: if world.random.random() < world.beemizer[player] * 0.25: if world.random.random() < (0.5 + world.beemizer[player] * 0.1): - return "Bee Trap" if isinstance(item, str) else ItemFactory("Bee Trap", player) + return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player) else: - return "Bee" if isinstance(item, str) else ItemFactory("Bee", player) + return "Bee" if isinstance(item, str) else world.create_item("Bee", player) else: return item else: return item -def ItemFactory(items, player): - from worlds.alttp import ALttPItem +# should be replaced with direct world.create_item(item) call in the future +def ItemFactory(items, player: int): + from worlds.alttp import ALTTPWorld + world = ALTTPWorld(None, player) ret = [] singleton = False if isinstance(items, str): @@ -24,7 +26,7 @@ def ItemFactory(items, player): singleton = True for item in items: if item in item_table: - ret.append(ALttPItem(item, *item_table[item], player)) + ret.append(world.create_item(item)) else: raise Exception(f"Unknown item {item}") @@ -211,6 +213,8 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl 'Open Floodgate': ItemData(True, 'Event', None, None, None, None, None, None, None, None), } +as_dict_item_table = {name: data._asdict() for name, data in item_table.items()} + progression_mapping = { "Golden Sword": ("Progressive Sword", 4), "Tempered Sword": ("Progressive Sword", 3), @@ -268,8 +272,8 @@ for basename, substring in _simple_groups: del (_simple_groups) -progression_items = {name for name, data in item_table.items() if type(data[2]) == int and data[0]} -item_name_groups['Everything'] = {name for name, data in item_table.items() if type(data[2]) == int} +progression_items = {name for name, data in item_table.items() if type(data.item_code) == int and data.advancement} +item_name_groups['Everything'] = {name for name, data in item_table.items() if type(data.item_code) == int} item_name_groups['Progression Items'] = progression_items item_name_groups['Non Progression Items'] = item_name_groups['Everything'] - progression_items diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index e3c5e409..8fa61a78 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -3,11 +3,15 @@ from typing import Optional from BaseClasses import Location, Item, CollectionState from ..AutoWorld import World from .Options import alttp_options +from .Items import as_dict_item_table, item_name_groups, item_table + class ALTTPWorld(World): game: str = "A Link to the Past" options = alttp_options topology_present = True + item_name_groups = item_name_groups + item_names = frozenset(item_table) def collect(self, state: CollectionState, item: Item) -> bool: if item.name.startswith('Progressive '): @@ -66,6 +70,9 @@ class ALTTPWorld(World): def get_required_client_version(self) -> tuple: return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) + def create_item(self, name: str) -> Item: + return ALttPItem(name, self.player, **as_dict_item_table[name]) + class ALttPLocation(Location): game: str = "A Link to the Past" @@ -80,16 +87,16 @@ class ALttPLocation(Location): class ALttPItem(Item): - game: str = "A Link to the Past" - def __init__(self, name='', advancement=False, type=None, code=None, pedestal_hint=None, pedestal_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None, hint_text=None, player=None): - super(ALttPItem, self).__init__(name, advancement, code, player) + def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None, + sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None): + super(ALttPItem, self).__init__(name, advancement, item_code, player) self.type = type self._pedestal_hint_text = pedestal_hint self.pedestal_credit_text = pedestal_credit - self.sickkid_credit_text = sickkid_credit + self.sickkid_credit_text = sick_kid_credit self.zora_credit_text = zora_credit self.magicshop_credit_text = witch_credit - self.fluteboy_credit_text = fluteboy_credit + self.fluteboy_credit_text = flute_boy_credit self._hint_text = hint_text \ No newline at end of file diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 14e18246..d05557e3 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -4,29 +4,30 @@ from BaseClasses import Region, Entrance, Location, Item from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \ all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ - get_science_pack_pools, Recipe, recipes, technology_table + get_science_pack_pools, Recipe, recipes, technology_table, tech_table from .Shapes import get_shapes from .Mod import generate_mod from .Options import factorio_options + +class FactorioItem(Item): + game = "Factorio" + + class Factorio(World): game: str = "Factorio" static_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes = {} additional_advancement_technologies = set() + item_names = frozenset(tech_table) def generate_basic(self): - for tech_name, tech_id in base_tech_table.items(): - if self.world.progressive and tech_name in tech_to_progressive_lookup: - item_name = tech_to_progressive_lookup[tech_name] - tech_id = progressive_tech_table[item_name] + for tech_name in base_tech_table: + if self.world.progressive: + item_name = tech_to_progressive_lookup.get(tech_name, tech_name) else: - item_name = tech_name - - tech_item = Item(item_name, item_name in advancement_technologies or - item_name in self.additional_advancement_technologies, - tech_id, self.player) - tech_item.game = "Factorio" + item_name = item_name + tech_item = self.create_item(item_name) if tech_name in self.static_nodes: self.world.get_location(tech_name, self.player).place_locked_item(tech_item) else: @@ -157,3 +158,9 @@ class Factorio(World): if tech in tech_to_progressive_lookup: prog_add.add(tech_to_progressive_lookup[tech]) self.additional_advancement_technologies |= prog_add + + def create_item(self, name: str) -> Item: + assert name in tech_table + return FactorioItem(name, name in advancement_technologies or + name in self.additional_advancement_technologies, + tech_table[name], self.player) \ No newline at end of file diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 7ee82e18..851c6be5 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -1,6 +1,7 @@ from typing import NamedTuple, Union import logging + class PlandoItem(NamedTuple): item: str location: str @@ -25,7 +26,3 @@ class PlandoConnection(NamedTuple): entrance: str exit: str direction: str # entrance, exit or both - - -class World(): - pass diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index db764a08..f7044073 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -1,4 +1,5 @@ import logging +from typing import Set logger = logging.getLogger("Hollow Knight") @@ -14,6 +15,7 @@ from ..AutoWorld import World class HKWorld(World): game: str = "Hollow Knight" options = hollow_knight_options + item_names: Set[str] = frozenset(item_table) def generate_basic(self): # Link regions @@ -22,8 +24,7 @@ class HKWorld(World): # Generate item pool pool = [] for item_name, item_data in item_table.items(): - - item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=self.player) + item = self.create_item(item_name) if item_data.type == "Event": event_location = self.world.get_location(item_name, self.player) @@ -83,6 +84,9 @@ class HKWorld(World): slot_data[option_name] = int(option.value) return slot_data + def create_item(self, name: str) -> Item: + item_data = item_table[name] + return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player) def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): ret = Region(name, None, name, player) diff --git a/worlds/minecraft/Items.py b/worlds/minecraft/Items.py index 72217b68..a5ddc23a 100644 --- a/worlds/minecraft/Items.py +++ b/worlds/minecraft/Items.py @@ -1,14 +1,15 @@ from BaseClasses import Item import typing + class ItemData(typing.NamedTuple): code: int progression: bool + class MinecraftItem(Item): game: str = "Minecraft" - def __init__(self, name: str, progression: bool, code: int, player: int): - super().__init__(name, progression, code if code else None, player) + item_table = { "Archery": ItemData(45000, True), diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 08f3d13b..d1d6e81e 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -1,9 +1,12 @@ +from typing import Dict, Set + + from .Items import MinecraftItem, item_table, item_frequencies from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table from .Regions import mc_regions, link_minecraft_structures from .Rules import set_rules -from BaseClasses import Region, Entrance +from BaseClasses import Region, Entrance, Item from .Options import minecraft_options from ..AutoWorld import World @@ -13,6 +16,7 @@ class MinecraftWorld(World): game: str = "Minecraft" options = minecraft_options topology_present = True + item_names = frozenset(item_table) def _get_mc_data(self): exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", @@ -27,7 +31,6 @@ class MinecraftWorld(World): 'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits} } - def generate_basic(self): link_minecraft_structures(self.world, self.player) @@ -35,9 +38,9 @@ class MinecraftWorld(World): pool_counts = item_frequencies.copy() if getattr(self.world, "bee_traps")[self.player]: pool_counts.update({"Rotten Flesh": 0, "Bee Trap (Minecraft)": 4}) - for item_name, item_data in item_table.items(): + for item_name in item_table: for count in range(pool_counts.get(item_name, 1)): - pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, self.player)) + pool.append(self.create_item(item_name)) prefill_pool = {} prefill_pool.update(events_table) @@ -49,7 +52,7 @@ class MinecraftWorld(World): for loc_name, item_name in prefill_pool.items(): item_data = item_table[item_name] location = self.world.get_location(loc_name, self.player) - item = MinecraftItem(item_name, item_data.progression, item_data.code, self.player) + item = self.create_item(item_name) self.world.push_item(location, item, collect=False) pool.remove(item) location.event = item_data.progression @@ -57,11 +60,9 @@ class MinecraftWorld(World): self.world.itempool += pool - def set_rules(self): set_rules(self.world, self.player) - def create_regions(self): def MCRegion(region_name: str, exits=[]): ret = Region(region_name, None, region_name, self.player) @@ -75,7 +76,6 @@ class MinecraftWorld(World): self.world.regions += [MCRegion(*r) for r in mc_regions] - def generate_output(self): import json from base64 import b64encode @@ -86,10 +86,13 @@ class MinecraftWorld(World): with open(output_path(filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) - def fill_slot_data(self): slot_data = self._get_mc_data() for option_name in minecraft_options: option = getattr(self.world, option_name)[self.player] slot_data[option_name] = int(option.value) return slot_data + + def create_item(self, name: str) -> Item: + item_data = item_table[name] + return MinecraftItem(name, item_data.progression, item_data.code, self.player) \ No newline at end of file