Core: implement first version of ItemLinks
This commit is contained in:
parent
6923800081
commit
28201a6c38
|
@ -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
84
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):
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
71
Options.py
71
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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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", {})
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue