Core: implement first version of ItemLinks

This commit is contained in:
Fabian Dill 2022-02-05 15:49:19 +01:00
parent 6923800081
commit 28201a6c38
10 changed files with 277 additions and 75 deletions

View File

@ -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)})'

84
Main.py
View File

@ -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):

View File

@ -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())

View File

@ -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):

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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", {})

View File

@ -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):

View File

@ -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)