From ab5cb7adad770ee3cba7564764898e03341833e5 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 23 Apr 2023 15:59:46 -0400 Subject: [PATCH 01/35] Stardew Valley - Add alias for renamed Fishsanity option (#1758) --- worlds/stardew_valley/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 445c2a2e1bf4..e365dd6735bd 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -292,6 +292,7 @@ class Fishsanity(Choice): option_legendaries = 1 option_special = 2 option_randomized = 3 + alias_random_selection = option_randomized option_all = 4 From 67c307657267a619f3a74f3e64618717f963cd1a Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:16:01 -0400 Subject: [PATCH 02/35] SM: comeback fix6 and some refactor (#1756) refactored and cleaned a bit SMWorld class for best practices: - moved content of Regions.py and Rules.py in SMWorld - moved appropiate code to their dedicated World core functions - moved some Entrances being created in generate_basic to create_regions more comeback check fixes: - fixed setting progression door openers items local if doors_colors_rando is used - enable comeback check only for filling stage as later stages (progression balancing, accessibility and spoiler playthrough) are prone to fail with it --- worlds/sm/Regions.py | 42 ----- worlds/sm/Rules.py | 38 ----- worlds/sm/__init__.py | 366 ++++++++++++++++++++++++------------------ 3 files changed, 209 insertions(+), 237 deletions(-) delete mode 100644 worlds/sm/Regions.py delete mode 100644 worlds/sm/Rules.py diff --git a/worlds/sm/Regions.py b/worlds/sm/Regions.py deleted file mode 100644 index ee6af4082d6f..000000000000 --- a/worlds/sm/Regions.py +++ /dev/null @@ -1,42 +0,0 @@ -def create_regions(self, world, player: int): - from . import create_region - from BaseClasses import Entrance - from .variaRandomizer.logic.logic import Logic - from .variaRandomizer.graph.vanilla.graph_locations import locationsDict - - regions = [] - for accessPoint in Logic.accessPoints: - if not accessPoint.Escape: - regions.append(create_region(self, - world, - player, - accessPoint.Name, - None, - [accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()])) - - world.regions += regions - - # create a region for each location and link each to what the location has access - # we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path - # this is required in AP because a location cant have multiple parent regions - locationRegions = [] - for locationName, value in locationsDict.items(): - locationRegions.append(create_region( self, - world, - player, - locationName, - [locationName])) - for key in value.AccessFrom.keys(): - currentRegion =world.get_region(key, player) - currentRegion.exits.append(Entrance(player, key + "->"+ locationName, currentRegion)) - - world.regions += locationRegions - #create entrances - regionConcat = regions + locationRegions - for region in regionConcat: - for exit in region.exits: - exit.connect(world.get_region(exit.name[exit.name.find("->") + 2:], player)) - - world.regions += [ - create_region(self, world, player, 'Menu', None, ['StartAP']) - ] diff --git a/worlds/sm/Rules.py b/worlds/sm/Rules.py deleted file mode 100644 index 15706987ffd6..000000000000 --- a/worlds/sm/Rules.py +++ /dev/null @@ -1,38 +0,0 @@ -from worlds.generic.Rules import set_rule, add_rule - -from .variaRandomizer.graph.vanilla.graph_locations import locationsDict -from .variaRandomizer.logic.logic import Logic - -def evalSMBool(smbool, maxDiff): - return smbool.bool == True and smbool.difficulty <= maxDiff - -def add_accessFrom_rule(location, player, accessFrom): - add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items())) - -def add_postAvailable_rule(location, player, func): - add_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_available_rule(location, player, func): - set_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_entrance_rule(entrance, player, func): - set_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def add_entrance_rule(entrance, player, func): - add_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_rules(world, player): - world.completion_condition[player] = lambda state: state.has('Mother Brain', player) - - for key, value in locationsDict.items(): - location = world.get_location(key, player) - set_available_rule(location, player, value.Available) - if value.AccessFrom is not None: - add_accessFrom_rule(location, player, value.AccessFrom) - if value.PostAvailable is not None: - add_postAvailable_rule(location, player, value.PostAvailable) - - for accessPoint in Logic.accessPoints: - if not accessPoint.Escape: - for key, value1 in accessPoint.intraTransitions.items(): - set_entrance_rule(world.get_entrance(accessPoint.Name + "->" + key, player), player, value1) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index d1804d920921..ef7e50ba58e1 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -10,11 +10,10 @@ from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import World, AutoLogicRegister, WebWorld +from worlds.generic.Rules import set_rule, add_rule, add_item_rule logger = logging.getLogger("Super Metroid") -from .Regions import create_regions -from .Rules import set_rules, add_entrance_rule from .Options import sm_options from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols @@ -106,6 +105,7 @@ class SMWorld(World): def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() self.locations = {} + self.need_comeback_check = True super().__init__(world, player) @classmethod @@ -134,7 +134,7 @@ def generate_early(self): self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal") logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") - def generate_basic(self): + def create_items(self): itemPool = self.variaRando.container.itemPool self.startItems = [variaItem for item in self.multiworld.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name] if self.multiworld.start_inventory_removes_from_pool[self.player]: @@ -150,7 +150,6 @@ def generate_basic(self): pool = [] self.locked_items = {} self.NothingPool = [] - self.prefilled_locked_items = [] weaponCount = [0, 0, 0] for item in itemPool: isAdvancement = True @@ -180,12 +179,9 @@ def generate_basic(self): player=self.player) beamItems = ['Spazer', 'Ice', 'Wave' ,'Plasma'] - self.ammoItems = ['Missile', 'Super', 'PowerBomb'] if self.multiworld.doors_colors_rando[self.player].value != 0: if item.Type in beamItems: self.multiworld.local_items[self.player].value.add(item.Name) - elif item.Type in self.ammoItems and isAdvancement: - self.prefilled_locked_items.append(smitem) if itemClass == 'Boss': self.locked_items[item.Name] = smitem @@ -199,9 +195,96 @@ def generate_basic(self): for (location, item) in self.locked_items.items(): self.multiworld.get_location(location, self.player).place_locked_item(item) self.multiworld.get_location(location, self.player).address = None + + def evalSMBool(self, smbool, maxDiff): + return smbool.bool == True and smbool.difficulty <= maxDiff + + def add_entrance_rule(self, entrance, player, func): + add_rule(entrance, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - startAP = self.multiworld.get_entrance('StartAP', self.player) - startAP.connect(self.multiworld.get_region(self.variaRando.args.startLocation, self.player)) + def set_rules(self): + def add_accessFrom_rule(location, player, accessFrom): + add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and self.evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items())) + + def add_postAvailable_rule(location, player, func): + add_rule(location, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + def set_available_rule(location, player, func): + set_rule(location, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + def set_entrance_rule(entrance, player, func): + set_rule(entrance, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has('Mother Brain', self.player) + + ammoItems = ['Missile', 'Super', 'PowerBomb'] + for key, value in locationsDict.items(): + location = self.multiworld.get_location(key, self.player) + set_available_rule(location, self.player, value.Available) + if value.AccessFrom is not None: + add_accessFrom_rule(location, self.player, value.AccessFrom) + if value.PostAvailable is not None: + add_postAvailable_rule(location, self.player, value.PostAvailable) + + if self.multiworld.doors_colors_rando[self.player].value != 0: + add_item_rule(location, lambda item: item.type not in ammoItems or + (item.type in ammoItems and \ + (not item.advancement or (item.advancement and item.player == self.player)))) + + for accessPoint in Logic.accessPoints: + if not accessPoint.Escape: + for key, value1 in accessPoint.intraTransitions.items(): + set_entrance_rule(self.multiworld.get_entrance(accessPoint.Name + "->" + key, self.player), self.player, value1) + + def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): + ret = Region(name, player, world) + if locations: + for loc in locations: + location = self.locations[loc] + location.parent_region = ret + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + return ret + + def create_regions(self): + # create locations + for name in locationsDict: + self.locations[name] = SMLocation(self.player, name, self.location_name_to_id.get(name, None)) + + # create regions + regions = [] + for accessPoint in Logic.accessPoints: + if not accessPoint.Escape: + regions.append(self.create_region( self.multiworld, + self.player, + accessPoint.Name, + None, + [accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()])) + + self.multiworld.regions += regions + + # create a region for each location and link each to what the location has access + # we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path + # this is required in AP because a location cant have multiple parent regions + locationRegions = [] + for locationName, value in locationsDict.items(): + locationRegions.append(self.create_region( self.multiworld, + self.player, + locationName, + [locationName])) + for key in value.AccessFrom.keys(): + currentRegion = self.multiworld.get_region(key, self.player) + currentRegion.exits.append(Entrance(self.player, key + "->"+ locationName, currentRegion)) + + self.multiworld.regions += locationRegions + + #create entrances + regionConcat = regions + locationRegions + for region in regionConcat: + for exit in region.exits: + exit.connect(self.multiworld.get_region(exit.name[exit.name.find("->") + 2:], self.player)) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions: src_region = self.multiworld.get_region(src.Name, self.player) @@ -210,26 +293,125 @@ def generate_basic(self): src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region)) srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player) srcDestEntrance.connect(dest_region) - add_entrance_rule(self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse) + self.add_entrance_rule(self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse) - def set_rules(self): - set_rules(self.multiworld, self.player) + self.multiworld.regions += [ + self.create_region(self.multiworld, self.player, 'Menu', None, ['StartAP']) + ] - def create_regions(self): - create_locations(self, self.player) - create_regions(self, self.multiworld, self.player) + startAP = self.multiworld.get_entrance('StartAP', self.player) + startAP.connect(self.multiworld.get_region(self.variaRando.args.startLocation, self.player)) + + def collect(self, state: CollectionState, item: Item) -> bool: + state.smbm[self.player].addItem(item.type) + if item.location != None and item.location.game == self.game: + for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: + if (entrance.parent_region.can_reach(state)): + state.smbm[item.location.player].lastAP = entrance.parent_region.name + break + return super(SMWorld, self).collect(state, item) + def remove(self, state: CollectionState, item: Item) -> bool: + state.smbm[self.player].removeItem(item.type) + return super(SMWorld, self).remove(state, item) + + def create_item(self, name: str) -> Item: + item = next(x for x in ItemManager.Items.values() if x.Name == name) + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], + player=self.player) + + def get_filler_item_name(self) -> str: + if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: + power_bombs = self.multiworld.power_bomb_qty[self.player].value + missiles = self.multiworld.missile_qty[self.player].value + super_missiles = self.multiworld.super_qty[self.player].value + roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) + if roll <= power_bombs: + return "Power Bomb" + elif roll <= power_bombs + missiles: + return "Missile" + else: + return "Super Missile" + else: + return "Nothing" + def pre_fill(self): - from Fill import fill_restrictive - if len(self.prefilled_locked_items) > 0: - locations = [loc for loc in self.locations.values() if loc.item is None] - self.multiworld.random.shuffle(locations) - all_state = self.multiworld.get_all_state(False) - for item in self.ammoItems: - while (all_state.has(item.name, self.player, 1)): - all_state.remove(item) + if len(self.NothingPool) > 0: + nonChozoLoc = [] + chozoLoc = [] + + for loc in self.locations.values(): + if loc.item is None: + if locationsDict[loc.name].isChozo(): + chozoLoc.append(loc) + else: + nonChozoLoc.append(loc) - fill_restrictive(self.multiworld, all_state, locations, self.prefilled_locked_items, True, True) + self.multiworld.random.shuffle(nonChozoLoc) + self.multiworld.random.shuffle(chozoLoc) + missingCount = len(self.NothingPool) - len(nonChozoLoc) + locations = nonChozoLoc + if (missingCount > 0): + locations += chozoLoc[:missingCount] + locations = locations[:len(self.NothingPool)] + for item, loc in zip(self.NothingPool, locations): + loc.place_locked_item(item) + loc.address = loc.item.code = None + + def post_fill(self): + self.itemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) + ] + self.progItemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement + ] + for itemLoc in self.itemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + for itemLoc in self.progItemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + + localItemLocs = [il for il in self.itemLocs if il.player == self.player] + localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] + + escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None + escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) + assert escapeOk, "Could not find a solution for escape" + + self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, + self.variaRando.args.area, self.variaRando.args.bosses, + self.variaRando.args.escapeRando) + + self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) + + self.need_comeback_check = False + + @classmethod + def stage_post_fill(cls, world): + new_state = CollectionState(world) + progitempool = [] + for item in world.itempool: + if item.game == "Super Metroid" and item.advancement: + progitempool.append(item) + + for item in progitempool: + new_state.collect(item, True) + + bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] + for player in world.get_game_players("Super Metroid"): + for bossLoc in bossesLoc: + if not world.get_location(bossLoc, player).can_reach(new_state): + world.state.smbm[player].onlyBossLeft = True + break def getWordArray(self, w: int) -> List[int]: """ little-endian convert a 16-bit number to an array of numbers <= 255 each """ @@ -669,115 +851,6 @@ def fill_slot_data(self): return slot_data - def collect(self, state: CollectionState, item: Item) -> bool: - state.smbm[self.player].addItem(item.type) - if item.location != None and item.location.game == self.game: - for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: - if (entrance.parent_region.can_reach(state)): - state.smbm[item.location.player].lastAP = entrance.parent_region.name - break - return super(SMWorld, self).collect(state, item) - - def remove(self, state: CollectionState, item: Item) -> bool: - state.smbm[self.player].removeItem(item.type) - return super(SMWorld, self).remove(state, item) - - def create_item(self, name: str) -> Item: - item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], - player=self.player) - - def get_filler_item_name(self) -> str: - if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: - power_bombs = self.multiworld.power_bomb_qty[self.player].value - missiles = self.multiworld.missile_qty[self.player].value - super_missiles = self.multiworld.super_qty[self.player].value - roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) - if roll <= power_bombs: - return "Power Bomb" - elif roll <= power_bombs + missiles: - return "Missile" - else: - return "Super Missile" - else: - return "Nothing" - - def pre_fill(self): - if len(self.NothingPool) > 0: - nonChozoLoc = [] - chozoLoc = [] - - for loc in self.locations.values(): - if loc.item is None: - if locationsDict[loc.name].isChozo(): - chozoLoc.append(loc) - else: - nonChozoLoc.append(loc) - - self.multiworld.random.shuffle(nonChozoLoc) - self.multiworld.random.shuffle(chozoLoc) - missingCount = len(self.NothingPool) - len(nonChozoLoc) - locations = nonChozoLoc - if (missingCount > 0): - locations += chozoLoc[:missingCount] - locations = locations[:len(self.NothingPool)] - for item, loc in zip(self.NothingPool, locations): - loc.place_locked_item(item) - loc.address = loc.item.code = None - - def post_fill(self): - self.itemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], itemLoc.item.player, True) - for itemLoc in self.multiworld.get_locations(self.player) - ] - self.progItemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], itemLoc.item.player, True) - for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement - ] - for itemLoc in self.itemLocs: - if itemLoc.Item.Class == "Boss": - itemLoc.Item.Class = "Minor" - for itemLoc in self.progItemLocs: - if itemLoc.Item.Class == "Boss": - itemLoc.Item.Class = "Minor" - - localItemLocs = [il for il in self.itemLocs if il.player == self.player] - localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] - - escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None - escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) - assert escapeOk, "Could not find a solution for escape" - - self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, - self.variaRando.args.area, self.variaRando.args.bosses, - self.variaRando.args.escapeRando) - - self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) - - @classmethod - def stage_post_fill(cls, world): - new_state = CollectionState(world) - progitempool = [] - for item in world.itempool: - if item.game == "Super Metroid" and item.advancement: - progitempool.append(item) - - for item in progitempool: - new_state.collect(item, True) - - bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] - for player in world.get_game_players("Super Metroid"): - for bossLoc in bossesLoc: - if not world.get_location(bossLoc, player).can_reach(new_state): - world.state.smbm[player].onlyBossLeft = True - break - def write_spoiler(self, spoiler_handle: TextIO): if self.multiworld.area_randomization[self.player].value != 0: spoiler_handle.write('\n\nArea Transitions:\n\n') @@ -793,39 +866,18 @@ def write_spoiler(self, spoiler_handle: TextIO): '<=>', dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss])) -def create_locations(self, player: int): - for name in locationsDict: - self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None)) - - -def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, player, world) - if locations: - for loc in locations: - location = self.locations[loc] - location.parent_region = ret - ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) - return ret - - class SMLocation(Location): game: str = "Super Metroid" def __init__(self, player: int, name: str, address=None, parent=None): super(SMLocation, self).__init__(player, name, address, parent) - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) - def can_reach(self, state: CollectionState) -> bool: # self.access_rule computes faster on average, so placing it first for faster abort assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and \ - self.parent_region.can_reach(state) and \ - self.can_comeback(state, self.item) + return super(SMLocation, self).can_reach(state) and \ + (not state.multiworld.worlds[self.player].need_comeback_check or \ + self.can_comeback(state, self.item)) def can_comeback(self, state: CollectionState, item): randoExec = state.multiworld.worlds[self.player].variaRando.randoExec From 62a265cc31cc1820f70f5c311d70928999f2a8d5 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:17:03 -0400 Subject: [PATCH 03/35] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20logic=20and=20locat?= =?UTF-8?q?ion=20name=20fixes=20(#1752)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects incorrect name listed for location in rules.py leading to logic rules failing to apply. Swaps location names for incorrectly-named trainersanity checks in Viridian Gym --- worlds/pokemon_rb/__init__.py | 2 +- worlds/pokemon_rb/locations.py | 4 ++-- worlds/pokemon_rb/rules.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index b223568ff07d..753d35c735cd 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -40,7 +40,7 @@ class PokemonRedBlueWorld(World): game = "Pokemon Red and Blue" option_definitions = pokemon_rb_options - data_version = 7 + data_version = 8 required_client_version = (0, 3, 9) topology_present = False diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index a1b64e12e531..f942662c8e08 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -708,8 +708,8 @@ def __init__(self, flag): LocationData("Viridian Gym", "Cooltrainer M 2", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_0_ITEM"], EventFlag(446), inclusion=trainersanity), LocationData("Viridian Gym", "Blackbelt 2", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_1_ITEM"], EventFlag(445), inclusion=trainersanity), LocationData("Viridian Gym", "Tamer 2", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_2_ITEM"], EventFlag(440), inclusion=trainersanity), - LocationData("Viridian Gym", "Cooltrainer M 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_5_ITEM"], EventFlag(437), inclusion=trainersanity), - LocationData("Viridian Gym", "Blackbelt 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_4_ITEM"], EventFlag(438), inclusion=trainersanity), + LocationData("Viridian Gym", "Blackbelt 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_5_ITEM"], EventFlag(437), inclusion=trainersanity), + LocationData("Viridian Gym", "Cooltrainer M 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_4_ITEM"], EventFlag(438), inclusion=trainersanity), LocationData("Victory Road 1F", "Cooltrainer F", None, rom_addresses["Trainersanity_EVENT_BEAT_VICTORY_ROAD_1_TRAINER_0_ITEM"], EventFlag(15), inclusion=trainersanity), LocationData("Victory Road 1F", "Cooltrainer M", None, rom_addresses["Trainersanity_EVENT_BEAT_VICTORY_ROAD_1_TRAINER_1_ITEM"], EventFlag(14), inclusion=trainersanity), LocationData("Victory Road 2F", "Blackbelt", None, rom_addresses["Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_0_ITEM"], EventFlag(162), inclusion=trainersanity), diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index e9e64e218e99..33fe248a8c93 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -146,7 +146,7 @@ def prize_rule(i): "Silph Co 11F - Rocket 2 (Card Key)": lambda state: state.has("Card Key", player), "Silph Co 9F - Rocket 2 (Card Key)": lambda state: state.has("Card Key", player), "Silph Co 3F - Scientist (Card Key)": lambda state: state.has("Card Key", player), - "Route 10 North - Pokemaniac": lambda state: state.pokemon_rb_can_surf(player), + "Route 10 - Pokemaniac": lambda state: state.pokemon_rb_can_surf(player), "Rocket Hideout B1F - Rocket 5 (Lift Key)": lambda state: state.has("Lift Key", player), "Rocket Hideout B4F - Rocket 2 (Lift Key)": lambda state: state.has("Lift Key", player), "Rocket Hideout B4F - Rocket 3 (Lift Key)": lambda state: state.has("Lift Key", player), From 06a25a903e803933af4bc23112185e6ce524b25b Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:20:43 -0400 Subject: [PATCH 04/35] KH2: New Unit Test and better keyblade fill (#1744) __init__: - Added exception for if the player has too many excluded abilities on keyblades. - Fixed Action Abilities only on keyblades from breaking. - Added proper support for ability quantity's instead of 1 of the ability - Moved filling the localitems slot data to init instead of generate_output so I could easily unit test it TestSlotData: - Checks if the "localItems" part of slot data is filled. This is used for keeping track of local items and making sure nothing dupes --- worlds/kh2/OpenKH.py | 8 ++------ worlds/kh2/__init__.py | 35 +++++++++++++++++++++++++-------- worlds/kh2/test/TestSlotData.py | 21 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 worlds/kh2/test/TestSlotData.py diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py index eb1a846e4216..c3334dbb9949 100644 --- a/worlds/kh2/OpenKH.py +++ b/worlds/kh2/OpenKH.py @@ -6,8 +6,7 @@ import zipfile from .Items import item_dictionary_table, CheckDupingItems -from .Locations import all_locations, SoraLevels, exclusion_table, AllWeaponSlot -from .Names import LocationName +from .Locations import all_locations, SoraLevels, exclusion_table from .XPValues import lvlStats, formExp, soraExp from worlds.Files import APContainer @@ -83,7 +82,7 @@ def increaseStat(i): elif self.multiworld.LevelDepth[self.player] == "level_99": levelsetting.extend(exclusion_table["Level99"]) - elif self.multiworld.LevelDepth[self.player] in ["level_50_sanity", "level_99_sanity"]: + elif self.multiworld.LevelDepth[self.player] != "level_1": levelsetting.extend(exclusion_table["Level50Sanity"]) if self.multiworld.LevelDepth[self.player] == "level_99_sanity": @@ -96,9 +95,6 @@ def increaseStat(i): data = all_locations[location.name] if location.item.player == self.player: itemcode = item_dictionary_table[location.item.name].kh2id - if location.item.name in slotDataDuping and \ - location.name not in AllWeaponSlot: - self.LocalItems[location.address] = item_dictionary_table[location.item.name].code else: itemcode = 90 # castle map diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 08ab9eabcee6..23075a2084df 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -2,7 +2,7 @@ import logging from .Items import * -from .Locations import all_locations, setup_locations, exclusion_table +from .Locations import all_locations, setup_locations, exclusion_table, AllWeaponSlot from .Names import ItemName, LocationName from .OpenKH import patch_kh2 from .Options import KH2_Options @@ -62,8 +62,22 @@ def __init__(self, multiworld: "MultiWorld", player: int): self.growth_list = list() for x in range(4): self.growth_list.extend(Movement_Table.keys()) + self.slotDataDuping = set() + self.localItems = dict() def fill_slot_data(self) -> dict: + for values in CheckDupingItems.values(): + if isinstance(values, set): + self.slotDataDuping = self.slotDataDuping.union(values) + else: + for inner_values in values.values(): + self.slotDataDuping = self.slotDataDuping.union(inner_values) + self.LocalItems = {location.address: item_dictionary_table[location.item.name].code + for location in self.multiworld.get_filled_locations(self.player) + if location.item.player == self.player + and location.item.name in self.slotDataDuping + and location.name not in AllWeaponSlot} + return {"hitlist": self.hitlist, "LocalItems": self.LocalItems, "Goal": self.multiworld.Goal[self.player].value, @@ -132,7 +146,7 @@ def create_items(self) -> None: # Creating filler for unfilled locations itempool += [self.create_filler() - for _ in range(self.totalLocations-len(itempool))] + for _ in range(self.totalLocations - len(itempool))] self.multiworld.itempool += itempool def generate_early(self) -> None: @@ -245,13 +259,15 @@ def keyblade_fill(self): ItemName.FinishingPlus: 1}} elif self.multiworld.KeybladeAbilities[self.player] == "action": - self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table} + self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if + item in ActionAbility_Table} # there are too little action abilities so 2 random support abilities are placed for _ in range(3): - randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys())) + randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( + list(SupportAbility_Table.keys())) while randomSupportAbility in self.sora_keyblade_ability_pool: randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( - list(SupportAbility_Table.keys())) + list(SupportAbility_Table.keys())) self.sora_keyblade_ability_pool[randomSupportAbility] = 1 else: # both action and support on keyblades. @@ -259,7 +275,8 @@ def keyblade_fill(self): self.sora_keyblade_ability_pool = { **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, **{item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table}, - **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, ItemName.FinishingPlus: 1}} + **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, + ItemName.FinishingPlus: 1}} for ability in self.multiworld.BlacklistKeyblade[self.player].value: if ability in self.sora_keyblade_ability_pool: @@ -267,7 +284,8 @@ def keyblade_fill(self): # magic number for amount of keyblades if sum(self.sora_keyblade_ability_pool.values()) < 28: - raise Exception(f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") + raise Exception( + f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") self.valid_abilities = list(self.sora_keyblade_ability_pool.keys()) # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key @@ -379,4 +397,5 @@ def level_subtraction(self): self.totalLocations -= 76 def get_filler_item_name(self) -> str: - return self.multiworld.random.choice([ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) + return self.multiworld.random.choice( + [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) diff --git a/worlds/kh2/test/TestSlotData.py b/worlds/kh2/test/TestSlotData.py new file mode 100644 index 000000000000..656cd48d5a6f --- /dev/null +++ b/worlds/kh2/test/TestSlotData.py @@ -0,0 +1,21 @@ +import unittest + +from test.general import setup_solo_multiworld +from . import KH2TestBase +from .. import KH2World, all_locations, item_dictionary_table, CheckDupingItems, AllWeaponSlot, KH2Item +from ..Names import ItemName +from ... import AutoWorldRegister +from ...AutoWorld import call_all + + +class TestLocalItems(KH2TestBase): + + def testSlotData(self): + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + multiworld = setup_solo_multiworld(KH2World, gen_steps) + for location in multiworld.get_locations(): + if location.item is None: + location.place_locked_item(multiworld.worlds[1].create_item(ItemName.NoExperience)) + call_all(multiworld, "fill_slot_data") + slotdata = multiworld.worlds[1].fill_slot_data() + assert len(slotdata["LocalItems"]) > 0, f"{slotdata['LocalItems']} is empty" From 58aea7ca5841fc0d61943d48f359a428a7360929 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 23 Apr 2023 22:21:28 +0200 Subject: [PATCH 05/35] Multiserver: cleaner exit (#1743) --- MultiServer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 59c2975e125c..3d5053bbe580 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1865,7 +1865,7 @@ def _cmd_status(self, tag: str = "") -> bool: def _cmd_exit(self) -> bool: """Shutdown the server""" - async_start(self.ctx.server.ws_server._close()) + self.ctx.server.ws_server.close() if self.ctx.shutdown_task: self.ctx.shutdown_task.cancel() self.ctx.exit_event.set() @@ -2206,7 +2206,7 @@ async def auto_shutdown(ctx, to_cancel=None): await asyncio.sleep(ctx.auto_shutdown) while not ctx.exit_event.is_set(): if not ctx.client_activity_timers.values(): - async_start(ctx.server.ws_server._close()) + ctx.server.ws_server.close() ctx.exit_event.set() if to_cancel: for task in to_cancel: @@ -2217,7 +2217,7 @@ async def auto_shutdown(ctx, to_cancel=None): delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity seconds = ctx.auto_shutdown - delta.total_seconds() if seconds < 0: - async_start(ctx.server.ws_server._close()) + ctx.server.ws_server.close() ctx.exit_event.set() if to_cancel: for task in to_cancel: From b950af09a6368a9923ab0caf77b96ea4e6deac84 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 Apr 2023 01:58:26 +0200 Subject: [PATCH 06/35] Factorio: remove tech_tree_layout_prerequisites from core --- BaseClasses.py | 1 - worlds/factorio/Mod.py | 2 +- worlds/factorio/Shapes.py | 2 +- worlds/factorio/__init__.py | 2 ++ 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 35761bc2387c..68407ee08388 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -135,7 +135,6 @@ def __init__(self, players: int): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val - set_player_attr('tech_tree_layout_prerequisites', {}) set_player_attr('_region_cache', {}) set_player_attr('shuffle', "vanilla") set_player_attr('logic', "noglitches") diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 4f1f3fd9d0bd..270e7dacf087 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -120,7 +120,7 @@ def flop_random(low, high, base=None): "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), "custom_technologies": multiworld.worlds[player].custom_technologies, - "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], + "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_player": player, "starting_items": multiworld.starting_items[player], "recipes": recipes, diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 84bcb06cab1a..d40871f7fa82 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -247,5 +247,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se else: raise NotImplementedError(f"Layout {layout} is not implemented.") - world.tech_tree_layout_prerequisites[player] = prerequisites + factorio_world.tech_tree_layout_prerequisites = prerequisites return prerequisites diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 269ec4556652..10dda905e1fd 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -69,6 +69,7 @@ class Factorio(World): required_client_version = (0, 4, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() + tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] tech_mix: int = 0 skip_silo: bool = False science_locations: typing.List[FactorioScienceLocation] @@ -78,6 +79,7 @@ def __init__(self, world, player: int): self.advancement_technologies = set() self.custom_recipes = {} self.science_locations = [] + self.tech_tree_layout_prerequisites = {} generate_output = generate_mod From dcc628f878c0074a2d7780153179c28513b8c7c1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 23 Apr 2023 22:12:55 +0200 Subject: [PATCH 07/35] Core: correct typing info for item_in_locations Core: rename item_in_locations to item_name_in_location_names Core: add actual item_name_in_locations --- worlds/alttp/Rules.py | 12 ++++++------ worlds/generic/Rules.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index e6c5f15a2f4b..09c63aca01d2 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -4,7 +4,7 @@ from BaseClasses import Entrance, MultiWorld from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, - item_in_locations, location_item_name, set_rule, allow_self_locking_items) + item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) from . import OverworldGlitchRules from .Bosses import GanonDefeatRule @@ -305,7 +305,7 @@ def global_rules(world, player): set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( - item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) + item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ... @@ -381,17 +381,17 @@ def global_rules(world, player): #The actual requirements for these rooms to avoid key-lock set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or (( - item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2))) + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2))) for location in randomizer_room_chests: set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) # Once again it is possible to need more than 3 keys... set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player)) # Actual requirements for location in compass_room_chests: set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))) + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))) set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) @@ -919,7 +919,7 @@ def set_trock_key_rules(world, player): else: # Middle to front requires 2 keys if the back is locked, otherwise 4 set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2) - if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations) + if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations) else state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) # Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index fb783edb6722..520ad2252568 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -140,14 +140,22 @@ def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str location.item_rule = lambda item: rule(item) or old_rule(item) -def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, - locations: typing.Sequence["BaseClasses.Location"]) -> bool: - for location in locations: +def item_name_in_location_names(state: "BaseClasses.CollectionState", item: str, player: int, + location_name_player_pairs: typing.Sequence[typing.Tuple[str, int]]) -> bool: + for location in location_name_player_pairs: if location_item_name(state, location[0], location[1]) == (item, player): return True return False +def item_name_in_locations(item: str, player: int, + locations: typing.Sequence["BaseClasses.Location"]) -> bool: + for location in locations: + if location.item and location.item.name == item and location.item.player == player: + return True + return False + + def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ typing.Optional[typing.Tuple[str, int]]: location = state.multiworld.get_location(location, player) From c0cf35edda630777fbba9e12d4a723c3fc22d5d1 Mon Sep 17 00:00:00 2001 From: Bicoloursnake <60069210+Bicoloursnake@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:53:33 -0400 Subject: [PATCH 08/35] StarCraft 2 macOS documentation (#1747) * Adding SC2 macOS instructions A few hours ago, I tested whether the client would run successfully on macOS (Send/Receive items, load maps, download maps, etc.). After the successful testing, I thought adding some documentation would be nice for those who want to play Archipelago on a macOS system. * Don't need sudo Turns out you don't need sudo to do the download_data, oops. * Removed Extraneous Parantheses --- worlds/sc2wol/docs/setup_en.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 267c8430aa6a..13c7cb91e3aa 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -16,6 +16,7 @@ to obtain a config file for StarCraft 2. - Linux users should also follow the instructions found at the bottom of this page (["Running in Linux"](#running-in-linux)). 2. Run ArchipelagoStarcraft2Client.exe. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. 3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above. ## Where do I get a config file (aka "YAML") for this game? @@ -34,6 +35,7 @@ Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en ## How do I join a MultiWorld game? 1. Run ArchipelagoStarcraft2Client.exe. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. 2. Type `/connect [server ip]`. 3. Type your slot name and the server's password when prompted. 4. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see every mission. By default, @@ -45,6 +47,10 @@ First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to your message. +## Running in macOS + +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. This is done to make sure that `/download_data` works correctly. + ## Running in Linux To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build From 173513c9f4c957240eb30d1686ba5be014dfdf4d Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Tue, 25 Apr 2023 03:06:58 -0400 Subject: [PATCH 09/35] DLCQuest: Generation bug fix (#1757) * Fix documentation error * init_creation somehow this fix bug * item_shuffle_fix also a count as been corrected * Fix_early_generation * Update __init__.py * Update __init__.py fix version specific bug * fix rule set for final boss and did some reformation (thanks kaito) * Update Rules.py the sword trio can now be in itself if before or actually themself * Core: correct typing info for item_in_locations Core: rename item_in_locations to item_name_in_location_names Core: add actual item_name_in_locations * item_shuffle_fix also a count as been corrected * Fix_early_generation * fix rule set for final boss and did some reformation (thanks kaito) * Update Rules.py the sword trio can now be in itself if before or actually themself * Fix the missing [] and switch to the good function * - Cleanup and Black Sliver's suggestions --------- Co-authored-by: Fabian Dill Co-authored-by: Alex Gilbert --- worlds/dlcquest/Rules.py | 727 ++++++++++++++++++++++----------------- 1 file changed, 403 insertions(+), 324 deletions(-) diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index c57976e79ec5..b571bdd3eb5b 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -1,7 +1,7 @@ import math import re from .Locations import DLCQuestLocation -from ..generic.Rules import add_rule, set_rule +from ..generic.Rules import add_rule, set_rule, item_name_in_locations from .Items import DLCQuestItem from BaseClasses import ItemClassification from . import Options @@ -22,7 +22,6 @@ def has_coin(state, player: int, coins: int): return coin_possessed >= coins - return lambda state: has_coin(state, player, coin) def has_enough_coin_freemium(player: int, coin: int): @@ -37,334 +36,414 @@ def has_coin(state, player: int, coins: int): return lambda state: has_coin(state, player, coin) - if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ - Options.Campaign] == Options.Campaign.option_both: - set_rule(world.get_entrance("Moving", player), - lambda state: state.has("Movement Pack", player)) - set_rule(world.get_entrance("Cloud", player), - lambda state: state.has("Psychological Warfare Pack", player)) - set_rule(world.get_entrance("Forest Entrance", player), - lambda state: state.has("Map Pack", player)) - set_rule(world.get_entrance("Forest True Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: - set_rule(world.get_entrance("Behind Ogre", player), - lambda state: state.has("Gun Pack", player)) - - if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: - set_rule(world.get_entrance("Tree", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_entrance("Cave Tree", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("Shepherd Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("North West Ceiling Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("North West Alcove Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("West Cave Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: - set_rule(world.get_entrance("Behind Ogre", player), - lambda state: state.has("Gun", player)) - set_rule(world.get_entrance("Tree", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_entrance("Cave Tree", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_entrance("True Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_location("Shepherd Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_location("North West Ceiling Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_location("North West Alcove Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_location("West Cave Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - - if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: - set_rule(world.get_location("Sword", player), - lambda state: state.has("Time is Money Pack", player)) - - if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_none: - set_rule(world.get_entrance("Cloud Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_entrance("Forest Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - - if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_none or World_Options[ - Options.FalseDoubleJump] == Options.FalseDoubleJump.option_simple: - set_rule(world.get_entrance("Behind Tree Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_entrance("Cave Roof", player), - lambda state: state.has("Double Jump Pack", player)) - - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) - for i in range(number_of_bundle): - - item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" - set_rule(world.get_location(item_coin, player), - has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) - if 825 % World_Options[Options.CoinSanityRange] != 0: - set_rule(world.get_location("DLC Quest: 825 Coin", player), - has_enough_coin(player, 825)) - - set_rule(world.get_location("Movement Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(4 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Animation Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Audio Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Pause Menu Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Time is Money Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(20 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Double Jump Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(100 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Pet Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Sexy Outfits Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Top Hat Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Map Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(140 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Gun Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(75 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("The Zombie Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Night Map Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(75 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Psychological Warfare Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(50 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Armor for your Horse Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(250 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Finish the Fight Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_none: - set_rule(world.get_location("Movement Pack", player), - has_enough_coin(player, 4)) - set_rule(world.get_location("Animation Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Audio Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Pause Menu Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Time is Money Pack", player), - has_enough_coin(player, 20)) - set_rule(world.get_location("Double Jump Pack", player), - has_enough_coin(player, 100)) - set_rule(world.get_location("Pet Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Sexy Outfits Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Top Hat Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Map Pack", player), - has_enough_coin(player, 140)) - set_rule(world.get_location("Gun Pack", player), - has_enough_coin(player, 75)) - set_rule(world.get_location("The Zombie Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Night Map Pack", player), - has_enough_coin(player, 75)) - set_rule(world.get_location("Psychological Warfare Pack", player), - has_enough_coin(player, 50)) - set_rule(world.get_location("Armor for your Horse Pack", player), - has_enough_coin(player, 250)) - set_rule(world.get_location("Finish the Fight Pack", player), - has_enough_coin(player, 5)) - - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: - set_rule(world.get_location("Winning Basic", player), - lambda state: state.has("Finish the Fight Pack", player)) - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true: - set_rule(world.get_location("Winning Basic", player), - lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack", - player)) - - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ - Options.Campaign] == Options.Campaign.option_both: - set_rule(world.get_entrance("Wall Jump Entrance", player), - lambda state: state.has("Wall Jump Pack", player)) - set_rule(world.get_entrance("Harmless Plants", player), - lambda state: state.has("Harmless Plants Pack", player)) - set_rule(world.get_entrance("Name Change Entrance", player), - lambda state: state.has("Name Change Pack", player)) - set_rule(world.get_entrance("Cut Content Entrance", player), - lambda state: state.has("Cut Content Pack", player)) - set_rule(world.get_entrance("Blizzard", player), - lambda state: state.has("Season Pass", player)) - set_rule(world.get_entrance("Boss Door", player), - lambda state: state.has("Big Sword Pack", player) and state.has("Really Big Sword Pack", - player) and state.has( - "Unfathomable Sword Pack", player)) - set_rule(world.get_location("I Get That Reference!", player), - lambda state: state.has("Death of Comedy Pack", player)) - set_rule(world.get_location("Story is Important", player), - lambda state: state.has("DLC NPC Pack", player)) - set_rule(world.get_entrance("Pickaxe Hard Cave", player), - lambda state: state.has("Pickaxe", player)) - - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: - set_rule(world.get_entrance("Vines", player), - lambda state: state.has("Incredibly Important Pack", player)) - set_rule(world.get_entrance("Behind Rocks", player), - lambda state: state.can_reach("Cut Content", 'region', player)) - set_rule(world.get_entrance("Pickaxe Hard Cave", player), - lambda state: state.can_reach("Cut Content", 'region', player) and state.has("Name Change Pack", - player)) - - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: - set_rule(world.get_entrance("Vines", player), - lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player)) - set_rule(world.get_entrance("Behind Rocks", player), - lambda state: state.has("Pickaxe", player)) - - set_rule(world.get_location("Wooden Sword", player), - lambda state: state.has("Incredibly Important Pack", player)) - set_rule(world.get_location("Pickaxe", player), - lambda state: state.has("Humble Indie Bindle", player)) - set_rule(world.get_location("Humble Indie Bindle", player), - lambda state: state.has("Box of Various Supplies", player) and state.can_reach("Cut Content", - 'region', player)) - set_rule(world.get_location("Box of Various Supplies", player), - lambda state: state.can_reach("Cut Content", 'region', player)) - - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange]) - for i in range(number_of_bundle): - - item_coin_freemium = "Live Freemium or Die: number Coin" - item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), - item_coin_freemium) - set_rule(world.get_location(item_coin_loc_freemium, player), - has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) - if 889 % World_Options[Options.CoinSanityRange] != 0: - set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), - has_enough_coin_freemium(player, 889)) - - set_rule(world.get_entrance("Boss Door", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(889 / World_Options[Options.CoinSanityRange]))) - - set_rule(world.get_location("Particles Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Day One Patch Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Checkpoint Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Incredibly Important Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Wall Jump Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(35 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Health Bar Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Parallax Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Harmless Plants Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(130 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Death of Comedy Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Canadian Dialog Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(10 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("DLC NPC Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Cut Content Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(40 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Name Change Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(150 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Season Pass", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(199 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("High Definition Next Gen Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(20 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Increased HP Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(10 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Remove Ads Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(25 / World_Options[Options.CoinSanityRange]))) - - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_none: - set_rule(world.get_entrance("Boss Door", player), - has_enough_coin_freemium(player, 889)) + set_basic_rules(World_Options, has_enough_coin, player, world) + set_lfod_rules(World_Options, has_enough_coin_freemium, player, world) + set_completion_condition(World_Options, player, world) - set_rule(world.get_location("Particles Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Day One Patch Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Checkpoint Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Incredibly Important Pack", player), - has_enough_coin_freemium(player, 15)) - set_rule(world.get_location("Wall Jump Pack", player), - has_enough_coin_freemium(player, 35)) - set_rule(world.get_location("Health Bar Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Parallax Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Harmless Plants Pack", player), - has_enough_coin_freemium(player, 130)) - set_rule(world.get_location("Death of Comedy Pack", player), - has_enough_coin_freemium(player, 15)) - set_rule(world.get_location("Canadian Dialog Pack", player), - has_enough_coin_freemium(player, 10)) - set_rule(world.get_location("DLC NPC Pack", player), - has_enough_coin_freemium(player, 15)) - set_rule(world.get_location("Cut Content Pack", player), - has_enough_coin_freemium(player, 40)) - set_rule(world.get_location("Name Change Pack", player), - has_enough_coin_freemium(player, 150)) - set_rule(world.get_location("Season Pass", player), - has_enough_coin_freemium(player, 199)) - set_rule(world.get_location("High Definition Next Gen Pack", player), - has_enough_coin_freemium(player, 20)) - set_rule(world.get_location("Increased HP Pack", player), - has_enough_coin_freemium(player, 10)) - set_rule(world.get_location("Remove Ads Pack", player), - has_enough_coin_freemium(player, 25)) +def set_basic_rules(World_Options, has_enough_coin, player, world): + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: + return + set_basic_entrance_rules(player, world) + set_basic_self_obtained_items_rules(World_Options, player, world) + set_basic_shuffled_items_rules(World_Options, player, world) + set_double_jump_glitchless_rules(World_Options, player, world) + set_easy_double_jump_glitch_rules(World_Options, player, world) + self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world) + set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world) + self_basic_win_condition(World_Options, player, world) + + +def set_basic_entrance_rules(player, world): + set_rule(world.get_entrance("Moving", player), + lambda state: state.has("Movement Pack", player)) + set_rule(world.get_entrance("Cloud", player), + lambda state: state.has("Psychological Warfare Pack", player)) + set_rule(world.get_entrance("Forest Entrance", player), + lambda state: state.has("Map Pack", player)) + set_rule(world.get_entrance("Forest True Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + + +def set_basic_self_obtained_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled: + return + set_rule(world.get_entrance("Behind Ogre", player), + lambda state: state.has("Gun Pack", player)) + + if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + set_rule(world.get_entrance("Tree", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_entrance("Cave Tree", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("Shepherd Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("North West Ceiling Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("North West Alcove Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("West Cave Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + + +def set_basic_shuffled_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled: + return + set_rule(world.get_entrance("Behind Ogre", player), + lambda state: state.has("Gun", player)) + set_rule(world.get_entrance("Tree", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_entrance("Cave Tree", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_entrance("True Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_location("Shepherd Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("North West Ceiling Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("North West Alcove Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("West Cave Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + + if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + set_rule(world.get_location("Sword", player), + lambda state: state.has("Time is Money Pack", player)) + + +def set_double_jump_glitchless_rules(World_Options, player, world): + if World_Options[Options.FalseDoubleJump] != Options.FalseDoubleJump.option_none: + return + set_rule(world.get_entrance("Cloud Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_entrance("Forest Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + + +def set_easy_double_jump_glitch_rules(World_Options, player, world): + if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_all: + return + set_rule(world.get_entrance("Behind Tree Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_entrance("Cave Roof", player), + lambda state: state.has("Double Jump Pack", player)) + + +def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin: + return + number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) + for i in range(number_of_bundle): + + item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + set_rule(world.get_location(item_coin, player), + has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) + if 825 % World_Options[Options.CoinSanityRange] != 0: + set_rule(world.get_location("DLC Quest: 825 Coin", player), + has_enough_coin(player, 825)) + + set_rule(world.get_location("Movement Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(4 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Animation Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Audio Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Pause Menu Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Time is Money Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(20 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Double Jump Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(100 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Pet Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Sexy Outfits Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Top Hat Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Map Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(140 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Gun Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(75 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("The Zombie Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Night Map Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(75 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Psychological Warfare Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(50 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Armor for your Horse Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(250 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Finish the Fight Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + + +def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none: + return + set_rule(world.get_location("Movement Pack", player), + has_enough_coin(player, 4)) + set_rule(world.get_location("Animation Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Audio Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Pause Menu Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Time is Money Pack", player), + has_enough_coin(player, 20)) + set_rule(world.get_location("Double Jump Pack", player), + has_enough_coin(player, 100)) + set_rule(world.get_location("Pet Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Sexy Outfits Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Top Hat Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Map Pack", player), + has_enough_coin(player, 140)) + set_rule(world.get_location("Gun Pack", player), + has_enough_coin(player, 75)) + set_rule(world.get_location("The Zombie Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Night Map Pack", player), + has_enough_coin(player, 75)) + set_rule(world.get_location("Psychological Warfare Pack", player), + has_enough_coin(player, 50)) + set_rule(world.get_location("Armor for your Horse Pack", player), + has_enough_coin(player, 250)) + set_rule(world.get_location("Finish the Fight Pack", player), + has_enough_coin(player, 5)) + + +def self_basic_win_condition(World_Options, player, world): + if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: + set_rule(world.get_location("Winning Basic", player), + lambda state: state.has("Finish the Fight Pack", player)) + if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true: + set_rule(world.get_location("Winning Basic", player), + lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack", + player)) + + +def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world): if World_Options[Options.Campaign] == Options.Campaign.option_basic: - world.completion_condition[player] = lambda state: state.has("Victory Basic", player) + return + set_lfod_entrance_rules(player, world) + set_boss_door_requirements_rules(player, world) + set_lfod_self_obtained_items_rules(World_Options, player, world) + set_lfod_shuffled_items_rules(World_Options, player, world) + self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) + set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) + + +def set_lfod_entrance_rules(player, world): + set_rule(world.get_entrance("Wall Jump Entrance", player), + lambda state: state.has("Wall Jump Pack", player)) + set_rule(world.get_entrance("Harmless Plants", player), + lambda state: state.has("Harmless Plants Pack", player)) + set_rule(world.get_entrance("Name Change Entrance", player), + lambda state: state.has("Name Change Pack", player)) + set_rule(world.get_entrance("Cut Content Entrance", player), + lambda state: state.has("Cut Content Pack", player)) + set_rule(world.get_entrance("Blizzard", player), + lambda state: state.has("Season Pass", player)) + set_rule(world.get_location("I Get That Reference!", player), + lambda state: state.has("Death of Comedy Pack", player)) + set_rule(world.get_location("Story is Important", player), + lambda state: state.has("DLC NPC Pack", player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.has("Pickaxe", player)) + + +def set_boss_door_requirements_rules(player, world): + sword_1 = "Big Sword Pack" + sword_2 = "Really Big Sword Pack" + sword_3 = "Unfathomable Sword Pack" + + big_sword_location = world.get_location(sword_1, player) + really_big_sword_location = world.get_location(sword_2, player) + unfathomable_sword_location = world.get_location(sword_3, player) + + big_sword_valid_locations = [big_sword_location] + really_big_sword_valid_locations = [big_sword_location, really_big_sword_location] + unfathomable_sword_valid_locations = [big_sword_location, really_big_sword_location, unfathomable_sword_location] + + big_sword_during_boss_fight = item_name_in_locations(sword_1, player, big_sword_valid_locations) + really_big_sword_during_boss_fight = item_name_in_locations(sword_2, player, really_big_sword_valid_locations) + unfathomable_sword_during_boss_fight = item_name_in_locations(sword_3, player, unfathomable_sword_valid_locations) + + # For each sword, either already have received it, or be guaranteed to get it during the fight at a valid stage. + # Otherwise, a player can get soft locked. + has_3_swords = lambda state: ((state.has(sword_1, player) or big_sword_during_boss_fight) and + (state.has(sword_2, player) or really_big_sword_during_boss_fight) and + (state.has(sword_3, player) or unfathomable_sword_during_boss_fight)) + set_rule(world.get_entrance("Boss Door", player), has_3_swords) + + +def set_lfod_self_obtained_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled: + return + set_rule(world.get_entrance("Vines", player), + lambda state: state.has("Incredibly Important Pack", player)) + set_rule(world.get_entrance("Behind Rocks", player), + lambda state: state.can_reach("Cut Content", 'region', player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.can_reach("Cut Content", 'region', player) and + state.has("Name Change Pack", player)) + + +def set_lfod_shuffled_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled: + return + set_rule(world.get_entrance("Vines", player), + lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player)) + set_rule(world.get_entrance("Behind Rocks", player), + lambda state: state.has("Pickaxe", player)) + + set_rule(world.get_location("Wooden Sword", player), + lambda state: state.has("Incredibly Important Pack", player)) + set_rule(world.get_location("Pickaxe", player), + lambda state: state.has("Humble Indie Bindle", player)) + set_rule(world.get_location("Humble Indie Bindle", player), + lambda state: state.has("Box of Various Supplies", player) and + state.can_reach("Cut Content", 'region', player)) + set_rule(world.get_location("Box of Various Supplies", player), + lambda state: state.can_reach("Cut Content", 'region', player)) + + +def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin: + return + number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange]) + for i in range(number_of_bundle): + + item_coin_freemium = "Live Freemium or Die: number Coin" + item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), + item_coin_freemium) + set_rule(world.get_location(item_coin_loc_freemium, player), + has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) + if 889 % World_Options[Options.CoinSanityRange] != 0: + set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), + has_enough_coin_freemium(player, 889)) + add_rule(world.get_entrance("Boss Door", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(889 / World_Options[Options.CoinSanityRange]))) + + set_rule(world.get_location("Particles Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Day One Patch Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Checkpoint Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Incredibly Important Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Wall Jump Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(35 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Health Bar Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Parallax Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Harmless Plants Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(130 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Death of Comedy Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Canadian Dialog Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(10 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("DLC NPC Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Cut Content Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(40 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Name Change Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(150 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Season Pass", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(199 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("High Definition Next Gen Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(20 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Increased HP Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(10 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Remove Ads Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(25 / World_Options[Options.CoinSanityRange]))) + + +def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none: + return + add_rule(world.get_entrance("Boss Door", player), + has_enough_coin_freemium(player, 889)) + + set_rule(world.get_location("Particles Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Day One Patch Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Checkpoint Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Incredibly Important Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Wall Jump Pack", player), + has_enough_coin_freemium(player, 35)) + set_rule(world.get_location("Health Bar Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Parallax Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Harmless Plants Pack", player), + has_enough_coin_freemium(player, 130)) + set_rule(world.get_location("Death of Comedy Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Canadian Dialog Pack", player), + has_enough_coin_freemium(player, 10)) + set_rule(world.get_location("DLC NPC Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Cut Content Pack", player), + has_enough_coin_freemium(player, 40)) + set_rule(world.get_location("Name Change Pack", player), + has_enough_coin_freemium(player, 150)) + set_rule(world.get_location("Season Pass", player), + has_enough_coin_freemium(player, 199)) + set_rule(world.get_location("High Definition Next Gen Pack", player), + has_enough_coin_freemium(player, 20)) + set_rule(world.get_location("Increased HP Pack", player), + has_enough_coin_freemium(player, 10)) + set_rule(world.get_location("Remove Ads Pack", player), + has_enough_coin_freemium(player, 25)) + + +def set_completion_condition(World_Options, player, world): + if World_Options[Options.Campaign] == Options.Campaign.option_basic: + world.completion_condition[player] = lambda state: state.has("Victory Basic", player) if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: world.completion_condition[player] = lambda state: state.has("Victory Freemium", player) - if World_Options[Options.Campaign] == Options.Campaign.option_both: world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has( "Victory Freemium", player) From 22ed7ff9c3efca8c8cbc9040d9ecf328cce300b7 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Tue, 25 Apr 2023 22:24:47 -0700 Subject: [PATCH 10/35] Zillion: fix empty 1st Sphere (#1770) There was a low probability that the Zillion 1st sphere could be empty. caused this test failure: https://github.com/ArchipelagoMW/Archipelago/actions/runs/4791795268/jobs/8522615992 --- worlds/zillion/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 241cb452a9d3..e52e7300725c 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -147,6 +147,16 @@ def create_regions(self) -> None: self.my_locations = [] self.zz_system.randomizer.place_canister_gun_reqs() + # low probability that place_canister_gun_reqs() results in empty 1st sphere + # testing code to force low probability event: + # for zz_room_name in ["r01c2", "r02c0", "r02c7", "r03c5"]: + # for zz_loc in self.zz_system.randomizer.regions[zz_room_name].locations: + # zz_loc.req.gun = 2 + if len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) == 0: + self.logger.info("Zillion avoided rare empty 1st sphere.") + for zz_loc in self.zz_system.randomizer.regions["r03c5"].locations: + zz_loc.req.gun = 1 + assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0 start = self.zz_system.randomizer.regions['start'] From bb56f7b400a2d734b3e75a9838212d0a8effadf4 Mon Sep 17 00:00:00 2001 From: TheBigSalarius <60804015+TheBigSalarius@users.noreply.github.com> Date: Wed, 26 Apr 2023 04:47:25 -0400 Subject: [PATCH 11/35] FF1: Added URange fix for Bizhawk 2.9 support URange wasn't moved to common.lua (and no longer exists in connector_ff1.lua) when the lua files were changed for Bizhawk 2.9 socket change. --- data/lua/common.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/lua/common.lua b/data/lua/common.lua index 4df2ab8470e3..93caf9bdb4b8 100644 --- a/data/lua/common.lua +++ b/data/lua/common.lua @@ -31,6 +31,7 @@ local untestedBizhawkMessage = "Warning: this version of bizhawk is newer than w u8 = memory.read_u8 wU8 = memory.write_u8 u16 = memory.read_u16_le +uRange = memory.readbyterange function getMaxMessageLength() local denominator = 12 @@ -99,4 +100,4 @@ function checkBizhawkVersion() print(untestedBizhawkMessage) end return true -end \ No newline at end of file +end From 4c3eaf2996aea6120676daaf9a2c2aa5ad7cad8b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 26 Apr 2023 10:48:08 +0200 Subject: [PATCH 12/35] LttP: fix that collect can bypass requirements for ganon ped goal (#1771) LttP: more pep8 --- Utils.py | 2 +- worlds/alttp/Bosses.py | 1 + worlds/alttp/Client.py | 57 ++++++++++++++------------- worlds/alttp/Dungeons.py | 10 +++-- worlds/alttp/EntranceShuffle.py | 6 ++- worlds/alttp/InvertedRegions.py | 5 ++- worlds/alttp/ItemPool.py | 15 +++---- worlds/alttp/Items.py | 1 + worlds/alttp/OverworldGlitchRules.py | 6 ++- worlds/alttp/Rom.py | 22 +++++------ worlds/alttp/Shops.py | 10 +++-- worlds/alttp/StateHelpers.py | 25 ++++++++++++ worlds/alttp/SubClasses.py | 1 + worlds/alttp/UnderworldGlitchRules.py | 7 ++-- worlds/alttp/__init__.py | 5 +-- 15 files changed, 107 insertions(+), 66 deletions(-) diff --git a/Utils.py b/Utils.py index 60b3904ff662..8a9478e49c3d 100644 --- a/Utils.py +++ b/Utils.py @@ -39,7 +39,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.4.0" +__version__ = "0.4.1" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 51615ddc452f..8c6dcabc87e6 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -6,6 +6,7 @@ from .Options import LTTPBosses as Bosses from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source + def BossFactory(boss: str, player: int) -> Optional[Boss]: if boss in boss_table: enemizer_name, defeat_rule = boss_table[boss] diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 71a0cf36001f..f81222e268d8 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -10,7 +10,7 @@ from NetUtils import ClientStatus, color from worlds.AutoSNIClient import SNIClient -from worlds.alttp import Shops, Regions +from . import Shops, Regions from .Rom import ROM_PLAYER_LIMIT snes_logger = logging.getLogger("SNES") @@ -270,17 +270,20 @@ 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), 'Ganons Tower - Validation Chest': (0x4d, 0x10)} -boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss', - 'Desert Palace - Boss', - 'Tower of Hera - Boss', - 'Palace of Darkness - Boss', - 'Swamp Palace - Boss', - 'Skull Woods - Boss', - "Thieves' Town - Boss", - 'Ice Palace - Boss', - 'Misery Mire - Boss', - 'Turtle Rock - Boss', - 'Sahasrahla'}} +collect_ignore_locations = {Regions.lookup_name_to_id[name] for name in { + 'Eastern Palace - Boss', + 'Desert Palace - Boss', + 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', + 'Swamp Palace - Boss', + 'Skull Woods - Boss', + "Thieves' Town - Boss", + 'Ice Palace - Boss', + 'Misery Mire - Boss', + 'Turtle Rock - Boss', + 'Sahasrahla', + 'Master Sword Pedestal', # can circumvent ganon pedestal's goal's pendant collection +}} location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()} @@ -322,8 +325,15 @@ location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()} +def should_collect(ctx, location_id: int) -> bool: + return ctx.allow_collect and location_id not in collect_ignore_locations and location_id in ctx.checked_locations \ + and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ + and ctx.locations_info[location_id].player != ctx.slot + + async def track_locations(ctx, roomid, roomdata) -> bool: from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + location_id: int new_locations = [] def new_check(location_id): @@ -340,11 +350,10 @@ def new_check(location_id): shop_data_changed = False shop_data = list(shop_data) for cnt, b in enumerate(shop_data): - location = Shops.SHOP_ID_START + cnt - if int(b) and location not in ctx.locations_checked: - new_check(location) - if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \ - and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot: + location_id = Shops.SHOP_ID_START + cnt + if int(b) and location_id not in ctx.locations_checked: + new_check(location_id) + if should_collect(ctx, location_id): if not int(b): shop_data[cnt] += 1 shop_data_changed = True @@ -371,9 +380,7 @@ def new_check(location_id): uw_unchecked[location_id] = (roomid, mask) uw_begin = min(uw_begin, roomid) uw_end = max(uw_end, roomid + 1) - if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ - and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): uw_begin = min(uw_begin, roomid) uw_end = max(uw_end, roomid + 1) uw_checked[location_id] = (roomid, mask) @@ -404,8 +411,7 @@ def new_check(location_id): ow_unchecked[location_id] = screenid ow_begin = min(ow_begin, screenid) ow_end = max(ow_end, screenid + 1) - if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): ow_checked[location_id] = screenid if ow_begin < ow_end: @@ -428,9 +434,7 @@ def new_check(location_id): for location_id, mask in location_table_npc_id.items(): if npc_value & mask != 0 and location_id not in ctx.locations_checked: new_check(location_id) - if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ - and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): npc_value |= mask npc_value_changed = True if npc_value_changed: @@ -446,8 +450,7 @@ def new_check(location_id): assert (0x3c6 <= offset <= 0x3c9) if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked: new_check(location_id) - if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ - and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): misc_data_changed = True misc_data[offset - 0x3c6] |= mask if misc_data_changed: diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index ec6862b9d054..a6a3bec9bf8d 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -1,15 +1,17 @@ import typing from BaseClasses import Dungeon -from worlds.alttp.Bosses import BossFactory from Fill import fill_restrictive -from worlds.alttp.Items import ItemFactory -from worlds.alttp.Regions import lookup_boss_drops -from worlds.alttp.Options import smallkey_shuffle + +from .Bosses import BossFactory +from .Items import ItemFactory +from .Regions import lookup_boss_drops +from .Options import smallkey_shuffle if typing.TYPE_CHECKING: from .SubClasses import ALttPLocation + def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): dungeon = Dungeon(name, dungeon_regions, big_key, diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index e10f4d544533..b7fe688431b7 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,7 +1,9 @@ # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict -from worlds.alttp.OverworldGlitchRules import overworld_glitch_connections -from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections + +from .OverworldGlitchRules import overworld_glitch_connections +from .UnderworldGlitchRules import underworld_glitch_connections + def link_entrances(world, player): connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 153dda4fc305..acec73bf33be 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -1,6 +1,7 @@ import collections -from worlds.alttp.Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region -from worlds.alttp.SubClasses import LTTPRegionType + +from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region +from .SubClasses import LTTPRegionType def create_inverted_regions(world, player): diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 7fd93ab93e3b..5761e5f099fe 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -2,14 +2,15 @@ import logging from BaseClasses import ItemClassification -from worlds.alttp.SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType -from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations -from worlds.alttp.Bosses import place_bosses -from worlds.alttp.Dungeons import get_dungeon_item_pool_player -from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError -from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses + +from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType +from .Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations +from .Bosses import place_bosses +from .Dungeons import get_dungeon_item_pool_player +from .EntranceShuffle import connect_entrance +from .Items import ItemFactory, GetBeemizerItem +from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses from .StateHelpers import has_triforce_pieces, has_melee_weapon # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index caa916ca1daf..40634de8daa3 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -2,6 +2,7 @@ from BaseClasses import ItemClassification as IC + def GetBeemizerItem(world, player: int, item): item_name = item if isinstance(item, str) else item.name diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index f6c3ec8d14a3..146fc2f0cac9 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -6,18 +6,21 @@ from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw + def get_sword_required_superbunny_mirror_regions(): """ Cave regions that superbunny can get through - but only with a sword. """ yield 'Spiral Cave (Top)' + def get_boots_required_superbunny_mirror_regions(): """ Cave regions that superbunny can get through - but only with boots. """ yield 'Two Brothers House' + def get_boots_required_superbunny_mirror_locations(): """ Cave locations that superbunny can access - but only with boots. @@ -207,7 +210,6 @@ def get_mirror_offset_spots_lw(player): yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player)) - def get_invalid_bunny_revival_dungeons(): """ Dungeon regions that can't be bunny revived from without superbunny state. @@ -300,6 +302,7 @@ def create_no_logic_connections(player, world, connections): parent.exits.append(connection) connection.connect(target) + def create_owg_connections(player, world, connections): for entrance, parent_region, target_region, *rule_override in connections: parent = world.get_region(parent_region, player) @@ -308,6 +311,7 @@ def create_owg_connections(player, world, connections): parent.exits.append(connection) connection.connect(target) + def set_owg_connection_rules(player, world, connections, default_rule): for entrance, _, _, *rule_override in connections: connection = world.get_entrance(entrance, player) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index e1cbb5c0cd10..a9dba3277a8e 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -21,22 +21,22 @@ from typing import Optional, List from BaseClasses import CollectionState, Region, Location, MultiWorld -from worlds.alttp.Shops import ShopType, ShopPriceType -from worlds.alttp.Dungeons import dungeon_music_addresses -from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address -from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable -from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \ +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom + +from .Shops import ShopType, ShopPriceType +from .Dungeons import dungeon_music_addresses +from .Regions import old_location_address_to_new_location_address +from .Text import MultiByteTextMapper, text_addresses, Credits, TextTable +from .Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \ Blind_texts, \ BombShop2_texts, junk_texts - -from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \ +from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \ DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom -from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items -from worlds.alttp.EntranceShuffle import door_addresses -from worlds.alttp.Options import smallkey_shuffle +from .Items import ItemFactory, item_table, item_name_groups, progression_items +from .EntranceShuffle import door_addresses +from .Options import smallkey_shuffle try: from maseya import z3pr diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 8e183c87a38e..b067634da051 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -3,12 +3,14 @@ from typing import List, Optional, Set, NamedTuple, Dict import logging -from worlds.alttp.SubClasses import ALttPLocation -from worlds.alttp.EntranceShuffle import door_addresses -from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle from Utils import int16_as_bytes +from .SubClasses import ALttPLocation +from .EntranceShuffle import door_addresses +from .Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem +from .Options import smallkey_shuffle + + logger = logging.getLogger("Shops") diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 33cea8fbfbb5..95e31e5ba328 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -1,50 +1,62 @@ from .SubClasses import LTTPRegion from BaseClasses import CollectionState + def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> bool: if state.has('Moon Pearl', player): return True return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world + def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool: return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player) + def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for shop in state.multiworld.shops) + def can_buy(state: CollectionState, item: str, player: int) -> bool: return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for shop in state.multiworld.shops) + def can_shoot_arrows(state: CollectionState, player: int) -> bool: if state.multiworld.retro_bow[player]: return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player) return state.has('Bow', player) or state.has('Silver Bow', player) + def has_triforce_pieces(state: CollectionState, player: int) -> bool: count = state.multiworld.treasure_hunt_count[player] return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count + def has_crystals(state: CollectionState, count: int, player: int) -> bool: found = state.count_group("Crystals", player) return found >= count + def can_lift_rocks(state: CollectionState, player: int): return state.has('Power Glove', player) or state.has('Titans Mitts', player) + def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool: return state.has('Titans Mitts', player) + def bottle_count(state: CollectionState, player: int) -> int: return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit, state.count_group("Bottles", player)) + def has_hearts(state: CollectionState, player: int, count: int) -> int: # Warning: This only considers items that are marked as advancement items return heart_count(state, player) >= count + def heart_count(state: CollectionState, player: int) -> int: # Warning: This only considers items that are marked as advancement items diff = state.multiworld.difficulty_requirements[player] @@ -53,6 +65,7 @@ def heart_count(state: CollectionState, player: int) -> int: + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + 3 # starting hearts + def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. basemagic = 8 @@ -69,6 +82,7 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, basemagic = basemagic + basemagic * bottle_count(state, player) return basemagic >= smallmagic + def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool: return (has_melee_weapon(state, player) or state.has('Cane of Somaria', player) @@ -77,6 +91,7 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) or state.has('Fire Rod', player) or (state.has('Bombs (10)', player) and enemies < 6)) + def can_get_good_bee(state: CollectionState, player: int) -> bool: cave = state.multiworld.get_region('Good Bee Cave', player) return ( @@ -87,49 +102,59 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool: is_not_bunny(state, cave, player) ) + def can_retrieve_tablet(state: CollectionState, player: int) -> bool: return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or (state.multiworld.swordless[player] and state.has("Hammer", player))) + def has_sword(state: CollectionState, player: int) -> bool: return state.has('Fighter Sword', player) \ or state.has('Master Sword', player) \ or state.has('Tempered Sword', player) \ or state.has('Golden Sword', player) + def has_beam_sword(state: CollectionState, player: int) -> bool: return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) + def has_melee_weapon(state: CollectionState, player: int) -> bool: return has_sword(state, player) or state.has('Hammer', player) + def has_fire_source(state: CollectionState, player: int) -> bool: return state.has('Fire Rod', player) or state.has('Lamp', player) + def can_melt_things(state: CollectionState, player: int) -> bool: return state.has('Fire Rod', player) or \ (state.has('Bombos', player) and (state.multiworld.swordless[player] or has_sword(state, player))) + def has_misery_mire_medallion(state: CollectionState, player: int) -> bool: return state.has(state.multiworld.required_medallions[player][0], player) def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool: return state.has(state.multiworld.required_medallions[player][1], player) + def can_boots_clip_lw(state: CollectionState, player: int) -> bool: if state.multiworld.mode[player] == 'inverted': return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) return state.has('Pegasus Boots', player) + def can_boots_clip_dw(state: CollectionState, player: int) -> bool: if state.multiworld.mode[player] != 'inverted': return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) return state.has('Pegasus Boots', player) + def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool: rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])] if state.multiworld.mode[player] != 'inverted': diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 5fc2aa0ba369..e791b73e75d9 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -4,6 +4,7 @@ from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld + class ALttPLocation(Location): game: str = "A Link to the Past" crystal: bool diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index f3d78e365c61..11a95bf7cd6c 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,12 +1,11 @@ - from BaseClasses import Entrance -from .SubClasses import LTTPRegion from worlds.generic.Rules import set_rule, add_rule from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion + # We actually need the logic to properly "mark" these regions as Light or Dark world. -# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. -def underworld_glitch_connections(world, player): +# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. +def underworld_glitch_connections(world, player): specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) mire = world.get_region('Misery Mire (West)', player) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d1a44df12f17..5ea936cc9d27 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -3,7 +3,6 @@ import random import threading import typing -from collections import OrderedDict import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld @@ -122,7 +121,7 @@ class ALTTPWorld(World): dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil Ganon! """ - game: str = "A Link to the Past" + game = "A Link to the Past" option_definitions = alttp_options topology_present = True item_name_groups = item_name_groups @@ -202,7 +201,7 @@ class ALTTPWorld(World): location_name_to_id = lookup_name_to_id data_version = 8 - required_client_version = (0, 3, 2) + required_client_version = (0, 4, 1) web = ALTTPWeb() pedestal_credit_texts: typing.Dict[int, str] = \ From 6c459066a761db1fb69b0759bd03b51af26afdcf Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 25 Apr 2023 13:26:52 +0200 Subject: [PATCH 13/35] Core: add generator_version to network protocol --- CommonClient.py | 15 +++++++++++---- MultiServer.py | 18 +++++++++--------- Utils.py | 3 +++ docs/network protocol.md | 3 ++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 2e10f6d5c0ee..87fa59cbf2c9 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -157,6 +157,7 @@ class CommonContext: disconnected_intentionally: bool = False server: typing.Optional[Endpoint] = None server_version: Version = Version(0, 0, 0) + generator_version: Version = Version(0, 0, 0) current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server last_death_link: float = time.time() # last send/received death link on AP layer @@ -260,6 +261,7 @@ def reset_server_state(self): self.items_received = [] self.locations_info = {} self.server_version = Version(0, 0, 0) + self.generator_version = Version(0, 0, 0) self.server = None self.server_task = None self.hint_cost = None @@ -646,11 +648,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict): logger.info('Room Information:') logger.info('--------------------------------') version = args["version"] - ctx.server_version = tuple(version) - version = ".".join(str(item) for item in version) + ctx.server_version = Version(*version) - logger.info(f'Server protocol version: {version}') - logger.info("Server protocol tags: " + ", ".join(args["tags"])) + if "generator_version" in args: + ctx.generator_version = Version(*args["generator_version"]) + logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, ' + f'generator version: {ctx.generator_version.as_simple_string()}, ' + f'tags: {", ".join(args["tags"])}') + else: + logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, ' + f'tags: {", ".join(args["tags"])}') if args['password']: logger.info('Password required') ctx.update_permissions(args.get("permissions", {})) diff --git a/MultiServer.py b/MultiServer.py index 3d5053bbe580..0537fd9b95cb 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -3,9 +3,6 @@ import argparse import asyncio import copy -import functools -import logging -import zlib import collections import datetime import functools @@ -162,7 +159,7 @@ class Context: read_data: typing.Dict[str, object] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] slot_info: typing.Dict[int, NetworkSlot] - + generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] @@ -226,7 +223,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.save_dirty = False self.tags = ['AP'] self.games: typing.Dict[int, str] = {} - self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} + self.minimum_client_versions: typing.Dict[int, Version] = {} self.seed_name = "" self.groups = {} self.group_collected: typing.Dict[int, typing.Set[int]] = {} @@ -384,15 +381,17 @@ def decompress(data: bytes) -> dict: def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any], use_embedded_server_options: bool): + self.read_data = {} mdata_ver = decoded_obj["minimum_versions"]["server"] - if mdata_ver > Utils.version_tuple: + if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," - f"however this server is of version {Utils.version_tuple}") + f"however this server is of version {version_tuple}") + self.generator_version = Version(*decoded_obj["version"]) clients_ver = decoded_obj["minimum_versions"].get("clients", {}) self.minimum_client_versions = {} for player, version in clients_ver.items(): - self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version) + self.minimum_client_versions[player] = max(Version(*version), min_client_version) self.slot_info = decoded_obj["slot_info"] self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} @@ -758,7 +757,8 @@ async def on_client_connected(ctx: Context, client: Client): # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, - 'version': Utils.version_tuple, + 'version': version_tuple, + 'generator_version': ctx.generator_version, 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, diff --git a/Utils.py b/Utils.py index 8a9478e49c3d..46312dc3fff8 100644 --- a/Utils.py +++ b/Utils.py @@ -38,6 +38,9 @@ class Version(typing.NamedTuple): minor: int build: int + def as_simple_string(self) -> str: + return ".".join(str(item) for item in self) + __version__ = "0.4.1" version_tuple = tuplize_version(__version__) diff --git a/docs/network protocol.md b/docs/network protocol.md index 052d62a531e5..48cf33183d0d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -67,10 +67,11 @@ Sent to clients when they connect to an Archipelago server. | Name | Type | Notes | |-----------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | +| generator_version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which generated the multiworld. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | | password | bool | Denoted whether a password is required to join this room. | | permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | -| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | +| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. | | games | list\[str\] | List of games present in this multiworld. | | datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | From b704070de54666c483943792339f85cda986d716 Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 26 Apr 2023 01:49:38 -0700 Subject: [PATCH 14/35] LADX: Fix palettes (#1767) --- worlds/ladx/LADXR/generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 90670c02584f..c63d2b72c5d3 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -54,8 +54,10 @@ from .patches import tradeSequence as _ from . import hints -from .locations.keyLocation import KeyLocation from .patches import bank34 +from .patches.aesthetics import rgb_to_bin, bin_to_rgb + +from .locations.keyLocation import KeyLocation from ..Options import TrendyGame, Palette, MusicChangeCondition @@ -368,7 +370,6 @@ def clamp(x, min, max): if x > max: return max return x - from patches.aesthetics import rgb_to_bin, bin_to_rgb for address in range(start, end, 2): packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address] From 9d40471dee687183821e80e8b8b37af366ff8557 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 Apr 2023 15:49:19 +0200 Subject: [PATCH 15/35] Subnautica: add free samples option --- worlds/subnautica/Options.py | 9 ++++++++- worlds/subnautica/__init__.py | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index fa66026d7d4a..582e93eb0ecb 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,6 +1,6 @@ import typing -from Options import Choice, Range, DeathLink, DefaultOnToggle, StartInventoryPool +from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool from .Creatures import all_creatures, Definitions @@ -35,6 +35,12 @@ class EarlySeaglide(DefaultOnToggle): display_name = "Early Seaglide" +class FreeSamples(Toggle): + """Get free items with your blueprints. + Items that can go into your inventory are awarded when you unlock their blueprint through Archipelago.""" + display_name = "Free Samples" + + class Goal(Choice): """Goal to complete. Launch: Leave the planet. @@ -100,6 +106,7 @@ class SubnauticaDeathLink(DeathLink): options = { "swim_rule": SwimRule, "early_seaglide": EarlySeaglide, + "free_samples": FreeSamples, "goal": Goal, "creature_scans": CreatureScans, "creature_scan_logic": AggressiveScanLogic, diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index bc1e4f696c14..0807f1a77da6 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -45,7 +45,7 @@ class SubnauticaWorld(World): option_definitions = Options.options data_version = 9 - required_client_version = (0, 3, 9) + required_client_version = (0, 4, 0) creatures_to_scan: List[str] @@ -129,6 +129,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "vanilla_tech": vanilla_tech, "creatures_to_scan": self.creatures_to_scan, "death_link": self.multiworld.death_link[self.player].value, + "free_samples": self.multiworld.free_samples[self.player].value, } return slot_data From a7816d186ff404e2412d93874947de9140409a69 Mon Sep 17 00:00:00 2001 From: Abacys <45606585+Abacys@users.noreply.github.com> Date: Wed, 26 Apr 2023 07:43:23 -0400 Subject: [PATCH 16/35] Launcher: Correcting minor formatting error (#1768) Reformatting comment to comply with PEP format --- worlds/LauncherComponents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 3a2e319e8b2f..d9f04e601d11 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -95,7 +95,7 @@ def __call__(self, path: str): # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), - #Kingdom Hearts 2 + # Kingdom Hearts 2 Component('KH2 Client', "KH2Client"), ] From 7bcf29941286c2206d1370e6d2eb88f6406e2352 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 26 Apr 2023 20:23:52 -0400 Subject: [PATCH 17/35] SM: missing foreign item filter fix (#1774) --- worlds/sm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index ef7e50ba58e1..d60b0f51e65c 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -227,7 +227,7 @@ def set_entrance_rule(entrance, player, func): add_postAvailable_rule(location, self.player, value.PostAvailable) if self.multiworld.doors_colors_rando[self.player].value != 0: - add_item_rule(location, lambda item: item.type not in ammoItems or + add_item_rule(location, lambda item: item.game != self.game or item.type not in ammoItems or (item.type in ammoItems and \ (not item.advancement or (item.advancement and item.player == self.player)))) From b55174ccdf033894c39f81d32f125915a6cc7368 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 27 Apr 2023 02:33:49 -0500 Subject: [PATCH 18/35] Docs: document option alias in the options doc (#1755) * Docs: document option alias in the options doc * give an example of alias and move it under option creation. * use clearer example names --- docs/options api.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/options api.md b/docs/options api.md index a1407f2cebc0..fdabd9facd8a 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -13,14 +13,20 @@ need to create: - A new option class with a docstring detailing what the option will do to your user. - A `display_name` to be displayed on the webhost. - A new entry in the `option_definitions` dict for your World. -By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options -such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and -stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option` -on the webhost. All options support `random` as a generic option. `random` chooses from any of the available -values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own -new `option_random`. +By style and convention, the internal names should be snake_case. ### Option Creation +- If the option supports having multiple sub_options, such as Choice options, these can be defined with +`option_value1`. Any attributes of the class with a preceding `option_` is added to the class's `options` lookup. The +`option_` is then stripped for users, so will show as `value1` in yaml files. If `auto_display_name` is True, it will +display as `Value1` on the webhost. +- An alternative name can be set for any specific option by setting an alias attribute +(i.e. `alias_value_1 = option_value1`) which will allow users to use either `value_1` or `value1` in their yaml +files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a +Choice, and defining `alias_true = option_full`. +- All options support `random` as a generic option. `random` chooses from any of the available values for that option, +and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. + As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our options: From 28c5e9ee65f36bcd8bf16eb6703a57254482ff7f Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 27 Apr 2023 20:30:13 -0700 Subject: [PATCH 19/35] LADX: Rework dungeon item fill (#1763) --- worlds/ladx/__init__.py | 112 ++++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index ddb73f6b3c07..b30919a82cb0 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,5 +1,6 @@ import binascii import bsdiff4 +import itertools import os import pkgutil import tempfile @@ -137,7 +138,7 @@ def create_item(self, item_name: str): def create_event(self, event: str): return Item(event, ItemClassification.progression, None, self.player) - def create_items(self) -> None: + def create_items(self) -> None: exclude = [item.name for item in self.multiworld.precollected_items[self.player]] dungeon_item_types = { @@ -146,6 +147,7 @@ def create_items(self) -> None: self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] self.prefill_own_dungeons = [] + self.pre_fill_items = [] # For any and different world, set item rule instead for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks"]: @@ -185,6 +187,7 @@ def create_items(self) -> None: location = self.multiworld.get_location(item.item_data.vanilla_location, self.player) location.place_locked_item(item) continue + if isinstance(item.item_data, DungeonItemData): if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT: # Find instrument, lock @@ -210,8 +213,10 @@ def create_items(self) -> None: shuffle_type = dungeon_item_types[item_type] if shuffle_type == DungeonItemShuffle.option_original_dungeon: self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item) + self.pre_fill_items.append(item) elif shuffle_type == DungeonItemShuffle.option_own_dungeons: self.prefill_own_dungeons.append(item) + self.pre_fill_items.append(item) else: self.multiworld.itempool.append(item) else: @@ -219,16 +224,12 @@ def create_items(self) -> None: self.multi_key = self.generate_multi_key() - dungeon_locations = [] - dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] - all_state = self.multiworld.get_all_state(use_cache=False) - # Add special case for trendy shop access trendy_region = self.multiworld.get_region("Trendy Shop", self.player) event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) trendy_region.locations.insert(0, event_location) event_location.place_locked_item(self.create_event("Can Play Trendy Game")) - + # For now, special case first item FORCE_START_ITEM = True if FORCE_START_ITEM: @@ -241,41 +242,98 @@ def create_items(self) -> None: index = self.multiworld.random.choice(possible_start_items) start_item = self.multiworld.itempool.pop(index) start_loc.place_locked_item(start_item) + + self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] for r in self.multiworld.get_regions(): if r.player != self.player: continue # Set aside dungeon locations if r.dungeon_index: - dungeon_locations += r.locations - dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations + self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations for location in r.locations: - if location.name == "Pit Button Chest (Tail Cave)": - # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up - # TODO: no need for this if small key shuffle - dungeon_locations.remove(location) - dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) + # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up + # TODO: no need for this if small key shuffle + if location.name == "Pit Button Chest (Tail Cave)" or location.item: + self.dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) # Properly fill locations within dungeon location.dungeon = r.dungeon_index + def get_pre_fill_items(self): + return self.pre_fill_items + + def pre_fill(self) -> None: + allowed_locations_by_item = {} + + + # Set up filter rules + + # The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons + all_dungeon_items_to_fill = list(self.prefill_own_dungeons) + # set containing the list of all possible dungeon locations for the player + all_dungeon_locs = set() + + # Do dungeon specific things for dungeon_index in range(0, 9): - locs = dungeon_locations_by_dungeon[dungeon_index] - locs = [loc for loc in locs if not loc.item] - self.multiworld.random.shuffle(locs) - self.multiworld.random.shuffle(self.prefill_original_dungeon[dungeon_index]) - fill_restrictive(self.multiworld, all_state, locs, self.prefill_original_dungeon[dungeon_index], lock=True) - assert not self.prefill_original_dungeon[dungeon_index] - - # Fill dungeon items first, to not torture the fill algo - dungeon_locations = [loc for loc in dungeon_locations if not loc.item] - # dungeon_items = sorted(self.prefill_own_dungeons, key=lambda item: item.item_data.dungeon_item_type) - self.multiworld.random.shuffle(self.prefill_own_dungeons) - self.multiworld.random.shuffle(dungeon_locations) - fill_restrictive(self.multiworld, all_state, dungeon_locations, self.prefill_own_dungeons, lock=True) + # set up allow-list for dungeon specific items + locs = set(self.dungeon_locations_by_dungeon[dungeon_index]) + for item in self.prefill_original_dungeon[dungeon_index]: + allowed_locations_by_item[item] = locs + + # put the items for this dungeon in the list to fill + all_dungeon_items_to_fill.extend(self.prefill_original_dungeon[dungeon_index]) + + # ...and gather the list of all dungeon locations + all_dungeon_locs |= locs + # ...also set the rules for the dungeon + for location in locs: + orig_rule = location.item_rule + # If an item is about to be placed on a dungeon location, it can go there iff + # 1. it fits the general rules for that location (probably 'return True' for most places) + # 2. Either + # 2a. it's not a restricted dungeon item + # 2b. it's a restricted dungeon item and this location is specified as allowed + location.item_rule = lambda item, location=location, orig_rule=orig_rule: \ + (item not in allowed_locations_by_item or location in allowed_locations_by_item[item]) and orig_rule(item) + + # Now set up the allow-list for any-dungeon items + for item in self.prefill_own_dungeons: + # They of course get to go in any spot + allowed_locations_by_item[item] = all_dungeon_locs + + # Get the list of locations and shuffle + all_dungeon_locs_to_fill = list(all_dungeon_locs) + self.multiworld.random.shuffle(all_dungeon_locs_to_fill) + + # Get the list of items and sort by priority + def priority(item): + # 0 - Nightmare dungeon-specific + # 1 - Key dungeon-specific + # 2 - Other dungeon-specific + # 3 - Nightmare any local dungeon + # 4 - Key any local dungeon + # 5 - Other any local dungeon + i = 2 + if "Nightmare" in item.name: + i = 0 + elif "Key" in item.name: + i = 1 + if allowed_locations_by_item[item] is all_dungeon_locs: + i += 3 + return i + all_dungeon_items_to_fill.sort(key=priority) + + # Set up state + all_state = self.multiworld.get_all_state(use_cache=False) + # Remove dungeon items we are about to put in from the state so that we don't double count + for item in all_dungeon_items_to_fill: + all_state.remove(item) + + # Finally, fill! + fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) name_cache = {} - # Tries to associate an icon from another game with an icon we have def guess_icon_for_other_world(self, other): if not self.name_cache: From 42da24cb5e8f80b54613f2105cfb510a6a728570 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 21 Apr 2023 15:12:43 +0200 Subject: [PATCH 20/35] WebHost: offer room owner log download link --- WebHostLib/misc.py | 6 +++++- WebHostLib/templates/hostRoom.html | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index bf9f4e2fd7db..6d3e82c00c6d 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -116,7 +116,11 @@ def display_log(room: UUID): if room is None: return abort(404) if room.owner == session["_id"]: - return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") + file_path = os.path.join("logs", str(room.id) + ".txt") + if os.path.exists(file_path): + return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8") + return "Log File does not exist." + return "Access Denied", 403 diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 6f02dc0944e0..ba15d64acac1 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -32,13 +32,18 @@ {% endif %} {{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %} -
-
- - -
-
+
+
+
+ + +
+
+ + Open Log File... + +