diff --git a/ManualClient.py b/ManualClient.py index 625e199d3a5d..ecd2bcae847f 100644 --- a/ManualClient.py +++ b/ManualClient.py @@ -37,11 +37,8 @@ def __init__(self, server_address, password): async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(ManualContext, self).server_auth(password_requested) - - if "Manual_" not in self.ui.game_bar_text.text: - raise Exception("The Manual client can only be used for Manual games.") - self.game = self.ui.game_bar_text.text + self.game = "Manual" self.location_names_to_id = dict([(value, key) for key, value in self.location_names.items()]) @@ -117,19 +114,11 @@ class ManualManager(GameManager): def __init__(self, ctx): super().__init__(ctx) - def build(self) -> Layout: + def build(self) -> Layout: super(ManualManager, self).build() - + # self.manual_game_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30) - game_bar_label = Label(text="Manual Game ID", size=(150, 30), size_hint_y=None, size_hint_x=None) - self.manual_game_layout.add_widget(game_bar_label) - self.game_bar_text = TextInput(text="Manual_{\"game\" from game.json}_{\"player\" from game.json}", - size_hint_y=None, height=30, multiline=False, write_tab=False) - self.manual_game_layout.add_widget(self.game_bar_text) - - self.grid.add_widget(self.manual_game_layout, 3) - panel = TabbedPanelItem(text="Tracker and Locations", size_hint_y = 1) self.tracker_and_locations_panel = panel.content = TrackerAndLocationsLayout(cols = 2) diff --git a/worlds/_manual/Data.py b/worlds/_manual/Data.py deleted file mode 100644 index 71bad285477c..000000000000 --- a/worlds/_manual/Data.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -import os -import pkgutil - -# blatantly copied from the minecraft ap world because why not -def load_data_file(*args) -> dict: - fname = os.path.join("data", *args) - return json.loads(pkgutil.get_data(__name__, fname).decode()) - -game_table = load_data_file('game.json') -item_table = load_data_file('items.json') -#progressive_item_table = load_data_file('progressive_items.json') -progressive_item_table = {} -location_table = load_data_file('locations.json') diff --git a/worlds/_manual/Game.py b/worlds/_manual/Game.py deleted file mode 100644 index ebbfebe2f581..000000000000 --- a/worlds/_manual/Game.py +++ /dev/null @@ -1,10 +0,0 @@ -from .Data import game_table - -game_name = "Manual_%s_%s" % (game_table["game"], game_table["player"]) -filler_item_name = game_table["filler_item_name"] - -# Programmatically generate starting indexes for items and locations based upon the game name and player name to aim for non-colliding indexes -starting_index = (ord(game_table["game"][:1]) * 1000000) + \ - (ord(game_table["game"][1:2]) * 100000) + \ - (ord(game_table["player"][:1]) * 10000) + \ - (ord(game_table["player"][-1:]) * 1000) diff --git a/worlds/_manual/Items.py b/worlds/_manual/Items.py deleted file mode 100644 index 761e7e99d5fc..000000000000 --- a/worlds/_manual/Items.py +++ /dev/null @@ -1,63 +0,0 @@ -from BaseClasses import Item -from .Data import item_table, progressive_item_table -from .Game import filler_item_name, starting_index - -###################### -# Generate item lookups -###################### - -item_id_to_name = {} -item_name_to_item = {} -advancement_item_names = set() -lastItemId = -1 - -count = starting_index - -# add the filler item to the list of items for lookup -item_table.append({ - "name": filler_item_name -}) - -# add sequential generated ids to the lists -for key, val in enumerate(item_table): - item_table[key]["id"] = count - item_table[key]["progression"] = val["progression"] if "progression" in val else False - count += 1 - -for item in item_table: - item_name = item["name"] - item_id_to_name[item["id"]] = item_name - item_name_to_item[item_name] = item - if item["progression"]: - advancement_item_names.add(item_name) - - if item["id"] != None: - lastItemId = max(lastItemId, item["id"]) - -progressive_item_list = {} - -for item in progressive_item_table: - progressiveName = progressive_item_table[item] - if progressiveName not in progressive_item_list: - progressive_item_list[progressiveName] = [] - progressive_item_list[progressiveName].append(item) - -for progressiveItemName in progressive_item_list.keys(): - lastItemId += 1 - generatedItem = {} - generatedItem["id"] = lastItemId - generatedItem["name"] = progressiveItemName - generatedItem["progression"] = item_name_to_item[progressive_item_list[progressiveItemName][0]]["progression"] - item_name_to_item[progressiveItemName] = generatedItem - item_id_to_name[lastItemId] = progressiveItemName - -item_id_to_name[None] = "__Victory__" -item_name_to_id = {name: id for id, name in item_id_to_name.items()} - -###################### -# Item classes -###################### - - -class ManualItem(Item): - game = "Manual" diff --git a/worlds/_manual/Locations.py b/worlds/_manual/Locations.py deleted file mode 100644 index e2be438f6d32..000000000000 --- a/worlds/_manual/Locations.py +++ /dev/null @@ -1,49 +0,0 @@ -from BaseClasses import Location -from .Data import location_table -from .Game import starting_index - -###################### -# Generate location lookups -###################### - -count = starting_index + 500 # 500 each for items and locations -custom_victory_location = {} -victory_key = {} - -# add sequential generated ids to the lists -for key, _ in enumerate(location_table): - if "victory" in location_table[key] and location_table[key]["victory"]: - custom_victory_location = location_table[key] - victory_key = key # store the victory location to be removed later - - continue - - location_table[key]["id"] = count - location_table[key]["region"] = "Manual" # all locations are in the same region for Manual - count += 1 - -if victory_key: - location_table.pop(victory_key) - -# Add the game completion location, which will have the Victory item assigned to it automatically -location_table.append({ - "id": count + 1, - "name": "__Manual Game Complete__", - "region": "Manual", - "requires": custom_victory_location["requires"] if "requires" in custom_victory_location else [] -}) - -location_id_to_name = {} -for item in location_table: - location_id_to_name[item["id"]] = item["name"] - -# location_id_to_name[None] = "__Manual Game Complete__" -location_name_to_id = {name: id for id, name in location_id_to_name.items()} - -###################### -# Location classes -###################### - - -class ManualLocation(Location): - game = "Manual" diff --git a/worlds/_manual/Options.py b/worlds/_manual/Options.py deleted file mode 100644 index 10a5fa730c9c..000000000000 --- a/worlds/_manual/Options.py +++ /dev/null @@ -1,15 +0,0 @@ -from Options import FreeText -from BaseClasses import MultiWorld -from typing import Union - -manual_options = {} - -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 - -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]: - option = getattr(world, name, None) - if option == None: - return 0 - - return option[player].value \ No newline at end of file diff --git a/worlds/_manual/Regions.py b/worlds/_manual/Regions.py deleted file mode 100644 index c39944b74693..000000000000 --- a/worlds/_manual/Regions.py +++ /dev/null @@ -1,48 +0,0 @@ -from BaseClasses import Entrance, MultiWorld, Region -from .Locations import ManualLocation -from ..AutoWorld import World - -regionMap = { - "Manual": [] -} - -def create_regions(base: World, world: MultiWorld, player: int): - # Create regions and assign locations to each region - for region in regionMap: - exit_array = regionMap[region] - if len(exit_array) == 0: - exit_array = None - - new_region = create_region(base, world, player, region, [ - location["name"] for location in base.location_table if location["region"] == region - ], exit_array) - world.regions += [new_region] - - menu = create_region(base, world, player, "Menu", None, ["Manual"]) - world.regions += [menu] - menuConn = world.get_entrance("MenuToManual", player) - menuConn.connect(world.get_region("Manual", player)) - - # Link regions together - for region in regionMap: - for linkedRegion in regionMap[region]: - connection = world.get_entrance(getConnectionName(region, linkedRegion), player) - connection.connect(world.get_region(linkedRegion, player)) - -def create_region(base: World, world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, player, world) - - if locations: - for location in locations: - loc_id = base.location_name_to_id.get(location, 0) - locationObj = ManualLocation(player, location, loc_id, ret) - ret.locations.append(locationObj) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, getConnectionName(name, exit), ret)) - return ret - -def getConnectionName(entranceName: str, exitName: str): - return entranceName + "To" + exitName - - diff --git a/worlds/_manual/__init__.py b/worlds/_manual/__init__.py deleted file mode 100644 index 1fd5e24e6fed..000000000000 --- a/worlds/_manual/__init__.py +++ /dev/null @@ -1,132 +0,0 @@ -from .Data import item_table, progressive_item_table, location_table -from .Game import game_name, filler_item_name -from .Locations import location_id_to_name, location_name_to_id -from .Items import item_id_to_name, item_name_to_id, item_name_to_item, advancement_item_names - -from .Regions import create_regions -from .Items import ManualItem -from .Rules import set_rules -from .Options import get_option_value, manual_options - -from BaseClasses import ItemClassification, Tutorial -from ..AutoWorld import World, WebWorld - - -class ManualWeb(WebWorld): - tutorials = [Tutorial( - "Multiworld Setup Guide", - "A guide to setting up manual game integration for Archipelago multiworld games.", - "English", - "setup_en.md", - "setup/en", - ["Fuzzy"] - )] - - -class ManualWorld(World): - """ - Manual games allow you to set custom check locations and custom item names that will be rolled into a multiworld. - This allows any variety of game -- PC, console, board games, Microsoft Word memes... really anything -- to be part of a multiworld randomizer. - The key component to including these games is some level of manual restriction. Since the items are not actually withheld from the player, - the player must manually refrain from using these gathered items until the tracker shows that they have been acquired or sent. - """ - game: str = game_name - web = ManualWeb() - - option_definitions = manual_options - data_version = 2 - required_client_version = (0, 3, 4) - - # These properties are set from the imports of the same name above. - item_table = item_table - progressive_item_table = progressive_item_table - item_id_to_name = item_id_to_name - item_name_to_id = item_name_to_id - item_name_to_item = item_name_to_item - advancement_item_names = advancement_item_names - location_table = location_table - location_id_to_name = location_id_to_name - location_name_to_id = location_name_to_id - - def pre_fill(self): - location_game_complete = self.multiworld.get_location("__Manual Game Complete__", self.player) - location_game_complete.address = None - - location_game_complete.place_locked_item( - ManualItem("__Victory__", ItemClassification.progression, None, player=self.player)) - - def generate_basic(self): - # Generate item pool - pool = [] - configured_item_names = self.item_id_to_name.copy() - - for name in configured_item_names.values(): - if name == "__Victory__": - continue - - # If it's the filler item, skip it until we know if we need any extra items - if name == filler_item_name: - continue - - if (hasattr(self.multiworld, "progressive_items") and len(self.multiworld.progressive_items) > 0): - shouldUseProgressive = (self.multiworld.progressive_items[self.player].value); - - if shouldUseProgressive and name in self.progressive_item_table: - name = self.progressive_item_table[name] - - item = self.item_name_to_item[name] - classification = ItemClassification.filler - - if "useful" in item and item["useful"]: - classification = ItemClassification.useful - - if "progression" in item and item["progression"]: - classification = ItemClassification.progression - - item_name_parts = name.split(":") - item_name = name - item_count = 1 - - if len(item_name_parts) > 1: - item_name = item_name_parts[0] - item_count = int(item_name_parts[1]) - - for i in range(item_count): - manual_item = ManualItem(item_name, classification, - self.item_name_to_id[name], player=self.player) - - pool.append(manual_item) - - if len(item_name_parts) > 1: - # remove the item with a count from the lookups and replace them with the item without - self.item_name_to_item.pop(name) - self.item_name_to_item[item_name] = item - self.item_id_to_name[item["id"]] = item_name - - - extras = len(location_table) - len(pool) - 1 # subtracting 1 because of Victory; seems right - - if extras > 0: - for i in range(0, extras): - manual_item = ManualItem(filler_item_name, ItemClassification.filler, - self.item_name_to_id[filler_item_name], player=self.player) - - pool.append(manual_item) - - self.multiworld.itempool += pool - - def set_rules(self): - set_rules(self, self.multiworld, self.player) - - def create_regions(self): - create_regions(self, self.multiworld, self.player) - - def get_pre_fill_items(self): - return [] - - def fill_slot_data(self): - # return { - # "DeathLink": bool(self.multiworld.death_link[self.player].value) - # } - - pass diff --git a/worlds/_manual/data/game.json b/worlds/_manual/data/game.json deleted file mode 100644 index a969fc204d34..000000000000 --- a/worlds/_manual/data/game.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "game": "Celeste", - "player": "Fuzzy", - - "filler_item_name": "Hot Sauce" -} \ No newline at end of file diff --git a/worlds/_manual/data/items.json b/worlds/_manual/data/items.json deleted file mode 100644 index 644fb13f86f7..000000000000 --- a/worlds/_manual/data/items.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "name": "Level Key:6", - "progression": true - }, - { - "name": "A Wild Theo Appears", - "progression": true - }, - { - "name": "Cackling Grandma", - "progression": true - }, - { - "name": "Badeline Upgrade", - "progression": true - }, - { - "name": "Burrito at Flag 1", - "progression": true - }, - { - "name": "Burrito at Flag 9", - "useful": true - }, - { - "name": "Partially Eaten Burrito", - "progression": false, - "useful": false - } -] diff --git a/worlds/_manual/data/locations.json b/worlds/_manual/data/locations.json deleted file mode 100644 index 96f032ea7f5e..000000000000 --- a/worlds/_manual/data/locations.json +++ /dev/null @@ -1,120 +0,0 @@ -[ - { - "name": "Ch 1 - All Berries", - "requires": [] - }, - { - "name": "Ch 1 - Crystal Heart", - "requires": [] - }, - { - "name": "Ch 2 - All Berries", - "requires": [ - "Level Key" - ] - }, - { - "name": "Ch 2 - Crystal Heart", - "requires": [ - "Level Key" - ] - }, - { - "name": "Ch 3 - All Berries", - "requires": [ - "Level Key:2" - ] - }, - { - "name": "Ch 3 - Crystal Heart", - "requires": [ - "Level Key:2" - ] - }, - { - "name": "Ch 4 - All Berries", - "requires": [ - "Level Key:3" - ] - }, - { - "name": "Ch 4 - Crystal Heart", - "requires": [ - "Level Key:3" - ] - }, - { - "name": "Ch 5 - All Berries", - "requires": [ - "Level Key:4", - { - "or": [ - "A Wild Theo Appears" - ] - } - ] - }, - { - "name": "Ch 5 - Crystal Heart", - "requires": [ - "Level Key:4", - { - "or": [ - "A Wild Theo Appears" - ] - } - ] - }, - { - "name": "Ch 6 - All Berries", - "requires": [ - "Level Key:5", - { - "or": [ - "Cackling Grandma" - ] - } - ] - }, - { - "name": "Ch 6 - Crystal Heart", - "requires": [ - "Level Key:5", - { - "or": [ - "Cackling Grandma" - ] - } - ] - }, - { - "name": "Ch 7 - All Berries", - "requires": [ - "Level Key:6", - { - "or": [ - "Badeline Upgrade" - ] - } - ] - }, - { - "name": "Ch 7 - Crystal Heart", - "requires": [ - "Level Key:6", - { - "or": [ - "Badeline Upgrade" - ] - } - ] - }, - { - "name": "Summit Complete", - "victory": true, - "requires": [ - "Level Key:6", - "Burrito at Flag 1" - ] - } -] \ No newline at end of file diff --git a/worlds/manual/Items.py b/worlds/manual/Items.py new file mode 100644 index 000000000000..04afe60b0f31 --- /dev/null +++ b/worlds/manual/Items.py @@ -0,0 +1,33 @@ +from BaseClasses import Item, ItemClassification + + +def create_items(self): + # Generate item pool + pool = [] + + for item in self.multiworld.items[self.player].value: + + item_name = item["name"].split(":")[0] + if ":" in item["name"]: + item_count = int(item["name"].split(":")[1]) + else: + item_count = 1 + + classification = getattr(ItemClassification, "filler" if "classification" not in item else item["classification"]) + + for i in range(item_count): + manual_item = ManualItem(item_name, classification, + self.item_name_to_id[item_name], player=self.player) + + pool.append(manual_item) + + extras = (len(self.multiworld.locations[self.player].value) - len(pool)) - 1 + + for i in range(0, extras): + pool.append(self.create_filler()) + + self.multiworld.itempool += pool + + +class ManualItem(Item): + game = "Manual" diff --git a/worlds/manual/Locations.py b/worlds/manual/Locations.py new file mode 100644 index 000000000000..83fbed8acd25 --- /dev/null +++ b/worlds/manual/Locations.py @@ -0,0 +1,5 @@ +from BaseClasses import Location + + +class ManualLocation(Location): + game = "Manual" diff --git a/worlds/manual/Options.py b/worlds/manual/Options.py new file mode 100644 index 000000000000..40d8e3170349 --- /dev/null +++ b/worlds/manual/Options.py @@ -0,0 +1,24 @@ +from Options import OptionList, FreeText + + +class Locations(OptionList): + """List of locations. See documentation.""" + display_name = "Locations" + + +class Items(OptionList): + """List of items. See documentation""" + display_name = "Items" + + +class FillerItemName(FreeText): + """Item name for filler item to be used if locations specified are greater than items specified.""" + default = "The regret of not specifying a custom filler item" + display_name = "Filler Item Name" + + +manual_options = { + "locations": Locations, + "items": Items, + "filler_item_name": FillerItemName, +} \ No newline at end of file diff --git a/worlds/manual/Regions.py b/worlds/manual/Regions.py new file mode 100644 index 000000000000..14007b42499a --- /dev/null +++ b/worlds/manual/Regions.py @@ -0,0 +1,13 @@ +from BaseClasses import Region +from .Locations import ManualLocation + + +def create_regions(self): + region = Region("Menu", self.player, self.multiworld) + region.locations.append(ManualLocation(self.player, "Victory", None, region)) + for location in self.multiworld.locations[self.player].value: + if location["name"] != "Victory": + region.locations.append(ManualLocation(self.player, location["name"], + self.location_name_to_id[location["name"]], region)) + self.multiworld.regions.append(region) + diff --git a/worlds/_manual/Rules.py b/worlds/manual/Rules.py similarity index 70% rename from worlds/_manual/Rules.py rename to worlds/manual/Rules.py index 9001de122be8..2ce2ef1a1865 100644 --- a/worlds/_manual/Rules.py +++ b/worlds/manual/Rules.py @@ -1,12 +1,13 @@ from ..generic.Rules import set_rule -from ..AutoWorld import World -from BaseClasses import MultiWorld -def set_rules(base: World, world: MultiWorld, player: int): + +def set_rules(self): + multiworld = self.multiworld + player = self.player # Location access rules - for location in base.location_table: - locFromWorld = world.get_location(location["name"], player) - if "requires" in location: # Specific item access required + for location in multiworld.locations[player].value: + locFromWorld = multiworld.get_location(location["name"], player) + if "requires" in location: # Specific item access required def fullLocationCheck(state, location=location): canAccess = True @@ -43,10 +44,5 @@ def fullLocationCheck(state, location=location): return canAccess set_rule(locFromWorld, fullLocationCheck) - else: # Only region access required - def allRegionsAccessible(state, location=location): - return True - set_rule(locFromWorld, allRegionsAccessible) # everything is in the same region in manual - # Victory requirement - world.completion_condition[player] = lambda state: state.has("__Victory__", player) \ No newline at end of file + multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/manual/__init__.py b/worlds/manual/__init__.py new file mode 100644 index 000000000000..b3441f8ee226 --- /dev/null +++ b/worlds/manual/__init__.py @@ -0,0 +1,97 @@ +from .Regions import create_regions +from .Items import ManualItem, create_items +from .Rules import set_rules +from .Options import manual_options + +from BaseClasses import ItemClassification, Tutorial +from worlds.AutoWorld import World, WebWorld, AutoWorldRegister +import worlds + +class ManualWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up manual game integration for Archipelago multiworld games.", + "English", + "setup_en.md", + "setup/en", + ["Fuzzy"] + )] + + +class ManualWorld(World): + """ + Manual games allow you to set custom check locations and custom item names that will be rolled into a multiworld. + This allows any variety of game -- PC, console, board games, Microsoft Word memes... really anything -- to be part of a multiworld randomizer. + The key component to including these games is some level of manual restriction. Since the items are not actually withheld from the player, + the player must manually refrain from using these gathered items until the tracker shows that they have been acquired or sent. + """ + game: str = "Manual" + web = ManualWeb() + + option_definitions = manual_options + data_version = 0 + required_client_version = (0, 3, 4) + + create_items = create_items + create_regions = create_regions + set_rules = set_rules + + item_name_to_id = {} + location_name_to_id = {} + + def pre_fill(self): + victory = self.multiworld.get_location("Victory", self.player) + victory.place_locked_item(ManualItem("Victory", ItemClassification.progression, None, self.player)) + + def stage_generate_early(self): + item_index = 1000000 + loc_index = 1000000 + item_name_to_id = {} + location_name_to_id = {} + for player in self.get_game_players("Manual"): + for item in self.items[player].value: + item_name = item["name"].split(":")[0] + if item_name not in item_name_to_id: + item_name_to_id[item_name] = item_index + item_index += 1 + for location in self.locations[player].value: + if location["name"] not in location_name_to_id: + location_name_to_id[location["name"]] = loc_index + loc_index += 1 + if self.filler_item_name[player].value not in item_name_to_id: + item_name_to_id[self.filler_item_name[player].value] = item_index + item_index += 1 + location_id_to_name = {} + for location, id in location_name_to_id.items(): + location_id_to_name[id] = location + item_id_to_name = {} + for item, id in item_name_to_id.items(): + item_id_to_name[id] = item + for world in [AutoWorldRegister.world_types["Manual"], *self.get_game_worlds("Manual")]: + world.item_name_to_id = item_name_to_id + world.location_name_to_id = location_name_to_id + world.location_id_to_name = location_id_to_name + world.item_id_to_name = item_id_to_name + worlds.network_data_package["games"]["Manual"] = world.get_data_package_data() + + def fill_slot_data(self): + # return { + # "DeathLink": bool(self.multiworld.death_link[self.player].value) + # } + + pass + + def create_item(self, name): + if name == self.multiworld.filler_item_name[self.player].value: + return ManualItem(name, ItemClassification.filler, self.item_name_to_id[name], self.player) + for item in self.multiworld.items[self.player].value: + if item["name"] == name: + classification = getattr(ItemClassification, + "filler" if "classification" not in item else item["classification"]) + return ManualItem(name, classification, self.item_name_to_id[name], self.player) + raise KeyError(f"Invalid item name {name} for Manual player {self.player}") + + + def get_filler_item_name(self) -> str: + return self.multiworld.filler_item_name[self.player].value + diff --git a/worlds/_manual/docs/en_Manual.md b/worlds/manual/docs/en_Manual.md similarity index 100% rename from worlds/_manual/docs/en_Manual.md rename to worlds/manual/docs/en_Manual.md diff --git a/worlds/_manual/docs/setup_en.md b/worlds/manual/docs/setup_en.md similarity index 100% rename from worlds/_manual/docs/setup_en.md rename to worlds/manual/docs/setup_en.md