diff --git a/BaseClasses.py b/BaseClasses.py index 7d481408..69da9955 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,12 +1,13 @@ from __future__ import annotations import copy +import typing from enum import Enum, unique import logging import json import functools from collections import OrderedDict, Counter, deque -from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple +from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, TYPE_CHECKING import secrets import random @@ -14,6 +15,19 @@ import Options import Utils import NetUtils +if TYPE_CHECKING: + from worlds import AutoWorld + auto_world = AutoWorld.World +else: + auto_world = object + + +class Group(TypedDict): + name: str + game: str + world: auto_world + players: Set[int] + class MultiWorld(): debug_types = False @@ -27,6 +41,7 @@ class MultiWorld(): plando_items: List plando_connections: List worlds: Dict[int, Any] + groups: Dict[int, Group] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -44,6 +59,7 @@ class MultiWorld(): self.glitch_triforce = False self.algorithm = 'balanced' self.dungeons: Dict[Tuple[str, int], Dungeon] = {} + self.groups = {} self.regions = [] self.shops = [] self.itempool = [] @@ -132,6 +148,59 @@ class MultiWorld(): self.worlds = {} self.slot_seeds = {} + def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: + """Create a group with name and return the assigned player ID and group. + If a group of this name already exists, the set of players is extended instead of creating a new one.""" + for group_id, group in self.groups.items(): + if group["name"] == name: + group["players"] |= players + return group_id, group + new_id: int = self.players + len(self.groups) + 1 + from worlds import AutoWorld + self.game[new_id] = game + self.custom_data[new_id] = {} + self.player_types[new_id] = NetUtils.SlotType.group + world_type = AutoWorld.AutoWorldRegister.world_types[game] + for option_key, option in world_type.options.items(): + getattr(self, option_key)[new_id] = option(option.default) + for option_key, option in Options.common_options.items(): + getattr(self, option_key)[new_id] = option(option.default) + for option_key, option in Options.per_game_common_options.items(): + getattr(self, option_key)[new_id] = option(option.default) + + self.worlds[new_id] = world_type(self, new_id) + + self.player_name[new_id] = name + # TODO: remove when LttP are transitioned over + self.difficulty_requirements[new_id] = self.difficulty_requirements[next(iter(players))] + + new_group = self.groups[new_id] = Group(name=name, game=game, players=players, + world=self.worlds[new_id]) + + # instead of collect/remove overwrites, should encode sending as Events so they show up in spoiler log + def group_collect(state, item) -> bool: + changed = False + for player in new_group["players"]: + max(self.worlds[player].collect(state, item), changed) + return changed + + def group_remove(state, item) -> bool: + changed = False + for player in new_group["players"]: + max(self.worlds[player].remove(state, item), changed) + return changed + + new_world = new_group["world"] + new_world.collect = group_collect + new_world.remove = group_remove + + self.worlds[new_id] = new_world + + return new_id, new_group + + def get_player_groups(self, player) -> typing.Set[int]: + return {group_id for group_id, group in self.groups.items() if player in group["players"]} + def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None): self.seed = get_seed(seed) if secure: @@ -176,7 +245,8 @@ class MultiWorld(): @functools.lru_cache() def get_game_worlds(self, game_name: str): - return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name) + return tuple(world for player, world in self.worlds.items() if + player not in self.groups and self.game[player] == game_name) def get_name_string_for_object(self, obj) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})' diff --git a/Main.py b/Main.py index 11b8e32e..b71129df 100644 --- a/Main.py +++ b/Main.py @@ -1,3 +1,5 @@ +import copy +import collections from itertools import zip_longest, chain import logging import os @@ -7,7 +9,7 @@ import concurrent.futures import pickle import tempfile import zipfile -from typing import Dict, Tuple, Optional +from typing import Dict, Tuple, Optional, Set from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType from worlds.alttp.Items import item_name_groups @@ -18,7 +20,6 @@ from Utils import output_path, get_options, __version__, version_tuple from worlds.generic.Rules import locality_rules, exclusion_rules from worlds import AutoWorld - ordered_areas = ( 'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', @@ -136,6 +137,74 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No AutoWorld.call_all(world, "generate_basic") + # temporary home for item links, should be moved out of Main + item_links = {} + for player in world.player_ids: + for item_link in world.item_links[player].value: + if item_link["name"] in item_links: + item_links[item_link["name"]]["players"][player] = item_link["replacement_item"] + item_links[item_link["name"]]["item_pool"] &= set(item_link["item_pool"]) + else: + if item_link["name"] in world.player_name.values(): + raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}).") + item_links[item_link["name"]] = { + "players": {player: item_link["replacement_item"]}, + "item_pool": set(item_link["item_pool"]), + "game": world.game[player] + } + + for item_link in item_links.values(): + current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups + pool = set() + for item in item_link["item_pool"]: + pool |= current_item_name_groups.get(item, {item}) + item_link["item_pool"] = pool + + for group_name, item_link in item_links.items(): + game = item_link["game"] + group_id, group = world.add_group(group_name, game, set(item_link["players"])) + + def find_common_pool(players: Set[int], shared_pool: Set[int]) -> \ + Dict[int, Dict[str, int]]: + counters = {player: {name: 0 for name in shared_pool} for player in players} + for item in world.itempool: + if item.player in counters and item.name in shared_pool: + counters[item.player][item.name] += 1 + + for item in shared_pool: + count = min(counters[player][item] for player in players) + if count: + for player in players: + counters[player][item] = count + else: + for player in players: + del(counters[player][item]) + return counters + + common_item_count = find_common_pool(group["players"], item_link["item_pool"]) + + new_itempool = [] + for item_name, item_count in next(iter(common_item_count.values())).items(): + for _ in range(item_count): + new_itempool.append(group["world"].create_item(item_name)) + + for item in world.itempool: + if common_item_count.get(item.player, {}).get(item.name, 0): + common_item_count[item.player][item.name] -= 1 + else: + new_itempool.append(item) + + itemcount = len(world.itempool) + world.itempool = new_itempool + + while itemcount > len(world.itempool): + for player in world.get_game_players(game): + if item_link["players"][player]: + world.itempool.append(AutoWorld.call_single(world, "create_item", player, + item_link["players"][player])) + else: + AutoWorld.call_single(world, "create_filler", player) + logger.info("Running Item Plando") for item in world.itempool: @@ -253,10 +322,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for slot in world.player_ids: client_versions[slot] = world.worlds[slot].get_required_client_version() games[slot] = world.game[slot] - slot_info[slot] = NetUtils.NetworkSlot(names[0][slot-1], world.game[slot], world.player_types[slot]) + slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot], + world.player_types[slot]) + for slot, group in world.groups.items(): + games[slot] = world.game[slot] + slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot], + group_members=sorted(group["players"])) precollected_items = {player: [item.code for item in world_precollected] for player, world_precollected in world.precollected_items.items()} - precollected_hints = {player: set() for player in range(1, world.players + 1)} + precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))} sending_visible_players = set() @@ -321,7 +395,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: logger.warning("Location Accessibility requirements not fulfilled.") - # retrieve exceptions via .result() if they occured. + # retrieve exceptions via .result() if they occurred. multidata_task.result() for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1): if i % 10 == 0 or i == len(output_file_futures): diff --git a/MultiServer.py b/MultiServer.py index 98304017..4a6acaa7 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -98,6 +98,7 @@ class Context: # team -> slot id -> list of clients authenticated to slot. clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]] locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] + groups: typing.Dict[int, typing.Set[int]] save_version = 2 def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, @@ -158,6 +159,7 @@ class Context: self.games: typing.Dict[int, str] = {} self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} self.seed_name = "" + self.groups = {} self.random = random.Random() # General networking @@ -305,10 +307,11 @@ class Context: if "slot_info" in decoded_obj: self.slot_info = decoded_obj["slot_info"] self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} - + self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items() + if slot_info.type == SlotType.group} else: self.games = decoded_obj["games"] - + self.groups = {} self.slot_info = { slot: NetworkSlot( self.player_names[0, slot], @@ -417,7 +420,7 @@ class Context: self.received_items[(*old, False)] = items.copy() for (team, slot, remote) in self.received_items: # remove start inventory from items, since this is separate now - start_inventory = get_start_inventory(self, team, slot, slot in self.remote_start_inventory) + start_inventory = get_start_inventory(self, slot, slot in self.remote_start_inventory) if start_inventory: del self.received_items[team, slot, remote][:len(start_inventory)] logging.info("Upgraded save data") @@ -640,14 +643,15 @@ def get_players_string(ctx: Context): current_team = -1 text = '' for team, slot in player_names: - player_name = ctx.player_names[team, slot] - if team != current_team: - text += f':: Team #{team + 1}: ' - current_team = team - if (team, slot) in auth_clients: - text += f'{player_name} ' - else: - text += f'({player_name}) ' + if ctx.slot_info[slot].type == SlotType.player: + player_name = ctx.player_names[team, slot] + if team != current_team: + text += f':: Team #{team + 1}: ' + current_team = team + if (team, slot) in auth_clients: + text += f'{player_name} ' + else: + text += f'({player_name}) ' return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1] @@ -668,7 +672,7 @@ def get_received_items(ctx: Context, team: int, player: int, remote_items: bool) return ctx.received_items.setdefault((team, player, remote_items), []) -def get_start_inventory(ctx: Context, team: int, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]: +def get_start_inventory(ctx: Context, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]: return ctx.start_inventory.setdefault(player, []) if remote_start_inventory else [] @@ -678,7 +682,7 @@ def send_new_items(ctx: Context): for client in clients: if client.no_items: continue - start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory) + start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory) items = get_received_items(ctx, team, slot, client.remote_items) if len(start_inventory) + len(items) > client.send_index: first_new_item = max(0, client.send_index - len(start_inventory)) @@ -724,6 +728,15 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: return sorted(items) +def send_items_to(ctx: Context, team: int, slot: int, *items: NetworkItem): + targets = ctx.groups.get(slot, [slot]) + for target in targets: + for item in items: + if target != item.player: + get_received_items(ctx, team, target, False).append(item) + get_received_items(ctx, team, target, True).append(item) + + def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], count_activity: bool = True): new_locations = set(locations) - ctx.location_checks[team, slot] @@ -733,11 +746,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) for location in new_locations: item_id, target_player, flags = ctx.locations[slot][location] - new_item = NetworkItem(item_id, location, slot, flags) - if target_player != slot: - get_received_items(ctx, team, target_player, False).append(new_item) - get_received_items(ctx, team, target_player, True).append(new_item) + send_items_to(ctx, team, target_player, new_item) logging.info('(Team #%d) %s sent %s to %s (%s)' % ( team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), @@ -1362,7 +1372,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): "slot_data": ctx.slot_data[client.slot], "slot_info": ctx.slot_info }] - start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory) + start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) if (start_inventory or items) and not client.no_items: reply.append({"cmd": 'ReceivedItems', "index": 0, "items": start_inventory + items}) @@ -1397,7 +1407,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): if args.get('items_handling', None) is not None and client.items_handling != args['items_handling']: try: client.items_handling = args['items_handling'] - start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory) + start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) if (items or start_inventory) and not client.no_items: client.send_index = len(start_inventory) + len(items) @@ -1421,7 +1431,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): f"from {old_tags} to {client.tags}.") elif cmd == 'Sync': - start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory) + start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) if (start_inventory or items) and not client.no_items: client.send_index = len(start_inventory) + len(items) @@ -1611,9 +1621,8 @@ class ServerCommandProcessor(CommonCommandProcessor): if usable: amount: int = int(amount) new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))] + send_items_to(self.ctx, team, slot, *new_items) - get_received_items(self.ctx, team, slot, True).extend(new_items) - get_received_items(self.ctx, team, slot, False).extend(new_items) send_new_items(self.ctx) self.ctx.notify_all( 'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + @@ -1700,10 +1709,8 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}") if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}: self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}]) - if option_name in {"hint_cost", "location_check_points"}: - room_update = {"cmd": "RoomUpdate"} - room_update[option_name] = getattr(self.ctx, option_name) - self.ctx.broadcast_all([room_update]) + elif option_name in {"hint_cost", "location_check_points"}: + self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}]) return True else: known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items()) diff --git a/NetUtils.py b/NetUtils.py index f748bbb2..e6b4a8a5 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -71,6 +71,7 @@ class NetworkSlot(typing.NamedTuple): name: str game: str type: SlotType + group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group class NetworkItem(typing.NamedTuple): diff --git a/Options.py b/Options.py index 370201d0..fbcf5eea 100644 --- a/Options.py +++ b/Options.py @@ -2,6 +2,8 @@ from __future__ import annotations import typing import random +from schema import Schema, And, Or + class AssembleOptions(type): def __new__(mcs, name, bases, attrs): @@ -25,14 +27,28 @@ class AssembleOptions(type): # auto-validate schema on __init__ if "schema" in attrs.keys(): - def validate_decorator(func): - def validate(self, *args, **kwargs): - func(self, *args, **kwargs) + + if "__init__" in attrs: + def validate_decorator(func): + def validate(self, *args, **kwargs): + ret = func(self, *args, **kwargs) + self.value = self.schema.validate(self.value) + return ret + + return validate + attrs["__init__"] = validate_decorator(attrs["__init__"]) + else: + # construct an __init__ that calls parent __init__ + + cls = super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) + + def meta__init__(self, *args, **kwargs): + super(cls, self).__init__(*args, **kwargs) self.value = self.schema.validate(self.value) - return validate + cls.__init__ = meta__init__ + return cls - attrs["__init__"] = validate_decorator(attrs["__init__"]) return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) @@ -143,8 +159,8 @@ class Choice(Option): text = text.lower() if text == "random": return cls(random.choice(list(cls.name_lookup))) - for optionname, value in cls.options.items(): - if optionname == text: + for option_name, value in cls.options.items(): + if option_name == text: return cls(value) raise KeyError( f'Could not find option "{text}" for "{cls.__name__}", ' @@ -213,20 +229,22 @@ class Range(Option, int): elif text.startswith("random-range-"): textsplit = text.split("-") try: - randomrange = [int(textsplit[len(textsplit)-2]), int(textsplit[len(textsplit)-1])] + random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])] except ValueError: raise ValueError(f"Invalid random range {text} for option {cls.__name__}") - randomrange.sort() - if randomrange[0] < cls.range_start or randomrange[1] > cls.range_end: - raise Exception(f"{randomrange[0]}-{randomrange[1]} is outside allowed range {cls.range_start}-{cls.range_end} for option {cls.__name__}") + random_range.sort() + if random_range[0] < cls.range_start or random_range[1] > cls.range_end: + raise Exception( + f"{random_range[0]}-{random_range[1]} is outside allowed range " + f"{cls.range_start}-{cls.range_end} for option {cls.__name__}") if text.startswith("random-range-low"): - return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[0])))) + return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[0])))) elif text.startswith("random-range-middle"): - return cls(int(round(random.triangular(randomrange[0], randomrange[1])))) + return cls(int(round(random.triangular(random_range[0], random_range[1])))) elif text.startswith("random-range-high"): - return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[1])))) + return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[1])))) else: - return cls(int(round(random.randint(randomrange[0], randomrange[1])))) + return cls(int(round(random.randint(random_range[0], random_range[1])))) else: return cls(random.randint(cls.range_start, cls.range_end)) return cls(int(text)) @@ -412,11 +430,6 @@ class StartInventory(ItemDict): display_name = "Start Inventory" -class ItemLinks(OptionList): - """Share these items with players of the same game.""" - display_name = "Shared Items" - - class StartHints(ItemSet): """Start with these item's locations prefilled into the !hint command.""" display_name = "Start Hints" @@ -444,6 +457,18 @@ class DeathLink(Toggle): display_name = "Death Link" +class ItemLinks(OptionList): + """Share part of your item pool with other players.""" + default = [] + schema = Schema([ + { + "name": And(str, len), + "item_pool": [And(str, len)], + "replacement_item": Or(And(str, len), None) + } + ]) + + per_game_common_options = { **common_options, # can be overwritten per-game "local_items": LocalItems, @@ -453,8 +478,10 @@ per_game_common_options = { "start_location_hints": StartLocationHints, "exclude_locations": ExcludeLocations, "priority_locations": PriorityLocations, + "item_links": ItemLinks } + if __name__ == "__main__": from worlds.alttp.Options import Logic @@ -462,8 +489,8 @@ if __name__ == "__main__": map_shuffle = Toggle compass_shuffle = Toggle - keyshuffle = Toggle - bigkey_shuffle = Toggle + key_shuffle = Toggle + big_key_shuffle = Toggle hints = Toggle test = argparse.Namespace() test.logic = Logic.from_text("no_logic") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 3da65162..5e71f994 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import logging from typing import Dict, Set, Tuple, List, Optional, TextIO, Any from BaseClasses import MultiWorld, Item, CollectionState, Location @@ -87,6 +89,8 @@ class World(metaclass=AutoWorldRegister): hint_blacklist: Set[str] = frozenset() # any names that should not be hintable + # NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set. + # These values will be removed. # if a world is set to remote_items, then it just needs to send location checks to the server and the server # sends back the items # if a world is set to remote_items = False, then the server never sends an item where receiver == finder, @@ -189,35 +193,46 @@ class World(metaclass=AutoWorldRegister): pass # end of ordered Main.py calls - def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]: - """Collect an item name into state. For speed reasons items that aren't logically useful get skipped. - Collect None to skip item. - :param remove: indicate if this is meant to remove from state instead of adding.""" - if item.advancement: - return item.name - def create_item(self, name: str) -> Item: """Create an item for this world type and player. Warning: this may be called with self.world = None, for example by MultiServer""" raise NotImplementedError + def get_filler_item_name(self) -> str: + """Called when the item pool needs to be filled with additional items to match location count.""" + logging.warning(f"World {self} is generating a filler item without custom filler pool.") + return self.world.random.choice(self.item_name_to_id) + + # decent place to implement progressive items, in most cases can stay as-is + def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]: + """Collect an item name into state. For speed reasons items that aren't logically useful get skipped. + Collect None to skip item. + :param state: CollectionState to collect into + :param item: Item to decide on if it should be collected into state + :param remove: indicate if this is meant to remove from state instead of adding.""" + if item.advancement: + return item.name + # following methods should not need to be overridden. def collect(self, state: CollectionState, item: Item) -> bool: name = self.collect_item(state, item) if name: - state.prog_items[name, item.player] += 1 + state.prog_items[name, self.player] += 1 return True return False def remove(self, state: CollectionState, item: Item) -> bool: name = self.collect_item(state, item, True) if name: - state.prog_items[name, item.player] -= 1 - if state.prog_items[name, item.player] < 1: - del (state.prog_items[name, item.player]) + state.prog_items[name, self.player] -= 1 + if state.prog_items[name, self.player] < 1: + del (state.prog_items[name, self.player]) return True return False + def create_filler(self): + self.world.itempool.append(self.create_item(self.get_filler_item_name())) + # any methods attached to this can be used as part of CollectionState, # please use a prefix as all of them get clobbered together diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index c66758dc..6a9d872f 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -755,6 +755,7 @@ def patch_rom(world, rom, player, enemized): local_random = world.slot_seeds[player] # patch items + targets_pointing_to_here = world.get_player_groups(player) | {player} for location in world.get_locations(): if location.player != player or location.address is None or location.shop_slot is not None: @@ -785,7 +786,7 @@ def patch_rom(world, rom, player, enemized): itemid = list(location_table.keys()).index(location.name) + 1 assert itemid < 0x100 rom.write_byte(location.player_address, 0xFF) - elif location.item.player != player: + elif location.item.player not in targets_pointing_to_here: if location.player_address is not None: rom.write_byte(location.player_address, min(location.item.player, ROM_PLAYER_LIMIT)) else: @@ -1653,9 +1654,10 @@ def patch_rom(world, rom, player, enemized): rom.write_bytes(0x7FC0, rom.name) # set player names - for p in range(1, min(world.players, ROM_PLAYER_LIMIT) + 1): + encoded_players = world.players + len(world.groups) + for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1): rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p])) - if world.players > ROM_PLAYER_LIMIT: + if encoded_players > ROM_PLAYER_LIMIT: rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago")) # Write title screen Code diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 342b0a9d..41a62ae2 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -404,6 +404,9 @@ class ALTTPWorld(World): fill_locations.remove(spot_to_fill) # very slow, unfortunately trash_count -= 1 + def get_filler_item_name(self) -> str: + return "Rupees (5)" # temporary + def get_same_seed(world, seed_def: tuple) -> str: seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {}) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a4bcde78..4f9382f0 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -38,7 +38,9 @@ class Factorio(World): item_name_to_id = all_items location_name_to_id = base_tech_table - + item_name_groups = { + "Progressive": set(progressive_tech_table.values()), + } data_version = 5 def __init__(self, world, player: int): diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index fe8a4870..e569d761 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -460,11 +460,12 @@ class SMWorld(World): return slot_data def collect(self, state: CollectionState, item: Item) -> bool: - state.smbm[item.player].addItem(item.type) - if item.advancement: - state.prog_items[item.name, item.player] += 1 - return True # indicate that a logical state change has occured - return False + state.smbm[self.player].addItem(item.type) + 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)