mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 08:33:22 -07:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764e6e7926 | ||
|
|
4292cdddd5 | ||
|
|
9aef76767a | ||
|
|
3858a12f26 | ||
|
|
1943586221 | ||
|
|
6d15aef88a | ||
|
|
50f06c3aac | ||
|
|
7f3c46dd8a | ||
|
|
ea15f221ae | ||
|
|
d4b422840a | ||
|
|
0586b24579 | ||
|
|
e11016b0a2 | ||
|
|
74a368458e | ||
|
|
1b70d485c0 | ||
|
|
2355f9c8d3 | ||
|
|
ceea55e3c6 | ||
|
|
c4d6ac50be | ||
|
|
4461cb67f0 | ||
|
|
f0a6b5a8e4 | ||
|
|
443fc03700 | ||
|
|
6567f14415 | ||
|
|
32560eac92 | ||
|
|
4c71662719 | ||
|
|
96a28ed41e | ||
|
|
bc1d0ed583 | ||
|
|
635897574f | ||
|
|
0eca0b2209 | ||
|
|
20b72369d8 | ||
|
|
d451145d53 | ||
|
|
4ab59d522d | ||
|
|
250099f5fd | ||
|
|
c14a150795 | ||
|
|
91bcd59940 | ||
|
|
b871a688a4 | ||
|
|
d225eb9ca8 |
@@ -24,6 +24,13 @@ class MultiWorld():
|
|||||||
plando_connections: List[PlandoConnection]
|
plando_connections: List[PlandoConnection]
|
||||||
er_seeds: Dict[int, str]
|
er_seeds: Dict[int, str]
|
||||||
|
|
||||||
|
class AttributeProxy():
|
||||||
|
def __init__(self, rule):
|
||||||
|
self.rule = rule
|
||||||
|
|
||||||
|
def __getitem__(self, player) -> bool:
|
||||||
|
return self.rule(player)
|
||||||
|
|
||||||
def __init__(self, players: int):
|
def __init__(self, players: int):
|
||||||
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
|
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
|
||||||
|
|
||||||
@@ -56,15 +63,21 @@ class MultiWorld():
|
|||||||
self.dynamic_regions = []
|
self.dynamic_regions = []
|
||||||
self.dynamic_locations = []
|
self.dynamic_locations = []
|
||||||
self.spoiler = Spoiler(self)
|
self.spoiler = Spoiler(self)
|
||||||
|
self.fix_trock_doors = self.AttributeProxy(lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||||
|
self.fix_skullwoods_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||||
|
self.fix_palaceofdarkness_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||||
|
self.fix_trock_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||||
|
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
|
||||||
|
|
||||||
for player in range(1, players + 1):
|
for player in range(1, players + 1):
|
||||||
def set_player_attr(attr, val):
|
def set_player_attr(attr, val):
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
|
set_player_attr('tech_tree_layout_prerequisites', {})
|
||||||
set_player_attr('_region_cache', {})
|
set_player_attr('_region_cache', {})
|
||||||
set_player_attr('shuffle', "vanilla")
|
set_player_attr('shuffle', "vanilla")
|
||||||
set_player_attr('logic', "noglitches")
|
set_player_attr('logic', "noglitches")
|
||||||
set_player_attr('mode', 'open')
|
set_player_attr('mode', 'open')
|
||||||
set_player_attr('swords', 'random')
|
set_player_attr('swordless', False)
|
||||||
set_player_attr('difficulty', 'normal')
|
set_player_attr('difficulty', 'normal')
|
||||||
set_player_attr('item_functionality', 'normal')
|
set_player_attr('item_functionality', 'normal')
|
||||||
set_player_attr('timer', False)
|
set_player_attr('timer', False)
|
||||||
@@ -80,11 +93,6 @@ class MultiWorld():
|
|||||||
set_player_attr('powder_patch_required', False)
|
set_player_attr('powder_patch_required', False)
|
||||||
set_player_attr('ganon_at_pyramid', True)
|
set_player_attr('ganon_at_pyramid', True)
|
||||||
set_player_attr('ganonstower_vanilla', True)
|
set_player_attr('ganonstower_vanilla', True)
|
||||||
set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
|
|
||||||
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
|
||||||
set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
||||||
set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
||||||
set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
||||||
set_player_attr('can_access_trock_eyebridge', None)
|
set_player_attr('can_access_trock_eyebridge', None)
|
||||||
set_player_attr('can_access_trock_front', None)
|
set_player_attr('can_access_trock_front', None)
|
||||||
set_player_attr('can_access_trock_big_chest', None)
|
set_player_attr('can_access_trock_big_chest', None)
|
||||||
@@ -136,13 +144,9 @@ class MultiWorld():
|
|||||||
for hk_option in Options.hollow_knight_options:
|
for hk_option in Options.hollow_knight_options:
|
||||||
set_player_attr(hk_option, False)
|
set_player_attr(hk_option, False)
|
||||||
|
|
||||||
self.worlds = []
|
# self.worlds = []
|
||||||
#for i in range(players):
|
# for i in range(players):
|
||||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
||||||
|
|
||||||
@property
|
|
||||||
def NOTCURSED(self): # not here to stay
|
|
||||||
return {player: not cursed for player, cursed in self.CURSED.items()}
|
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = secrets.SystemRandom()
|
self.random = secrets.SystemRandom()
|
||||||
@@ -726,7 +730,7 @@ class CollectionState(object):
|
|||||||
|
|
||||||
def can_retrieve_tablet(self, player:int) -> bool:
|
def can_retrieve_tablet(self, player:int) -> bool:
|
||||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
||||||
(self.world.swords[player] == "swordless" and
|
(self.world.swordless[player] and
|
||||||
self.has("Hammer", player)))
|
self.has("Hammer", player)))
|
||||||
|
|
||||||
def has_sword(self, player: int) -> bool:
|
def has_sword(self, player: int) -> bool:
|
||||||
@@ -747,7 +751,7 @@ class CollectionState(object):
|
|||||||
def can_melt_things(self, player: int) -> bool:
|
def can_melt_things(self, player: int) -> bool:
|
||||||
return self.has('Fire Rod', player) or \
|
return self.has('Fire Rod', player) or \
|
||||||
(self.has('Bombos', player) and
|
(self.has('Bombos', player) and
|
||||||
(self.world.swords[player] == "swordless" or
|
(self.world.swordless[player] or
|
||||||
self.has_sword(player)))
|
self.has_sword(player)))
|
||||||
|
|
||||||
def can_avoid_lasers(self, player: int) -> bool:
|
def can_avoid_lasers(self, player: int) -> bool:
|
||||||
@@ -1111,7 +1115,7 @@ class Location():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def hint_text(self):
|
def hint_text(self):
|
||||||
return getattr(self, "_hint_text", self.name)
|
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||||
|
|
||||||
class Item():
|
class Item():
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
@@ -1313,7 +1317,7 @@ class Spoiler(object):
|
|||||||
'dark_room_logic': self.world.dark_room_logic,
|
'dark_room_logic': self.world.dark_room_logic,
|
||||||
'mode': self.world.mode,
|
'mode': self.world.mode,
|
||||||
'retro': self.world.retro,
|
'retro': self.world.retro,
|
||||||
'weapons': self.world.swords,
|
'swordless': self.world.swordless,
|
||||||
'goal': self.world.goal,
|
'goal': self.world.goal,
|
||||||
'shuffle': self.world.shuffle,
|
'shuffle': self.world.shuffle,
|
||||||
'item_pool': self.world.difficulty,
|
'item_pool': self.world.difficulty,
|
||||||
@@ -1412,7 +1416,7 @@ class Spoiler(object):
|
|||||||
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||||
outfile.write('Retro: %s\n' %
|
outfile.write('Retro: %s\n' %
|
||||||
('Yes' if self.metadata['retro'][player] else 'No'))
|
('Yes' if self.metadata['retro'][player] else 'No'))
|
||||||
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
|
||||||
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||||
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
||||||
outfile.write("Pieces available for Triforce: %s\n" %
|
outfile.write("Pieces available for Triforce: %s\n" %
|
||||||
|
|||||||
@@ -311,12 +311,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
args['players'].sort()
|
args['players'].sort()
|
||||||
current_team = -1
|
current_team = -1
|
||||||
logger.info('Players:')
|
logger.info('Players:')
|
||||||
for team, slot, name in args['players']:
|
for network_player in args['players']:
|
||||||
if team != current_team:
|
if network_player.team != current_team:
|
||||||
logger.info(f' Team #{team + 1}')
|
logger.info(f' Team #{network_player.team + 1}')
|
||||||
current_team = team
|
current_team = network_player.team
|
||||||
logger.info(' %s (Player %d)' % (name, slot))
|
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||||
if args["datapackage_version"] > network_data_package["version"]:
|
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
|
||||||
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
|
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
|
||||||
await ctx.server_auth(args['password'])
|
await ctx.server_auth(args['password'])
|
||||||
|
|
||||||
@@ -356,7 +356,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
if msgs:
|
if msgs:
|
||||||
await ctx.send_msgs(msgs)
|
await ctx.send_msgs(msgs)
|
||||||
if ctx.finished_game:
|
if ctx.finished_game:
|
||||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": CLientStatus.CLIENT_GOAL}])
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
|
|
||||||
# Get the server side view of missing as of time of connecting.
|
# Get the server side view of missing as of time of connecting.
|
||||||
# This list is used to only send to the server what is reported as ACTUALLY Missing.
|
# This list is used to only send to the server what is reported as ACTUALLY Missing.
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _cmd_connect(self, address: str = "", name="") -> bool:
|
||||||
|
"""Connect to a MultiWorld Server"""
|
||||||
|
self.ctx.auth = name
|
||||||
|
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
||||||
|
|
||||||
|
|
||||||
class FactorioContext(CommonContext):
|
class FactorioContext(CommonContext):
|
||||||
command_processor = FactorioCommandProcessor
|
command_processor = FactorioCommandProcessor
|
||||||
@@ -76,7 +81,8 @@ async def game_watcher(ctx: FactorioContext):
|
|||||||
researches_done_file = os.path.join(script_folder, "research_done.json")
|
researches_done_file = os.path.join(script_folder, "research_done.json")
|
||||||
if os.path.exists(researches_done_file):
|
if os.path.exists(researches_done_file):
|
||||||
os.remove(researches_done_file)
|
os.remove(researches_done_file)
|
||||||
from worlds.factorio.Technologies import lookup_id_to_name, tech_table
|
from worlds.factorio.Technologies import lookup_id_to_name
|
||||||
|
bridge_counter = 0
|
||||||
try:
|
try:
|
||||||
while 1:
|
while 1:
|
||||||
if os.path.exists(researches_done_file):
|
if os.path.exists(researches_done_file):
|
||||||
@@ -92,8 +98,11 @@ async def game_watcher(ctx: FactorioContext):
|
|||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
research_logger.info("Did not find Factorio Bridge file.")
|
bridge_counter += 1
|
||||||
await asyncio.sleep(5)
|
if bridge_counter >= 60:
|
||||||
|
research_logger.info("Did not find Factorio Bridge file, waiting for mod to run.")
|
||||||
|
bridge_counter = 1
|
||||||
|
await asyncio.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
@@ -137,11 +146,12 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
if ctx.rcon_client:
|
if ctx.rcon_client:
|
||||||
while ctx.send_index < len(ctx.items_received):
|
while ctx.send_index < len(ctx.items_received):
|
||||||
item_id = ctx.items_received[ctx.send_index].item
|
item_id = ctx.items_received[ctx.send_index].item
|
||||||
item_name = lookup_id_to_name[item_id]
|
if item_id not in lookup_id_to_name:
|
||||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
|
logging.error(f"Cannot send unknown item ID: {item_id}")
|
||||||
response = ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
|
else:
|
||||||
if response:
|
item_name = lookup_id_to_name[item_id]
|
||||||
factorio_server_logger.info(response)
|
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
|
||||||
|
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
|
||||||
ctx.send_index += 1
|
ctx.send_index += 1
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -982,8 +982,8 @@ async def main():
|
|||||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||||
parser.add_argument('--founditems', default=False, action='store_true',
|
parser.add_argument('--founditems', default=False, action='store_true',
|
||||||
help='Show items found by other players for themselves.')
|
help='Show items found by other players for themselves.')
|
||||||
parser.add_argument('--disable_web_ui', default=False, action='store_true',
|
parser.add_argument('--web_ui', default=False, action='store_true',
|
||||||
help="Turn off emitting a webserver for the webbrowser based user interface.")
|
help="Emit a webserver for the webbrowser based user interface.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||||
if args.diff_file:
|
if args.diff_file:
|
||||||
@@ -1002,7 +1002,7 @@ async def main():
|
|||||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||||
|
|
||||||
port = None
|
port = None
|
||||||
if not args.disable_web_ui:
|
if args.web_ui:
|
||||||
# Find an available port on the host system to use for hosting the websocket server
|
# Find an available port on the host system to use for hosting the websocket server
|
||||||
while True:
|
while True:
|
||||||
port = randrange(49152, 65535)
|
port = randrange(49152, 65535)
|
||||||
@@ -1015,7 +1015,7 @@ async def main():
|
|||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password, args.founditems, port)
|
ctx = Context(args.snes, args.connect, args.password, args.founditems, port)
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
if not args.disable_web_ui:
|
if args.web_ui:
|
||||||
ui_socket = websockets.serve(functools.partial(websocket_server, ctx=ctx),
|
ui_socket = websockets.serve(functools.partial(websocket_server, ctx=ctx),
|
||||||
'localhost', port, ping_timeout=None, ping_interval=None)
|
'localhost', port, ping_timeout=None, ping_interval=None)
|
||||||
await ui_socket
|
await ui_socket
|
||||||
|
|||||||
204
Main.py
204
Main.py
@@ -10,8 +10,7 @@ import pickle
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
||||||
from worlds.alttp import ALttPLocation
|
from worlds.alttp.Items import ItemFactory, item_name_groups
|
||||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups
|
|
||||||
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
|
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
|
||||||
lookup_vanilla_location_to_entrance
|
lookup_vanilla_location_to_entrance
|
||||||
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||||
@@ -23,7 +22,7 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p
|
|||||||
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||||
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
||||||
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
|
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
|
||||||
from worlds.hk import gen_hollow, set_rules as set_hk_rules
|
from worlds.hk import gen_hollow
|
||||||
from worlds.hk import create_regions as hk_create_regions
|
from worlds.hk import create_regions as hk_create_regions
|
||||||
from worlds.factorio import gen_factorio, factorio_create_regions
|
from worlds.factorio import gen_factorio, factorio_create_regions
|
||||||
from worlds.factorio.Mod import generate_mod
|
from worlds.factorio.Mod import generate_mod
|
||||||
@@ -70,7 +69,7 @@ def main(args, seed=None):
|
|||||||
world.shuffle = args.shuffle.copy()
|
world.shuffle = args.shuffle.copy()
|
||||||
world.logic = args.logic.copy()
|
world.logic = args.logic.copy()
|
||||||
world.mode = args.mode.copy()
|
world.mode = args.mode.copy()
|
||||||
world.swords = args.swords.copy()
|
world.swordless = args.swordless.copy()
|
||||||
world.difficulty = args.difficulty.copy()
|
world.difficulty = args.difficulty.copy()
|
||||||
world.item_functionality = args.item_functionality.copy()
|
world.item_functionality = args.item_functionality.copy()
|
||||||
world.timer = args.timer.copy()
|
world.timer = args.timer.copy()
|
||||||
@@ -135,6 +134,8 @@ def main(args, seed=None):
|
|||||||
import Options
|
import Options
|
||||||
for hk_option in Options.hollow_knight_options:
|
for hk_option in Options.hollow_knight_options:
|
||||||
setattr(world, hk_option, getattr(args, hk_option, {}))
|
setattr(world, hk_option, getattr(args, hk_option, {}))
|
||||||
|
for factorio_option in Options.factorio_options:
|
||||||
|
setattr(world, factorio_option, getattr(args, factorio_option, {}))
|
||||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||||
|
|
||||||
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
|
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
|
||||||
@@ -490,7 +491,12 @@ def main(args, seed=None):
|
|||||||
for future in roms:
|
for future in roms:
|
||||||
rom_name = future.result()
|
rom_name = future.result()
|
||||||
rom_names.append(rom_name)
|
rom_names.append(rom_name)
|
||||||
minimum_versions = {"server": (0, 0, 2)}
|
client_versions = {}
|
||||||
|
minimum_versions = {"server": (0, 0, 3), "clients": client_versions}
|
||||||
|
games = {}
|
||||||
|
for slot in world.player_ids:
|
||||||
|
client_versions[slot] = (0, 0, 3)
|
||||||
|
games[slot] = world.game[slot]
|
||||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
||||||
slot, team, rom_name in rom_names}
|
slot, team, rom_name in rom_names}
|
||||||
|
|
||||||
@@ -498,24 +504,27 @@ def main(args, seed=None):
|
|||||||
for player, name in enumerate(team, 1):
|
for player, name in enumerate(team, 1):
|
||||||
if player not in world.alttp_player_ids:
|
if player not in world.alttp_player_ids:
|
||||||
connect_names[name] = (i, player)
|
connect_names[name] = (i, player)
|
||||||
multidata = zlib.compress(pickle.dumps({"names": parsed_names,
|
|
||||||
"connect_names": connect_names,
|
multidata = zlib.compress(pickle.dumps({
|
||||||
"remote_items": {player for player in range(1, world.players + 1) if
|
"games": games,
|
||||||
world.remote_items[player] or
|
"names": parsed_names,
|
||||||
world.game[player] != "A Link to the Past"},
|
"connect_names": connect_names,
|
||||||
"locations": {
|
"remote_items": {player for player in range(1, world.players + 1) if
|
||||||
(location.address, location.player):
|
world.remote_items[player] or
|
||||||
(location.item.code, location.item.player)
|
world.game[player] != "A Link to the Past"},
|
||||||
for location in world.get_filled_locations() if
|
"locations": {
|
||||||
type(location.address) is int},
|
(location.address, location.player):
|
||||||
"checks_in_area": checks_in_area,
|
(location.item.code, location.item.player)
|
||||||
"server_options": get_options()["server_options"],
|
for location in world.get_filled_locations() if
|
||||||
"er_hint_data": er_hint_data,
|
type(location.address) is int},
|
||||||
"precollected_items": precollected_items,
|
"checks_in_area": checks_in_area,
|
||||||
"version": tuple(_version_tuple),
|
"server_options": get_options()["server_options"],
|
||||||
"tags": ["AP"],
|
"er_hint_data": er_hint_data,
|
||||||
"minimum_versions": minimum_versions,
|
"precollected_items": precollected_items,
|
||||||
}), 9)
|
"version": tuple(_version_tuple),
|
||||||
|
"tags": ["AP"],
|
||||||
|
"minimum_versions": minimum_versions,
|
||||||
|
}), 9)
|
||||||
|
|
||||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||||
f.write(bytes([1])) # version of format
|
f.write(bytes([1])) # version of format
|
||||||
@@ -542,155 +551,8 @@ def main(args, seed=None):
|
|||||||
return world
|
return world
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def copy_world(world):
|
|
||||||
# ToDo: Not good yet
|
|
||||||
# delete now?
|
|
||||||
ret = MultiWorld(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.item_functionality, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
|
||||||
ret.teams = world.teams
|
|
||||||
ret.player_names = copy.deepcopy(world.player_names)
|
|
||||||
ret.remote_items = world.remote_items.copy()
|
|
||||||
ret.required_medallions = world.required_medallions.copy()
|
|
||||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
|
||||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
|
||||||
ret.powder_patch_required = world.powder_patch_required.copy()
|
|
||||||
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
|
|
||||||
ret.treasure_hunt_count = world.treasure_hunt_count.copy()
|
|
||||||
ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
|
|
||||||
ret.sewer_light_cone = world.sewer_light_cone.copy()
|
|
||||||
ret.light_world_light_cone = world.light_world_light_cone
|
|
||||||
ret.dark_world_light_cone = world.dark_world_light_cone
|
|
||||||
ret.seed = world.seed
|
|
||||||
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
|
|
||||||
ret.can_access_trock_front = world.can_access_trock_front.copy()
|
|
||||||
ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
|
|
||||||
ret.can_access_trock_middle = world.can_access_trock_middle.copy()
|
|
||||||
ret.can_take_damage = world.can_take_damage
|
|
||||||
ret.difficulty_requirements = world.difficulty_requirements.copy()
|
|
||||||
ret.fix_fake_world = world.fix_fake_world.copy()
|
|
||||||
ret.mapshuffle = world.mapshuffle.copy()
|
|
||||||
ret.compassshuffle = world.compassshuffle.copy()
|
|
||||||
ret.keyshuffle = world.keyshuffle.copy()
|
|
||||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
|
||||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
|
||||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
|
||||||
ret.open_pyramid = world.open_pyramid.copy()
|
|
||||||
ret.boss_shuffle = world.boss_shuffle.copy()
|
|
||||||
ret.enemy_shuffle = world.enemy_shuffle.copy()
|
|
||||||
ret.enemy_health = world.enemy_health.copy()
|
|
||||||
ret.enemy_damage = world.enemy_damage.copy()
|
|
||||||
ret.beemizer = world.beemizer.copy()
|
|
||||||
ret.timer = world.timer.copy()
|
|
||||||
ret.shufflepots = world.shufflepots.copy()
|
|
||||||
ret.shuffle_prizes = world.shuffle_prizes.copy()
|
|
||||||
ret.shop_shuffle = world.shop_shuffle.copy()
|
|
||||||
ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
|
|
||||||
ret.dark_room_logic = world.dark_room_logic.copy()
|
|
||||||
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
|
|
||||||
ret.game = world.game.copy()
|
|
||||||
ret.completion_condition = world.completion_condition.copy()
|
|
||||||
|
|
||||||
for player in world.alttp_player_ids:
|
|
||||||
if world.mode[player] != 'inverted':
|
|
||||||
create_regions(ret, player)
|
|
||||||
else:
|
|
||||||
create_inverted_regions(ret, player)
|
|
||||||
create_shops(ret, player)
|
|
||||||
create_dungeons(ret, player)
|
|
||||||
|
|
||||||
for player in world.hk_player_ids:
|
|
||||||
hk_create_regions(ret, player)
|
|
||||||
|
|
||||||
copy_dynamic_regions_and_locations(world, ret)
|
|
||||||
|
|
||||||
# copy bosses
|
|
||||||
for dungeon in world.dungeons:
|
|
||||||
for level, boss in dungeon.bosses.items():
|
|
||||||
ret.get_dungeon(dungeon.name, dungeon.player).bosses[level] = boss
|
|
||||||
|
|
||||||
for shop in world.shops:
|
|
||||||
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
|
|
||||||
copied_shop.inventory = copy.copy(shop.inventory)
|
|
||||||
|
|
||||||
# connect copied world
|
|
||||||
for region in world.regions:
|
|
||||||
copied_region = ret.get_region(region.name, region.player)
|
|
||||||
copied_region.is_light_world = region.is_light_world
|
|
||||||
copied_region.is_dark_world = region.is_dark_world
|
|
||||||
for exit in copied_region.exits:
|
|
||||||
old_connection = world.get_entrance(exit.name, exit.player).connected_region
|
|
||||||
exit.connect(ret.get_region(old_connection.name, old_connection.player))
|
|
||||||
|
|
||||||
# fill locations
|
|
||||||
for location in world.get_locations():
|
|
||||||
if location.item is not None:
|
|
||||||
item = Item(location.item.name, location.item.advancement, location.item.code, player = location.item.player)
|
|
||||||
ret.get_location(location.name, location.player).item = item
|
|
||||||
item.location = ret.get_location(location.name, location.player)
|
|
||||||
item.world = ret
|
|
||||||
item.type = location.item.type
|
|
||||||
item.game = location.item.game
|
|
||||||
|
|
||||||
if location.event:
|
|
||||||
ret.get_location(location.name, location.player).event = True
|
|
||||||
if location.locked:
|
|
||||||
ret.get_location(location.name, location.player).locked = True
|
|
||||||
|
|
||||||
|
|
||||||
# copy remaining itempool. No item in itempool should have an assigned location
|
|
||||||
for old_item in world.itempool:
|
|
||||||
item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player)
|
|
||||||
item.type = old_item.type
|
|
||||||
ret.itempool.append(item)
|
|
||||||
|
|
||||||
for old_item in world.precollected_items:
|
|
||||||
item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player)
|
|
||||||
item.type = old_item.type
|
|
||||||
ret.push_precollected(item)
|
|
||||||
|
|
||||||
# copy progress items in state
|
|
||||||
ret.state.prog_items = world.state.prog_items.copy()
|
|
||||||
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
|
||||||
|
|
||||||
for player in world.alttp_player_ids:
|
|
||||||
set_rules(ret, player)
|
|
||||||
|
|
||||||
for player in world.hk_player_ids:
|
|
||||||
set_hk_rules(ret, player)
|
|
||||||
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def copy_dynamic_regions_and_locations(world, ret):
|
|
||||||
for region in world.dynamic_regions:
|
|
||||||
new_reg = Region(region.name, region.type, region.hint_text, region.player)
|
|
||||||
ret.regions.append(new_reg)
|
|
||||||
ret.initialize_regions([new_reg])
|
|
||||||
ret.dynamic_regions.append(new_reg)
|
|
||||||
|
|
||||||
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
|
|
||||||
|
|
||||||
if region.shop:
|
|
||||||
new_reg.shop = region.shop.__class__(new_reg, region.shop.room_id, region.shop.shopkeeper_config,
|
|
||||||
region.shop.custom, region.shop.locked, region.shop.sram_offset)
|
|
||||||
ret.shops.append(new_reg.shop)
|
|
||||||
|
|
||||||
for location in world.dynamic_locations:
|
|
||||||
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player)
|
|
||||||
new_loc = ALttPLocation(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg)
|
|
||||||
# todo: this is potentially dangerous. later refactor so we
|
|
||||||
# can apply dynamic region rules on top of copied world like other rules
|
|
||||||
new_loc.access_rule = location.access_rule
|
|
||||||
new_loc.always_allow = location.always_allow
|
|
||||||
new_loc.item_rule = location.item_rule
|
|
||||||
new_reg.locations.append(new_loc)
|
|
||||||
|
|
||||||
ret.clear_location_cache()
|
|
||||||
|
|
||||||
|
|
||||||
def create_playthrough(world):
|
def create_playthrough(world):
|
||||||
"""Destructive to the world it is run on."""
|
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||||
# get locations containing progress items
|
# get locations containing progress items
|
||||||
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
|
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
|
||||||
state_cache = [None]
|
state_cache = [None]
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
|||||||
import Utils
|
import Utils
|
||||||
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||||
_version_tuple, restricted_loads, Version
|
_version_tuple, restricted_loads, Version
|
||||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode
|
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
lttp_console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
|
lttp_console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
|
||||||
@@ -110,7 +110,8 @@ class Context(Node):
|
|||||||
self.auto_saver_thread = None
|
self.auto_saver_thread = None
|
||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
self.tags = ['AP']
|
self.tags = ['AP']
|
||||||
self.minimum_client_versions: typing.Dict[typing.Tuple[int, int], Utils.Version] = {}
|
self.games = {}
|
||||||
|
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
||||||
|
|
||||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||||
with open(multidatapath, 'rb') as f:
|
with open(multidatapath, 'rb') as f:
|
||||||
@@ -119,21 +120,23 @@ class Context(Node):
|
|||||||
self._load(self._decompress(data), use_embedded_server_options)
|
self._load(self._decompress(data), use_embedded_server_options)
|
||||||
self.data_filename = multidatapath
|
self.data_filename = multidatapath
|
||||||
|
|
||||||
def _decompress(self, data: bytes) -> dict:
|
@staticmethod
|
||||||
|
def _decompress(data: bytes) -> dict:
|
||||||
format_version = data[0]
|
format_version = data[0]
|
||||||
if format_version != 1:
|
if format_version != 1:
|
||||||
raise Exception("Incompatible multidata.")
|
raise Exception("Incompatible multidata.")
|
||||||
return restricted_loads(zlib.decompress(data[1:]))
|
return restricted_loads(zlib.decompress(data[1:]))
|
||||||
|
|
||||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||||
|
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > Utils._version_tuple:
|
if mdata_ver > Utils._version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
|
raise RuntimeError(f"Supplied Multidata 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 {Utils._version_tuple}")
|
||||||
clients_ver = decoded_obj["minimum_versions"].get("clients", [])
|
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||||
self.minimum_client_versions = {}
|
self.minimum_client_versions = {}
|
||||||
for team, player, version in clients_ver:
|
for player, version in clients_ver.items():
|
||||||
self.minimum_client_versions[team, player] = Utils.Version(*version)
|
self.minimum_client_versions[player] = Utils.Version(*version)
|
||||||
|
|
||||||
for team, names in enumerate(decoded_obj['names']):
|
for team, names in enumerate(decoded_obj['names']):
|
||||||
for player, name in enumerate(names, 1):
|
for player, name in enumerate(names, 1):
|
||||||
@@ -144,12 +147,13 @@ class Context(Node):
|
|||||||
self.locations = decoded_obj['locations']
|
self.locations = decoded_obj['locations']
|
||||||
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
||||||
for player, loc_data in decoded_obj["er_hint_data"].items()}
|
for player, loc_data in decoded_obj["er_hint_data"].items()}
|
||||||
|
self.games = decoded_obj["games"]
|
||||||
if use_embedded_server_options:
|
if use_embedded_server_options:
|
||||||
server_options = decoded_obj.get("server_options", {})
|
server_options = decoded_obj.get("server_options", {})
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
||||||
def get_players_package(self):
|
def get_players_package(self):
|
||||||
return [(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||||
|
|
||||||
def _set_options(self, server_options: dict):
|
def _set_options(self, server_options: dict):
|
||||||
for key, value in server_options.items():
|
for key, value in server_options.items():
|
||||||
@@ -331,11 +335,14 @@ async def server(websocket, path, ctx: Context):
|
|||||||
ctx.endpoints.append(client)
|
ctx.endpoints.append(client)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info("Incoming")
|
if ctx.log_network:
|
||||||
|
logging.info("Incoming connection")
|
||||||
await on_client_connected(ctx, client)
|
await on_client_connected(ctx, client)
|
||||||
logging.info("Sent Room Info")
|
if ctx.log_network:
|
||||||
|
logging.info("Sent Room Info")
|
||||||
async for data in websocket:
|
async for data in websocket:
|
||||||
logging.info(data)
|
if ctx.log_network:
|
||||||
|
logging.info(f"Incoming message: {data}")
|
||||||
for msg in decode(data):
|
for msg in decode(data):
|
||||||
await process_client_cmd(ctx, client, msg)
|
await process_client_cmd(ctx, client, msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -350,7 +357,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
'cmd': 'RoomInfo',
|
'cmd': 'RoomInfo',
|
||||||
'password': ctx.password is not None,
|
'password': ctx.password is not None,
|
||||||
'players': [(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name)) for client
|
'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client
|
||||||
in ctx.endpoints if client.auth],
|
in ctx.endpoints if client.auth],
|
||||||
# tags are for additional features in the communication.
|
# tags are for additional features in the communication.
|
||||||
# Name them by feature or fork, as you feel is appropriate.
|
# Name them by feature or fork, as you feel is appropriate.
|
||||||
@@ -525,10 +532,15 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
|||||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||||
parts = []
|
parts = []
|
||||||
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
||||||
NetUtils.add_json_text(parts, " sent ")
|
if net_item.player == receiving_player:
|
||||||
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
NetUtils.add_json_text(parts, " found their ")
|
||||||
NetUtils.add_json_text(parts, " to ")
|
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
||||||
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
|
else:
|
||||||
|
NetUtils.add_json_text(parts, " sent ")
|
||||||
|
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
||||||
|
NetUtils.add_json_text(parts, " to ")
|
||||||
|
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
|
||||||
|
|
||||||
NetUtils.add_json_text(parts, " (")
|
NetUtils.add_json_text(parts, " (")
|
||||||
NetUtils.add_json_text(parts, net_item.location, type=NetUtils.JSONTypes.location_id)
|
NetUtils.add_json_text(parts, net_item.location, type=NetUtils.JSONTypes.location_id)
|
||||||
NetUtils.add_json_text(parts, ")")
|
NetUtils.add_json_text(parts, ")")
|
||||||
@@ -794,7 +806,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
locations = get_missing_checks(self.ctx, self.client)
|
locations = get_missing_checks(self.ctx, self.client)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Missing: {get_item_name_from_id(location)}\n' for location in locations]
|
texts = [f'Missing: {get_item_name_from_id(location)}' for location in locations]
|
||||||
texts.append(f"Found {len(locations)} missing location checks")
|
texts.append(f"Found {len(locations)} missing location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.ctx.notify_client_multiple(self.client, texts)
|
||||||
else:
|
else:
|
||||||
@@ -967,6 +979,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
errors.add('InvalidSlot')
|
errors.add('InvalidSlot')
|
||||||
else:
|
else:
|
||||||
team, slot = ctx.connect_names[args['name']]
|
team, slot = ctx.connect_names[args['name']]
|
||||||
|
game = ctx.games[slot]
|
||||||
|
if args['game'] != game:
|
||||||
|
errors.add('InvalidSlot')
|
||||||
# this can only ever be 0 or 1 elements
|
# this can only ever be 0 or 1 elements
|
||||||
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
|
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
|
||||||
if clients:
|
if clients:
|
||||||
@@ -982,13 +997,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
client.name = ctx.player_names[(team, slot)]
|
client.name = ctx.player_names[(team, slot)]
|
||||||
client.team = team
|
client.team = team
|
||||||
client.slot = slot
|
client.slot = slot
|
||||||
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
|
minver = ctx.minimum_client_versions[slot]
|
||||||
if minver > args['version']:
|
if minver > args['version']:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
|
|
||||||
if ctx.compatibility == 1 and "AP" not in args['tags']:
|
if ctx.compatibility == 1 and "AP" not in args['tags']:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
#only exact version match allowed
|
# only exact version match allowed
|
||||||
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
|
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
if errors:
|
if errors:
|
||||||
@@ -1004,7 +1019,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
"team": client.team, "slot": client.slot,
|
"team": client.team, "slot": client.slot,
|
||||||
"players": ctx.get_players_package(),
|
"players": ctx.get_players_package(),
|
||||||
"missing_locations": get_missing_checks(ctx, client),
|
"missing_locations": get_missing_checks(ctx, client),
|
||||||
"checked_locations": get_checked_checks(ctx, client)}]
|
"checked_locations": get_checked_checks(ctx, client),
|
||||||
|
}]
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
|
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
|
||||||
@@ -1309,6 +1325,7 @@ def parse_args() -> argparse.Namespace:
|
|||||||
#1 -> recommended for friendly racing, tries to block third party clients
|
#1 -> recommended for friendly racing, tries to block third party clients
|
||||||
#0 -> recommended for tournaments to force a level playing field, only allow an exact version match
|
#0 -> recommended for tournaments to force a level playing field, only allow an exact version match
|
||||||
""")
|
""")
|
||||||
|
parser.add_argument('--log_network', default=defaults["log_network"], action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
@@ -1345,7 +1362,7 @@ async def main(args: argparse.Namespace):
|
|||||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
||||||
args.auto_shutdown, args.compatibility)
|
args.auto_shutdown, args.compatibility)
|
||||||
|
ctx.log_network = args.log_network
|
||||||
data_filename = args.multidata
|
data_filename = args.multidata
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
106
Mystery.py
106
Mystery.py
@@ -198,11 +198,15 @@ def main(args=None, callback=ERmain):
|
|||||||
pre_rolled["original_seed_name"] = seedname
|
pre_rolled["original_seed_name"] = seedname
|
||||||
pre_rolled["pre_rolled"] = vars(settings).copy()
|
pre_rolled["pre_rolled"] = vars(settings).copy()
|
||||||
if "plando_items" in pre_rolled["pre_rolled"]:
|
if "plando_items" in pre_rolled["pre_rolled"]:
|
||||||
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in pre_rolled["pre_rolled"]["plando_items"]]
|
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in
|
||||||
|
pre_rolled["pre_rolled"]["plando_items"]]
|
||||||
if "plando_connections" in pre_rolled["pre_rolled"]:
|
if "plando_connections" in pre_rolled["pre_rolled"]:
|
||||||
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in pre_rolled["pre_rolled"]["plando_connections"]]
|
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in
|
||||||
|
pre_rolled["pre_rolled"][
|
||||||
|
"plando_connections"]]
|
||||||
|
|
||||||
with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
|
with open(os.path.join(args.outputpath if args.outputpath else ".",
|
||||||
|
f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
|
||||||
yaml.dump(pre_rolled, f)
|
yaml.dump(pre_rolled, f)
|
||||||
for k, v in vars(settings).items():
|
for k, v in vars(settings).items():
|
||||||
if v is not None:
|
if v is not None:
|
||||||
@@ -294,7 +298,8 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
|||||||
name_counter[name] += 1
|
name_counter[name] += 1
|
||||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||||
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
|
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
|
||||||
NUMBER=(name_counter[name] if name_counter[name] > 1 else ''),
|
NUMBER=(name_counter[name] if name_counter[
|
||||||
|
name] > 1 else ''),
|
||||||
player=player,
|
player=player,
|
||||||
PLAYER=(player if player > 1 else '')))
|
PLAYER=(player if player > 1 else '')))
|
||||||
return new_name.strip().replace(' ', '_')[:16]
|
return new_name.strip().replace(' ', '_')[:16]
|
||||||
@@ -315,17 +320,44 @@ available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if leve
|
|||||||
boss_shuffle_options = {None: 'none',
|
boss_shuffle_options = {None: 'none',
|
||||||
'none': 'none',
|
'none': 'none',
|
||||||
'basic': 'basic',
|
'basic': 'basic',
|
||||||
'normal': 'normal',
|
'full': 'full',
|
||||||
'chaos': 'chaos',
|
'chaos': 'chaos',
|
||||||
'singularity': 'singularity'
|
'singularity': 'singularity'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goals = {
|
||||||
|
'ganon': 'ganon',
|
||||||
|
'crystals': 'crystals',
|
||||||
|
'bosses': 'bosses',
|
||||||
|
'pedestal': 'pedestal',
|
||||||
|
'ganon_pedestal': 'ganonpedestal',
|
||||||
|
'triforce_hunt': 'triforcehunt',
|
||||||
|
'local_triforce_hunt': 'localtriforcehunt',
|
||||||
|
'ganon_triforce_hunt': 'ganontriforcehunt',
|
||||||
|
'local_ganon_triforce_hunt': 'localganontriforcehunt',
|
||||||
|
'ice_rod_hunt': 'icerodhunt',
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove sometime before 1.0.0, warn before
|
||||||
|
legacy_boss_shuffle_options = {
|
||||||
|
# legacy, will go away:
|
||||||
|
'simple': 'basic',
|
||||||
|
'random': 'full',
|
||||||
|
'normal': 'full'
|
||||||
|
}
|
||||||
|
|
||||||
|
legacy_goals = {
|
||||||
|
'dungeons': 'bosses',
|
||||||
|
'fast_ganon': 'crystals',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
||||||
"""Roll a percentage chance.
|
"""Roll a percentage chance.
|
||||||
percentage is expected to be in range [0, 100]"""
|
percentage is expected to be in range [0, 100]"""
|
||||||
return random.random() < (float(percentage) / 100)
|
return random.random() < (float(percentage) / 100)
|
||||||
|
|
||||||
|
|
||||||
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
|
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
|
||||||
logging.debug(f'Applying {new_weights}')
|
logging.debug(f'Applying {new_weights}')
|
||||||
new_options = set(new_weights) - set(weights)
|
new_options = set(new_weights) - set(weights)
|
||||||
@@ -337,6 +369,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
|||||||
f'This is probably in error.')
|
f'This is probably in error.')
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def roll_linked_options(weights: dict) -> dict:
|
def roll_linked_options(weights: dict) -> dict:
|
||||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||||
for option_set in weights["linked_options"]:
|
for option_set in weights["linked_options"]:
|
||||||
@@ -349,7 +382,8 @@ def roll_linked_options(weights: dict) -> dict:
|
|||||||
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
|
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
|
||||||
if "rom_options" in option_set:
|
if "rom_options" in option_set:
|
||||||
rom_weights = weights.get("rom", dict())
|
rom_weights = weights.get("rom", dict())
|
||||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", option_set["name"])
|
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
|
||||||
|
option_set["name"])
|
||||||
weights["rom"] = rom_weights
|
weights["rom"] = rom_weights
|
||||||
else:
|
else:
|
||||||
logging.debug(f"linked option {option_set['name']} skipped.")
|
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||||
@@ -358,10 +392,11 @@ def roll_linked_options(weights: dict) -> dict:
|
|||||||
f"Please fix your linked option.") from e
|
f"Please fix your linked option.") from e
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def roll_triggers(weights: dict) -> dict:
|
def roll_triggers(weights: dict) -> dict:
|
||||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||||
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
|
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
|
||||||
for option_set in weights["triggers"]:
|
for i, option_set in enumerate(weights["triggers"]):
|
||||||
try:
|
try:
|
||||||
key = get_choice("option_name", option_set)
|
key = get_choice("option_name", option_set)
|
||||||
if key not in weights:
|
if key not in weights:
|
||||||
@@ -373,18 +408,25 @@ def roll_triggers(weights: dict) -> dict:
|
|||||||
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
||||||
if "options" in option_set:
|
if "options" in option_set:
|
||||||
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
|
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
|
||||||
|
|
||||||
if "rom_options" in option_set:
|
if "rom_options" in option_set:
|
||||||
rom_weights = weights.get("rom", dict())
|
rom_weights = weights.get("rom", dict())
|
||||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom", option_set["option_name"])
|
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
|
||||||
|
option_set["option_name"])
|
||||||
weights["rom"] = rom_weights
|
weights["rom"] = rom_weights
|
||||||
weights[key] = result
|
weights[key] = result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"A trigger is destroyed. "
|
raise ValueError(f"Your trigger number {i+1} is destroyed. "
|
||||||
f"Please fix your triggers.") from e
|
f"Please fix your triggers.") from e
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
||||||
|
if boss_shuffle in legacy_boss_shuffle_options:
|
||||||
|
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
||||||
|
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
|
||||||
|
f"please use {new_boss_shuffle} instead")
|
||||||
|
return new_boss_shuffle
|
||||||
if boss_shuffle in boss_shuffle_options:
|
if boss_shuffle in boss_shuffle_options:
|
||||||
return boss_shuffle_options[boss_shuffle]
|
return boss_shuffle_options[boss_shuffle]
|
||||||
elif "bosses" in plando_options:
|
elif "bosses" in plando_options:
|
||||||
@@ -392,6 +434,10 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
|||||||
remainder_shuffle = "none" # vanilla
|
remainder_shuffle = "none" # vanilla
|
||||||
bosses = []
|
bosses = []
|
||||||
for boss in options:
|
for boss in options:
|
||||||
|
if boss in legacy_boss_shuffle_options:
|
||||||
|
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
||||||
|
logging.warning(f"Boss shuffle {boss} is deprecated, "
|
||||||
|
f"please use {remainder_shuffle} instead")
|
||||||
if boss in boss_shuffle_options:
|
if boss in boss_shuffle_options:
|
||||||
remainder_shuffle = boss_shuffle_options[boss]
|
remainder_shuffle = boss_shuffle_options[boss]
|
||||||
elif "-" in boss:
|
elif "-" in boss:
|
||||||
@@ -419,7 +465,7 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
|||||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||||
|
|
||||||
|
|
||||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses", ))):
|
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
|
||||||
if "pre_rolled" in weights:
|
if "pre_rolled" in weights:
|
||||||
pre_rolled = weights["pre_rolled"]
|
pre_rolled = weights["pre_rolled"]
|
||||||
|
|
||||||
@@ -435,7 +481,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
if "plando_connections" in pre_rolled:
|
if "plando_connections" in pre_rolled:
|
||||||
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
|
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
|
||||||
connection["exit"],
|
connection["exit"],
|
||||||
connection["direction"]) for connection in pre_rolled["plando_connections"]]
|
connection["direction"]) for connection in
|
||||||
|
pre_rolled["plando_connections"]]
|
||||||
if "connections" not in plando_options and pre_rolled["plando_connections"]:
|
if "connections" not in plando_options and pre_rolled["plando_connections"]:
|
||||||
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
|
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
|
||||||
|
|
||||||
@@ -486,11 +533,22 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
for option_name, option in Options.hollow_knight_options.items():
|
for option_name, option in Options.hollow_knight_options.items():
|
||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
|
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
|
||||||
elif ret.game == "Factorio":
|
elif ret.game == "Factorio":
|
||||||
pass
|
for option_name, option in Options.factorio_options.items():
|
||||||
|
if option_name in weights:
|
||||||
|
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||||
|
else:
|
||||||
|
setattr(ret, option_name, option.from_any(option.default))
|
||||||
|
elif ret.game == "Minecraft":
|
||||||
|
for option_name, option in Options.minecraft_options.items():
|
||||||
|
if option_name in weights:
|
||||||
|
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||||
|
else:
|
||||||
|
setattr(ret, option_name, option.from_any(option.default))
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unsupported game {ret.game}")
|
raise Exception(f"Unsupported game {ret.game}")
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||||
glitches_required = get_choice('glitches_required', weights)
|
glitches_required = get_choice('glitches_required', weights)
|
||||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
|
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
|
||||||
@@ -533,17 +591,11 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||||
|
|
||||||
goal = get_choice('goals', weights, 'ganon')
|
goal = get_choice('goals', weights, 'ganon')
|
||||||
ret.goal = {'ganon': 'ganon',
|
|
||||||
'crystals': 'crystals',
|
if goal in legacy_goals:
|
||||||
'bosses': 'bosses',
|
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
|
||||||
'pedestal': 'pedestal',
|
goal = legacy_goals[goal]
|
||||||
'ganon_pedestal': 'ganonpedestal',
|
ret.goal = goals[goal]
|
||||||
'triforce_hunt': 'triforcehunt',
|
|
||||||
'local_triforce_hunt': 'localtriforcehunt',
|
|
||||||
'ganon_triforce_hunt': 'ganontriforcehunt',
|
|
||||||
'local_ganon_triforce_hunt': 'localganontriforcehunt',
|
|
||||||
'ice_rod_hunt': 'icerodhunt'
|
|
||||||
}[goal]
|
|
||||||
|
|
||||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||||
# fast ganon + ganon at hole
|
# fast ganon + ganon at hole
|
||||||
@@ -587,11 +639,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
|
|
||||||
ret.hints = get_choice('hints', weights)
|
ret.hints = get_choice('hints', weights)
|
||||||
|
|
||||||
ret.swords = {'randomized': 'random',
|
ret.swordless = get_choice('swordless', weights, False)
|
||||||
'assured': 'assured',
|
|
||||||
'vanilla': 'vanilla',
|
|
||||||
'swordless': 'swordless'
|
|
||||||
}[get_choice('weapons', weights, 'assured')]
|
|
||||||
|
|
||||||
ret.difficulty = get_choice('item_pool', weights)
|
ret.difficulty = get_choice('item_pool', weights)
|
||||||
|
|
||||||
@@ -602,6 +650,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
|
|
||||||
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
||||||
|
|
||||||
|
|
||||||
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||||
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
||||||
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
||||||
@@ -772,5 +821,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
ret.quickswap = True
|
ret.quickswap = True
|
||||||
ret.sprite = "Link"
|
ret.sprite = "Link"
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class Node:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.endpoints = []
|
self.endpoints = []
|
||||||
super(Node, self).__init__()
|
super(Node, self).__init__()
|
||||||
|
self.log_network = 0
|
||||||
|
|
||||||
def broadcast_all(self, msgs):
|
def broadcast_all(self, msgs):
|
||||||
msgs = self.dumper(msgs)
|
msgs = self.dumper(msgs)
|
||||||
@@ -114,6 +115,9 @@ class Node:
|
|||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
|
else:
|
||||||
|
if self.log_network:
|
||||||
|
logging.info(f"Outgoing message: {msg}")
|
||||||
|
|
||||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
|
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
|
||||||
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
|
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
|
||||||
@@ -123,6 +127,9 @@ class Node:
|
|||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
logging.exception("Exception during send_msgs")
|
logging.exception("Exception during send_msgs")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
|
else:
|
||||||
|
if self.log_network:
|
||||||
|
logging.info(f"Outgoing message: {msg}")
|
||||||
|
|
||||||
async def disconnect(self, endpoint):
|
async def disconnect(self, endpoint):
|
||||||
if endpoint in self.endpoints:
|
if endpoint in self.endpoints:
|
||||||
|
|||||||
63
Options.py
63
Options.py
@@ -23,6 +23,7 @@ class AssembleOptions(type):
|
|||||||
class Option(metaclass=AssembleOptions):
|
class Option(metaclass=AssembleOptions):
|
||||||
value: int
|
value: int
|
||||||
name_lookup: typing.Dict[int, str]
|
name_lookup: typing.Dict[int, str]
|
||||||
|
default = 0
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.get_option_name()})"
|
return f"{self.__class__.__name__}({self.get_option_name()})"
|
||||||
@@ -47,6 +48,7 @@ class Option(metaclass=AssembleOptions):
|
|||||||
class Toggle(Option):
|
class Toggle(Option):
|
||||||
option_false = 0
|
option_false = 0
|
||||||
option_true = 1
|
option_true = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
def __init__(self, value: int):
|
def __init__(self, value: int):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -86,6 +88,7 @@ class Toggle(Option):
|
|||||||
def get_option_name(self):
|
def get_option_name(self):
|
||||||
return bool(self.value)
|
return bool(self.value)
|
||||||
|
|
||||||
|
|
||||||
class Choice(Option):
|
class Choice(Option):
|
||||||
def __init__(self, value: int):
|
def __init__(self, value: int):
|
||||||
self.value: int = value
|
self.value: int = value
|
||||||
@@ -101,7 +104,9 @@ class Choice(Option):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
return cls.from_text(data)
|
if type(data) == int and data in cls.options.values():
|
||||||
|
return cls(data)
|
||||||
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
|
|
||||||
class Logic(Choice):
|
class Logic(Choice):
|
||||||
@@ -231,7 +236,61 @@ hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
|
|||||||
"SHADESKIPS": Toggle,
|
"SHADESKIPS": Toggle,
|
||||||
}
|
}
|
||||||
|
|
||||||
hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options, **hollow_knight_skip_options}
|
hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options, **hollow_knight_skip_options}
|
||||||
|
|
||||||
|
|
||||||
|
class MaxSciencePack(Choice):
|
||||||
|
option_automation_science_pack = 0
|
||||||
|
option_logistic_science_pack = 1
|
||||||
|
option_military_science_pack = 2
|
||||||
|
option_chemical_science_pack = 3
|
||||||
|
option_production_science_pack = 4
|
||||||
|
option_utility_science_pack = 5
|
||||||
|
option_space_science_pack = 6
|
||||||
|
default = 6
|
||||||
|
|
||||||
|
def get_allowed_packs(self):
|
||||||
|
return {option.replace("_", "-") for option, value in self.options.items()
|
||||||
|
if value <= self.value}
|
||||||
|
|
||||||
|
|
||||||
|
class TechCost(Choice):
|
||||||
|
option_very_easy = 0
|
||||||
|
option_easy = 1
|
||||||
|
option_kind = 2
|
||||||
|
option_normal = 3
|
||||||
|
option_hard = 4
|
||||||
|
option_very_hard = 5
|
||||||
|
option_insane = 6
|
||||||
|
default = 3
|
||||||
|
|
||||||
|
class FreeSamples(Choice):
|
||||||
|
option_none = 0
|
||||||
|
option_single_craft = 1
|
||||||
|
option_half_stack = 2
|
||||||
|
option_stack = 3
|
||||||
|
default = 3
|
||||||
|
|
||||||
|
class TechTreeLayout(Choice):
|
||||||
|
option_single = 0
|
||||||
|
option_small_diamonds = 1
|
||||||
|
option_medium_diamonds = 2
|
||||||
|
option_pyramid = 3
|
||||||
|
option_funnel = 4
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
class Visibility(Choice):
|
||||||
|
option_none = 0
|
||||||
|
option_sending = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
|
||||||
|
"tech_tree_layout": TechTreeLayout,
|
||||||
|
"tech_cost": TechCost,
|
||||||
|
"free_samples": FreeSamples,
|
||||||
|
"visibility": Visibility}
|
||||||
|
|
||||||
|
minecraft_options: typing.Dict[str, type(Option)] = {}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# [Archipelago](https://archipelago.gg)  | [Install](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
# [Archipelago](https://archipelago.gg)  | [Install](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
|
||||||
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself.
|
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself.
|
||||||
|
|
||||||
Currently, the following games are supported:
|
Currently, the following games are supported:
|
||||||
* The Legend of Zelda: A Link to the Past
|
* The Legend of Zelda: A Link to the Past
|
||||||
|
* Factorio (Alpha Status)
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
||||||
Downloads can be found at [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases), including compiled
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
windows binaries.
|
windows binaries.
|
||||||
|
|
||||||
## History
|
## History
|
||||||
@@ -25,7 +26,7 @@ We recognize that there is a strong community of incredibly smart people that ha
|
|||||||
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
||||||
|
|
||||||
## Running Archipelago
|
## Running Archipelago
|
||||||
For most people all you need to do is head over to the [releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page then download and run the appropriate installer. The installers function on Windows only.
|
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
|
||||||
|
|
||||||
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).
|
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).
|
||||||
|
|
||||||
|
|||||||
13
Utils.py
13
Utils.py
@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
|
|||||||
minor: int
|
minor: int
|
||||||
build: int
|
build: int
|
||||||
|
|
||||||
__version__ = "0.0.2"
|
__version__ = "0.0.3"
|
||||||
_version_tuple = tuplize_version(__version__)
|
_version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
@@ -84,9 +84,13 @@ def local_path(*path):
|
|||||||
# cx_Freeze
|
# cx_Freeze
|
||||||
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||||
else:
|
else:
|
||||||
# we are running in a normal Python environment
|
|
||||||
import __main__
|
import __main__
|
||||||
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
if hasattr(__main__, "__file__"):
|
||||||
|
# we are running in a normal Python environment
|
||||||
|
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
||||||
|
else:
|
||||||
|
# pray
|
||||||
|
local_path.cached_path = os.path.abspath(".")
|
||||||
|
|
||||||
return os.path.join(local_path.cached_path, *path)
|
return os.path.join(local_path.cached_path, *path)
|
||||||
|
|
||||||
@@ -190,6 +194,7 @@ def get_default_options() -> dict:
|
|||||||
"remaining_mode": "goal",
|
"remaining_mode": "goal",
|
||||||
"auto_shutdown": 0,
|
"auto_shutdown": 0,
|
||||||
"compatibility": 2,
|
"compatibility": 2,
|
||||||
|
"log_network": 0
|
||||||
},
|
},
|
||||||
"multi_mystery_options": {
|
"multi_mystery_options": {
|
||||||
"teams": 1,
|
"teams": 1,
|
||||||
@@ -385,7 +390,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
def find_class(self, module, name):
|
def find_class(self, module, name):
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
return getattr(builtins, name)
|
return getattr(builtins, name)
|
||||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus"}:
|
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
||||||
import NetUtils
|
import NetUtils
|
||||||
return getattr(NetUtils, name)
|
return getattr(NetUtils, name)
|
||||||
# Forbid everything else.
|
# Forbid everything else.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ app.config["PONY"] = {
|
|||||||
app.config["MAX_ROLL"] = 20
|
app.config["MAX_ROLL"] = 20
|
||||||
app.config["CACHE_TYPE"] = "simple"
|
app.config["CACHE_TYPE"] = "simple"
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
|
app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||||
|
|
||||||
app.autoversion = True
|
app.autoversion = True
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import socket
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import zlib
|
import pickle
|
||||||
|
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||||
from Utils import get_public_ipv4, get_public_ipv6, parse_yaml
|
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
|
||||||
|
|
||||||
|
|
||||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||||
@@ -27,7 +27,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
|||||||
self.ctx.save()
|
self.ctx.save()
|
||||||
self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}")
|
self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}")
|
||||||
return True
|
return True
|
||||||
elif platform.lower().startswith("y"): # youtube
|
elif platform.lower().startswith("y"): # youtube
|
||||||
self.ctx.video[self.client.team, self.client.slot] = "Youtube", user
|
self.ctx.video[self.client.team, self.client.slot] = "Youtube", user
|
||||||
self.ctx.save()
|
self.ctx.save()
|
||||||
self.output(f"Registered Youtube Stream for {user}")
|
self.output(f"Registered Youtube Stream for {user}")
|
||||||
@@ -81,16 +81,16 @@ class WebHostContext(Context):
|
|||||||
def init_save(self, enabled: bool = True):
|
def init_save(self, enabled: bool = True):
|
||||||
self.saving = enabled
|
self.saving = enabled
|
||||||
if self.saving:
|
if self.saving:
|
||||||
existing_savegame = Room.get(id=self.room_id).multisave
|
savegame_data = Room.get(id=self.room_id).multisave
|
||||||
if existing_savegame:
|
if savegame_data:
|
||||||
self.set_save(existing_savegame)
|
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||||
self._start_async_saving()
|
self._start_async_saving()
|
||||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||||
|
|
||||||
@db_session
|
@db_session
|
||||||
def _save(self, exit_save:bool = False) -> bool:
|
def _save(self, exit_save:bool = False) -> bool:
|
||||||
room = Room.get(id=self.room_id)
|
room = Room.get(id=self.room_id)
|
||||||
room.multisave = self.get_save()
|
room.multisave = pickle.dumps(self.get_save())
|
||||||
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
||||||
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
||||||
room.last_activity = datetime.utcnow()
|
room.last_activity = datetime.utcnow()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def download_patch(room_id, patch_id):
|
|||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
last_port = room.last_port
|
last_port = room.last_port
|
||||||
|
|
||||||
patch_data = update_patch_data(patch.data, server=f"{app.config['HOSTNAME']}:{last_port}")
|
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||||
patch_data = io.BytesIO(patch_data)
|
patch_data = io.BytesIO(patch_data)
|
||||||
|
|
||||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"
|
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{% macro list_patches_room(room) %}
|
{% macro list_patches_room(room) %}
|
||||||
{% if room.seed.patches %}
|
{% if room.seed.patches %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for patch in patches|list|sort(attribute="player") %}
|
{% for patch in room.seed.patches|list|sort(attribute="player_id") %}
|
||||||
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
||||||
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from uuid import UUID
|
|||||||
from worlds.alttp import Items, Regions
|
from worlds.alttp import Items, Regions
|
||||||
from WebHostLib import app, cache, Room
|
from WebHostLib import app, cache, Room
|
||||||
from NetUtils import Hint
|
from NetUtils import Hint
|
||||||
|
from Utils import restricted_loads
|
||||||
|
|
||||||
|
|
||||||
def get_id(item_name):
|
def get_id(item_name):
|
||||||
@@ -253,7 +254,7 @@ for item_name, data in Items.item_table.items():
|
|||||||
big_key_ids[area] = data[2]
|
big_key_ids[area] = data[2]
|
||||||
ids_big_key[data[2]] = area
|
ids_big_key[data[2]] = area
|
||||||
|
|
||||||
from MultiServer import get_item_name_from_id
|
from MultiServer import get_item_name_from_id, Context
|
||||||
|
|
||||||
|
|
||||||
def attribute_item(inventory, team, recipient, item):
|
def attribute_item(inventory, team, recipient, item):
|
||||||
@@ -295,9 +296,9 @@ def get_static_room_data(room: Room):
|
|||||||
result = _multidata_cache.get(room.seed.id, None)
|
result = _multidata_cache.get(room.seed.id, None)
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
multidata = room.seed.multidata
|
multidata = Context._decompress(room.seed.multidata)
|
||||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||||
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
|
locations = multidata['locations']
|
||||||
names = multidata["names"]
|
names = multidata["names"]
|
||||||
seed_checks_in_area = checks_in_area.copy()
|
seed_checks_in_area = checks_in_area.copy()
|
||||||
|
|
||||||
@@ -308,30 +309,24 @@ def get_static_room_data(room: Room):
|
|||||||
for area, checks in key_only_locations.items():
|
for area, checks in key_only_locations.items():
|
||||||
seed_checks_in_area[area] += len(checks)
|
seed_checks_in_area[area] += len(checks)
|
||||||
seed_checks_in_area["Total"] = 249
|
seed_checks_in_area["Total"] = 249
|
||||||
if "checks_in_area" not in multidata:
|
|
||||||
player_checks_in_area = {playernumber: (seed_checks_in_area if use_door_tracker and
|
|
||||||
(0x140031, playernumber) in locations else checks_in_area)
|
|
||||||
for playernumber in range(1, len(names[0]) + 1)}
|
|
||||||
player_location_to_area = {playernumber: location_to_area
|
|
||||||
for playernumber in range(1, len(names[0]) + 1)}
|
|
||||||
|
|
||||||
else:
|
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
|
||||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][f'{playernumber}'][areaname])
|
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||||
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
|
for areaname in ordered_areas}
|
||||||
for areaname in ordered_areas}
|
for playernumber in range(1, len(names[0]) + 1)}
|
||||||
for playernumber in range(1, len(names[0]) + 1)}
|
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
||||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
|
for playernumber in range(1, len(names[0]) + 1)}
|
||||||
for playernumber in range(1, len(names[0]) + 1)}
|
|
||||||
|
|
||||||
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||||
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||||
for _, (item_id, item_player) in multidata["locations"]:
|
for _, (item_id, item_player) in locations.items():
|
||||||
if item_id in ids_big_key:
|
if item_id in ids_big_key:
|
||||||
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
||||||
if item_id in ids_small_key:
|
if item_id in ids_small_key:
|
||||||
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
||||||
|
|
||||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations
|
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||||
|
player_big_key_locations, player_small_key_locations, multidata["precollected_items"]
|
||||||
_multidata_cache[room.seed.id] = result
|
_multidata_cache[room.seed.id] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -348,7 +343,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Collect seed information and pare it down to a single player
|
# Collect seed information and pare it down to a single player
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
|
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, precollected_items = get_static_room_data(room)
|
||||||
player_name = names[tracked_team][tracked_player - 1]
|
player_name = names[tracked_team][tracked_player - 1]
|
||||||
seed_checks_in_area = seed_checks_in_area[tracked_player]
|
seed_checks_in_area = seed_checks_in_area[tracked_player]
|
||||||
location_to_area = player_location_to_area[tracked_player]
|
location_to_area = player_location_to_area[tracked_player]
|
||||||
@@ -356,13 +351,18 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
|||||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||||
|
|
||||||
# Add starting items to inventory
|
# Add starting items to inventory
|
||||||
starting_items = room.seed.multidata.get("precollected_items", None)[tracked_player - 1]
|
starting_items = precollected_items[tracked_player - 1]
|
||||||
if starting_items:
|
if starting_items:
|
||||||
for item_id in starting_items:
|
for item_id in starting_items:
|
||||||
attribute_item_solo(inventory, item_id)
|
attribute_item_solo(inventory, item_id)
|
||||||
|
|
||||||
|
if room.multisave:
|
||||||
|
multisave = restricted_loads(room.multisave)
|
||||||
|
else:
|
||||||
|
multisave = {}
|
||||||
|
|
||||||
# Add items to player inventory
|
# Add items to player inventory
|
||||||
for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}):
|
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}):
|
||||||
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
|
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
|
||||||
# Skip teams and players not matching the request
|
# Skip teams and players not matching the request
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
|||||||
checks_done["Total"] += 1
|
checks_done["Total"] += 1
|
||||||
|
|
||||||
# Note the presence of the triforce item
|
# Note the presence of the triforce item
|
||||||
for (ms_team, ms_player), game_state in room.multisave.get("client_game_state", []):
|
for (ms_team, ms_player), game_state in multisave.get("client_game_state", []):
|
||||||
# Skip teams and players not matching the request
|
# Skip teams and players not matching the request
|
||||||
if ms_team != tracked_team or ms_player != tracked_player:
|
if ms_team != tracked_team or ms_player != tracked_player:
|
||||||
continue
|
continue
|
||||||
@@ -484,7 +484,8 @@ def getTracker(tracker: UUID):
|
|||||||
room = Room.get(tracker=tracker)
|
room = Room.get(tracker=tracker)
|
||||||
if not room:
|
if not room:
|
||||||
abort(404)
|
abort(404)
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
|
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
|
||||||
|
player_small_key_locations, precollected_items = get_static_room_data(room)
|
||||||
|
|
||||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||||
for teamnumber, team in enumerate(names)}
|
for teamnumber, team in enumerate(names)}
|
||||||
@@ -492,14 +493,18 @@ def getTracker(tracker: UUID):
|
|||||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||||
for playernumber in range(1, len(team) + 1)}
|
for playernumber in range(1, len(team) + 1)}
|
||||||
for teamnumber, team in enumerate(names)}
|
for teamnumber, team in enumerate(names)}
|
||||||
precollected_items = room.seed.multidata.get("precollected_items", None)
|
|
||||||
hints = {team: set() for team in range(len(names))}
|
hints = {team: set() for team in range(len(names))}
|
||||||
if "hints" in room.multisave:
|
if room.multisave:
|
||||||
for key, hintdata in room.multisave["hints"]:
|
multisave = restricted_loads(room.multisave)
|
||||||
|
else:
|
||||||
|
multisave = {}
|
||||||
|
if "hints" in multisave:
|
||||||
|
for key, hintdata in multisave["hints"]:
|
||||||
for hint in hintdata:
|
for hint in hintdata:
|
||||||
hints[key[0]].add(Hint(*hint))
|
hints[key[0]].add(Hint(*hint))
|
||||||
|
|
||||||
for (team, player), locations_checked in room.multisave.get("location_checks", {}):
|
for (team, player), locations_checked in multisave.get("location_checks", {}):
|
||||||
if precollected_items:
|
if precollected_items:
|
||||||
precollected = precollected_items[player - 1]
|
precollected = precollected_items[player - 1]
|
||||||
for item_id in precollected:
|
for item_id in precollected:
|
||||||
@@ -513,7 +518,7 @@ def getTracker(tracker: UUID):
|
|||||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||||
checks_done[team][player]["Total"] += 1
|
checks_done[team][player]["Total"] += 1
|
||||||
|
|
||||||
for (team, player), game_state in room.multisave.get("client_game_state", []):
|
for (team, player), game_state in multisave.get("client_game_state", []):
|
||||||
if game_state:
|
if game_state:
|
||||||
inventory[team][player][106] = 1 # Triforce
|
inventory[team][player][106] = 1 # Triforce
|
||||||
|
|
||||||
@@ -525,7 +530,7 @@ def getTracker(tracker: UUID):
|
|||||||
|
|
||||||
activity_timers = {}
|
activity_timers = {}
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.utcnow()
|
||||||
for (team, player), timestamp in room.multisave.get("client_activity_timers", []):
|
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||||
|
|
||||||
player_names = {}
|
player_names = {}
|
||||||
@@ -533,12 +538,12 @@ def getTracker(tracker: UUID):
|
|||||||
for player, name in enumerate(names, 1):
|
for player, name in enumerate(names, 1):
|
||||||
player_names[(team, player)] = name
|
player_names[(team, player)] = name
|
||||||
long_player_names = player_names.copy()
|
long_player_names = player_names.copy()
|
||||||
for (team, player), alias in room.multisave.get("name_aliases", []):
|
for (team, player), alias in multisave.get("name_aliases", []):
|
||||||
player_names[(team, player)] = alias
|
player_names[(team, player)] = alias
|
||||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||||
|
|
||||||
video = {}
|
video = {}
|
||||||
for (team, player), data in room.multisave.get("video", []):
|
for (team, player), data in multisave.get("video", []):
|
||||||
video[(team, player)] = data
|
video[(team, player)] = data
|
||||||
|
|
||||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfile
|
||||||
import logging
|
import logging
|
||||||
|
import MultiServer
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
from pony.orm import commit, select
|
from pony.orm import commit, select
|
||||||
@@ -40,15 +41,18 @@ def uploads():
|
|||||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||||
elif file.filename.endswith(".apbp"):
|
elif file.filename.endswith(".apbp"):
|
||||||
splitted = file.filename.split("/")[-1][3:].split("P", 1)
|
splitted = file.filename.split("/")[-1][3:].split("P", 1)
|
||||||
player = int(splitted[1].split(".")[0].split("_")[0])
|
player_id, player_name = splitted[1].split(".")[0].split("_")
|
||||||
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
|
patches.add(Patch(data=zfile.open(file, "r").read(), player_name=player_name, player_id=player_id))
|
||||||
elif file.filename.endswith(".txt"):
|
elif file.filename.endswith(".txt"):
|
||||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||||
elif file.filename.endswith(".archipelago"):
|
elif file.filename.endswith(".archipelago"):
|
||||||
try:
|
try:
|
||||||
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
|
multidata = zfile.open(file).read()
|
||||||
|
MultiServer.Context._decompress(multidata)
|
||||||
except:
|
except:
|
||||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||||
|
else:
|
||||||
|
multidata = zfile.open(file).read()
|
||||||
if multidata:
|
if multidata:
|
||||||
commit() # commit patches
|
commit() # commit patches
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
||||||
@@ -61,9 +65,11 @@ def uploads():
|
|||||||
flash("No multidata was found in the zip file, which is required.")
|
flash("No multidata was found in the zip file, which is required.")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
|
multidata = file.read()
|
||||||
|
MultiServer.Context._decompress(multidata)
|
||||||
except:
|
except:
|
||||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||||
commit() # place into DB and generate ids
|
commit() # place into DB and generate ids
|
||||||
|
|||||||
23
data/factorio/mod/lib.lua
Normal file
23
data/factorio/mod/lib.lua
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
function filter_ingredients(ingredients)
|
||||||
|
local new_ingredient_list = {}
|
||||||
|
for _, ingredient_table in pairs(ingredients) do
|
||||||
|
if allowed_ingredients[ingredient_table[1]] then -- name of ingredient_table
|
||||||
|
table.insert(new_ingredient_list, ingredient_table)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_ingredient_list
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_any_stack_size(name)
|
||||||
|
local item = game.item_prototypes[name]
|
||||||
|
if item ~= nil then
|
||||||
|
return item.stack_size
|
||||||
|
end
|
||||||
|
item = game.equipment_prototypes[name]
|
||||||
|
if item ~= nil then
|
||||||
|
return item.stack_size
|
||||||
|
end
|
||||||
|
-- failsafe
|
||||||
|
return 1
|
||||||
|
end
|
||||||
@@ -1,19 +1,43 @@
|
|||||||
|
require "lib"
|
||||||
-- for testing
|
-- for testing
|
||||||
script.on_event(defines.events.on_tick, function(event)
|
script.on_event(defines.events.on_tick, function(event)
|
||||||
if event.tick%600 == 0 then
|
if event.tick%600 == 0 then
|
||||||
dumpTech()
|
dumpTech(game.forces["player"])
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- hook into researches done
|
-- hook into researches done
|
||||||
script.on_event(defines.events.on_research_finished, function(event)
|
script.on_event(defines.events.on_research_finished, function(event)
|
||||||
game.print("Research done")
|
local technology = event.research
|
||||||
dumpTech()
|
dumpTech(technology.force)
|
||||||
|
{% if free_samples %}
|
||||||
|
local players = technology.force.players
|
||||||
|
if technology.effects then
|
||||||
|
for _, effect in pairs(technology.effects) do
|
||||||
|
if effect.type == "unlock-recipe" then
|
||||||
|
local recipe = game.recipe_prototypes[effect.recipe]
|
||||||
|
for _, result in pairs(recipe.products) do
|
||||||
|
if result.type == "item" and result.amount then
|
||||||
|
{% if free_samples == 1 %}
|
||||||
|
local new = {count=result.amount, name=result.name}
|
||||||
|
{% elif free_samples == 2 %}
|
||||||
|
local new = {count=get_any_stack_size(result.name) * 0.5, name=result.name}
|
||||||
|
{% else %}
|
||||||
|
local new = {count=get_any_stack_size(result.name), name=result.name}
|
||||||
|
{% endif %}
|
||||||
|
for _, player in pairs(players) do
|
||||||
|
player.insert(new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
function dumpTech()
|
function dumpTech(force)
|
||||||
|
|
||||||
local force = game.forces["player"]
|
|
||||||
local data_collection = {}
|
local data_collection = {}
|
||||||
for tech_name, tech in pairs(force.technologies) do
|
for tech_name, tech in pairs(force.technologies) do
|
||||||
if tech.researched and string.find(tech_name, "ap-") == 1 then
|
if tech.researched and string.find(tech_name, "ap-") == 1 then
|
||||||
@@ -30,7 +54,7 @@ function dumpGameInfo()
|
|||||||
local data_collection = {}
|
local data_collection = {}
|
||||||
local force = game.forces["player"]
|
local force = game.forces["player"]
|
||||||
for tech_name, tech in pairs(force.technologies) do
|
for tech_name, tech in pairs(force.technologies) do
|
||||||
if tech.enabled then
|
if tech.enabled and tech.research_unit_count_formula == nil then
|
||||||
local tech_data = {}
|
local tech_data = {}
|
||||||
local unlocks = {}
|
local unlocks = {}
|
||||||
tech_data["unlocks"] = unlocks
|
tech_data["unlocks"] = unlocks
|
||||||
@@ -89,7 +113,8 @@ commands.add_command("ap-get-info-dump", "Dump Game Info, used by Archipelago.",
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
||||||
dumpTech()
|
dumpTech(game.players[call.player_index].force)
|
||||||
|
game.print("Wrote bridge file.")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||||
@@ -1,23 +1,40 @@
|
|||||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||||
|
require('lib')
|
||||||
|
|
||||||
local technologies = data.raw["technology"]
|
local technologies = data.raw["technology"]
|
||||||
local original_tech
|
local original_tech
|
||||||
local new_tree_copy
|
local new_tree_copy
|
||||||
|
allowed_ingredients = {}
|
||||||
|
{%- for ingredient in allowed_science_packs %}
|
||||||
|
allowed_ingredients["{{ingredient}}"]= 1
|
||||||
|
{% endfor %}
|
||||||
local template_tech = table.deepcopy(technologies["automation"])
|
local template_tech = table.deepcopy(technologies["automation"])
|
||||||
{#- ensure the copy unlocks nothing #}
|
{#- ensure the copy unlocks nothing #}
|
||||||
template_tech.unlocks = {}
|
template_tech.unlocks = {}
|
||||||
template_tech.upgrade = false
|
template_tech.upgrade = false
|
||||||
template_tech.effects = {}
|
template_tech.effects = {}
|
||||||
|
template_tech.prerequisites = {}
|
||||||
|
|
||||||
|
function prep_copy(new_copy, old_tech)
|
||||||
|
old_tech.enabled = false
|
||||||
|
new_copy.unit = table.deepcopy(old_tech.unit)
|
||||||
|
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||||
{%- for original_tech_name, item_name, receiving_player in locations %}
|
{%- for original_tech_name, item_name, receiving_player in locations %}
|
||||||
original_tech = technologies["{{original_tech_name}}"]
|
original_tech = technologies["{{original_tech_name}}"]
|
||||||
{#- the tech researched by the local player #}
|
{#- the tech researched by the local player #}
|
||||||
new_tree_copy = table.deepcopy(template_tech)
|
new_tree_copy = table.deepcopy(template_tech)
|
||||||
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
||||||
{#- hide and disable original tech; which will be shown, unlocked and enabled by AP Client #}
|
prep_copy(new_tree_copy, original_tech)
|
||||||
original_tech.enabled = false
|
{% if tech_cost != 1 %}
|
||||||
{#- copy original tech costs #}
|
if new_tree_copy.unit.count then
|
||||||
new_tree_copy.unit = table.deepcopy(original_tech.unit)
|
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||||
{% if item_name in tech_table %}
|
end
|
||||||
|
{% endif %}
|
||||||
|
{% if item_name in tech_table and visibility %}
|
||||||
{#- copy Factorio Technology Icon #}
|
{#- copy Factorio Technology Icon #}
|
||||||
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
|
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
|
||||||
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
|
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
|
||||||
@@ -28,7 +45,13 @@ new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
|
|||||||
new_tree_copy.icons = nil
|
new_tree_copy.icons = nil
|
||||||
new_tree_copy.icon_size = 512
|
new_tree_copy.icon_size = 512
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{#- add new technology to game #}
|
{#- connect Technology #}
|
||||||
|
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
||||||
|
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||||
|
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
||||||
|
{% endfor %}
|
||||||
|
{% endif -%}
|
||||||
|
{#- add new Technology to game #}
|
||||||
data:extend{new_tree_copy}
|
data:extend{new_tree_copy}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
|
|
||||||
[technology-name]
|
[technology-name]
|
||||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
{% for original_tech_name, item_name, receiving_player in locations %}
|
||||||
|
{%- if visibility %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
||||||
|
{%- else %}
|
||||||
|
ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
|
||||||
|
{%- endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
[technology-description]
|
[technology-description]
|
||||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
{% for original_tech_name, item_name, receiving_player in locations %}
|
||||||
"ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
|
{%- if visibility %}
|
||||||
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
|
||||||
|
{%- else %}
|
||||||
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
|
||||||
|
{%- endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
File diff suppressed because one or more lines are too long
@@ -40,6 +40,8 @@ server_options:
|
|||||||
# 1 -> Recommended for friendly racing, only allow Berserker's Multiworld, to disallow old /getitem for example
|
# 1 -> Recommended for friendly racing, only allow Berserker's Multiworld, to disallow old /getitem for example
|
||||||
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
|
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
|
||||||
compatibility: 2
|
compatibility: 2
|
||||||
|
# log all server traffic, mostly for dev use
|
||||||
|
log_network: 0
|
||||||
# Options for MultiMystery.py
|
# Options for MultiMystery.py
|
||||||
multi_mystery_options:
|
multi_mystery_options:
|
||||||
# Teams
|
# Teams
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ game:
|
|||||||
A Link to the Past: 1
|
A Link to the Past: 1
|
||||||
Hollow Knight: 1
|
Hollow Knight: 1
|
||||||
Factorio: 1
|
Factorio: 1
|
||||||
|
Minecraft: 1
|
||||||
# Shared Options supported by all games:
|
# Shared Options supported by all games:
|
||||||
accessibility:
|
accessibility:
|
||||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||||
@@ -35,6 +36,34 @@ accessibility:
|
|||||||
progression_balancing:
|
progression_balancing:
|
||||||
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||||
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||||
|
# Factorio options:
|
||||||
|
tech_tree_layout:
|
||||||
|
single: 1
|
||||||
|
small_diamonds: 1
|
||||||
|
medium_diamonds: 1
|
||||||
|
pyramid: 1
|
||||||
|
funnel: 1
|
||||||
|
max_science_pack:
|
||||||
|
automation_science_pack: 0
|
||||||
|
logistic_science_pack: 0
|
||||||
|
military_science_pack: 0
|
||||||
|
chemical_science_pack: 0
|
||||||
|
production_science_pack: 0
|
||||||
|
utility_science_pack: 0
|
||||||
|
space_science_pack: 1
|
||||||
|
tech_cost:
|
||||||
|
very_easy : 0
|
||||||
|
easy : 0
|
||||||
|
kind : 0
|
||||||
|
normal : 1
|
||||||
|
hard : 0
|
||||||
|
very_hard : 0
|
||||||
|
insane : 0
|
||||||
|
free_samples:
|
||||||
|
none: 1
|
||||||
|
single_craft: 0
|
||||||
|
half_stack: 0
|
||||||
|
stack: 0
|
||||||
# A Link to the Past options:
|
# A Link to the Past options:
|
||||||
### Logic Section ###
|
### Logic Section ###
|
||||||
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version
|
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version
|
||||||
@@ -172,11 +201,9 @@ retro:
|
|||||||
hints:
|
hints:
|
||||||
'on': 50 # Hint tiles sometimes give item location hints
|
'on': 50 # Hint tiles sometimes give item location hints
|
||||||
'off': 0 # Hint tiles provide gameplay tips
|
'off': 0 # Hint tiles provide gameplay tips
|
||||||
weapons: # Specifically, swords
|
swordless:
|
||||||
randomized: 0 # Swords are placed randomly throughout the world
|
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||||
assured: 50 # Begin with a sword, the rest are placed randomly throughout the world
|
off: 1
|
||||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
|
||||||
swordless: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
|
||||||
item_pool:
|
item_pool:
|
||||||
easy: 0 # Doubled upgrades, progressives, and etc
|
easy: 0 # Doubled upgrades, progressives, and etc
|
||||||
normal: 50 # Item availability remains unchanged from vanilla game
|
normal: 50 # Item availability remains unchanged from vanilla game
|
||||||
@@ -235,12 +262,14 @@ beemizer: # Remove items from the global item pool and replace them with single
|
|||||||
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
|
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
|
||||||
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
|
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
|
||||||
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
|
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
|
||||||
|
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
|
||||||
### Shop Settings ###
|
### Shop Settings ###
|
||||||
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||||
0: 50
|
0: 50
|
||||||
5: 0
|
5: 0
|
||||||
15: 0
|
15: 0
|
||||||
30: 0
|
30: 0
|
||||||
|
random: 0 # 0 to 30 evenly distributed
|
||||||
shop_shuffle:
|
shop_shuffle:
|
||||||
none: 50
|
none: 50
|
||||||
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
||||||
@@ -301,8 +330,8 @@ meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option
|
|||||||
- inverted # Never play inverted seeds
|
- inverted # Never play inverted seeds
|
||||||
retro:
|
retro:
|
||||||
- on # Never play retro seeds
|
- on # Never play retro seeds
|
||||||
weapons:
|
swordless:
|
||||||
- swordless # Never play a swordless seed
|
- on # Never play a swordless seed
|
||||||
linked_options:
|
linked_options:
|
||||||
- name: crosskeys
|
- name: crosskeys
|
||||||
options: # These overwrite earlier options if the percentage chance triggers
|
options: # These overwrite earlier options if the percentage chance triggers
|
||||||
@@ -326,9 +355,9 @@ linked_options:
|
|||||||
- name: enemizer
|
- name: enemizer
|
||||||
options:
|
options:
|
||||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||||
simple: 1
|
basic: 1
|
||||||
full: 1
|
full: 1
|
||||||
random: 1
|
chaos: 1
|
||||||
singularity: 1
|
singularity: 1
|
||||||
enemy_damage:
|
enemy_damage:
|
||||||
shuffled: 1
|
shuffled: 1
|
||||||
@@ -339,12 +368,46 @@ linked_options:
|
|||||||
expert: 1
|
expert: 1
|
||||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||||
# triggers that replace options upon rolling certain options
|
# triggers that replace options upon rolling certain options
|
||||||
|
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
|
||||||
|
trigger_disabled: 50
|
||||||
|
randomized: 0 # Swords are placed randomly throughout the world
|
||||||
|
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||||
|
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||||
|
swordless: 0 # swordless mode
|
||||||
triggers:
|
triggers:
|
||||||
|
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
|
||||||
|
- option_name: legacy_weapons
|
||||||
|
option_result: randomized
|
||||||
|
options:
|
||||||
|
swordless: off
|
||||||
|
- option_name: legacy_weapons
|
||||||
|
option_result: assured
|
||||||
|
options:
|
||||||
|
swordless: off
|
||||||
|
startinventory:
|
||||||
|
Progressive Sword: 1
|
||||||
|
- option_name: legacy_weapons
|
||||||
|
option_result: vanilla
|
||||||
|
options:
|
||||||
|
swordless: off
|
||||||
|
plando_items:
|
||||||
|
- items:
|
||||||
|
Progressive Sword: 4
|
||||||
|
locations:
|
||||||
|
- Master Sword Pedestal
|
||||||
|
- Pyramid Fairy - Left
|
||||||
|
- Blacksmith
|
||||||
|
- Link's Uncle
|
||||||
|
- option_name: legacy_weapons
|
||||||
|
option_result: swordless
|
||||||
|
options:
|
||||||
|
swordless: on
|
||||||
|
# end of legacy weapons block
|
||||||
- option_name: enemy_damage # targets enemy_damage
|
- option_name: enemy_damage # targets enemy_damage
|
||||||
option_result: shuffled # if it rolls shuffled
|
option_result: shuffled # if it rolls shuffled
|
||||||
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
||||||
options: # then inserts these options
|
options: # then inserts these options
|
||||||
swords: assured
|
swordless: off
|
||||||
### door rando only options (not supported at all yet on this branch) ###
|
### door rando only options (not supported at all yet on this branch) ###
|
||||||
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
|
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||||
vanilla: 50 # Everything should be like in vanilla
|
vanilla: 50 # Everything should be like in vanilla
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ appdirs>=1.4.4
|
|||||||
maseya-z3pr>=1.0.0rc1
|
maseya-z3pr>=1.0.0rc1
|
||||||
xxtea>=2.0.0.post0
|
xxtea>=2.0.0.post0
|
||||||
factorio-rcon-py>=1.2.1
|
factorio-rcon-py>=1.2.1
|
||||||
|
jinja2>=2.11.3
|
||||||
1
setup.py
1
setup.py
@@ -57,7 +57,6 @@ def manifest_creation():
|
|||||||
scripts = {"LttPClient.py": "ArchipelagoLttPClient",
|
scripts = {"LttPClient.py": "ArchipelagoLttPClient",
|
||||||
"MultiMystery.py": "ArchipelagoMultiMystery",
|
"MultiMystery.py": "ArchipelagoMultiMystery",
|
||||||
"MultiServer.py": "ArchipelagoServer",
|
"MultiServer.py": "ArchipelagoServer",
|
||||||
"gui.py": "ArchipelagoLttPCreator",
|
|
||||||
"Mystery.py": "ArchipelagoMystery",
|
"Mystery.py": "ArchipelagoMystery",
|
||||||
"LttPAdjuster.py": "ArchipelagoLttPAdjuster",
|
"LttPAdjuster.py": "ArchipelagoLttPAdjuster",
|
||||||
"FactorioClient.py": "ArchipelagoFactorioClient"}
|
"FactorioClient.py": "ArchipelagoFactorioClient"}
|
||||||
|
|||||||
@@ -8,25 +8,28 @@ __all__ = {"lookup_any_item_id_to_name",
|
|||||||
from .alttp.Items import lookup_id_to_name as alttp
|
from .alttp.Items import lookup_id_to_name as alttp
|
||||||
from .hk.Items import lookup_id_to_name as hk
|
from .hk.Items import lookup_id_to_name as hk
|
||||||
from .factorio import Technologies
|
from .factorio import Technologies
|
||||||
lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name}
|
|
||||||
lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()}
|
|
||||||
|
|
||||||
|
lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name}
|
||||||
|
assert len(alttp) + len(hk) + len(Technologies.lookup_id_to_name) == len(lookup_any_item_id_to_name)
|
||||||
|
lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()}
|
||||||
|
|
||||||
from .alttp import Regions
|
from .alttp import Regions
|
||||||
from .hk import Locations
|
from .hk import Locations
|
||||||
|
|
||||||
lookup_any_location_id_to_name = {**Regions.lookup_id_to_name, **Locations.lookup_id_to_name,
|
lookup_any_location_id_to_name = {**Regions.lookup_id_to_name, **Locations.lookup_id_to_name,
|
||||||
**Technologies.lookup_id_to_name}
|
**Technologies.lookup_id_to_name}
|
||||||
|
assert len(Regions.lookup_id_to_name) + len(Locations.lookup_id_to_name) + len(Technologies.lookup_id_to_name) == \
|
||||||
|
len(lookup_any_location_id_to_name)
|
||||||
lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_id_to_name.items()}
|
lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_id_to_name.items()}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name,
|
network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name,
|
||||||
"lookup_any_item_id_to_name": lookup_any_item_id_to_name,
|
"lookup_any_item_id_to_name": lookup_any_item_id_to_name,
|
||||||
"version": 2}
|
"version": 4}
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
class Games(str, enum.Enum):
|
class Games(str, enum.Enum):
|
||||||
HK = "Hollow Knight"
|
HK = "Hollow Knight"
|
||||||
LTTP = "A Link to the Past"
|
LTTP = "A Link to the Past"
|
||||||
Factorio = "Factorio"
|
Factorio = "Factorio"
|
||||||
|
Minecraft = "Minecraft"
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def KholdstareDefeatRule(state, player: int):
|
|||||||
state.has('Fire Rod', player) or
|
state.has('Fire Rod', player) or
|
||||||
(
|
(
|
||||||
state.has('Bombos', player) and
|
state.has('Bombos', player) and
|
||||||
(state.has_sword(player) or state.world.swords[player] == 'swordless')
|
(state.has_sword(player) or state.world.swordless[player])
|
||||||
)
|
)
|
||||||
) and
|
) and
|
||||||
(
|
(
|
||||||
@@ -89,7 +89,7 @@ def KholdstareDefeatRule(state, player: int):
|
|||||||
(
|
(
|
||||||
state.has('Fire Rod', player) and
|
state.has('Fire Rod', player) and
|
||||||
state.has('Bombos', player) and
|
state.has('Bombos', player) and
|
||||||
state.world.swords[player] == 'swordless' and
|
state.world.swordless[player] and
|
||||||
state.can_extend_magic(player, 16)
|
state.can_extend_magic(player, 16)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -113,7 +113,7 @@ def AgahnimDefeatRule(state, player: int):
|
|||||||
|
|
||||||
|
|
||||||
def GanonDefeatRule(state, player: int):
|
def GanonDefeatRule(state, player: int):
|
||||||
if state.world.swords[player] == "swordless":
|
if state.world.swordless[player]:
|
||||||
return state.has('Hammer', player) and \
|
return state.has('Hammer', player) and \
|
||||||
state.has_fire_source(player) and \
|
state.has_fire_source(player) and \
|
||||||
state.has('Silver Bow', player) and \
|
state.has('Silver Bow', player) and \
|
||||||
@@ -241,7 +241,7 @@ def place_bosses(world, player: int):
|
|||||||
if shuffle_mode == "none":
|
if shuffle_mode == "none":
|
||||||
return # vanilla bosses come pre-placed
|
return # vanilla bosses come pre-placed
|
||||||
|
|
||||||
if shuffle_mode in ["basic", "normal"]:
|
if shuffle_mode in ["basic", "full"]:
|
||||||
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
||||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||||
else: # all bosses present, the three duplicates chosen at random
|
else: # all bosses present, the three duplicates chosen at random
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
Requires the moon pearl to be Link in the Light World
|
Requires the moon pearl to be Link in the Light World
|
||||||
instead of a bunny.
|
instead of a bunny.
|
||||||
''')
|
''')
|
||||||
parser.add_argument('--swords', default=defval('random'), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
|
parser.add_argument('--swordless', action='store_true',
|
||||||
help='''\
|
help='''\
|
||||||
Select sword placement. (default: %(default)s)
|
Toggles Swordless Mode
|
||||||
Random: All swords placed randomly.
|
|
||||||
Assured: Start game with a sword already.
|
|
||||||
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
|
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
|
||||||
Tower are removed, Agahnim\'s Tower barrier can be
|
Tower are removed, Agahnim\'s Tower barrier can be
|
||||||
destroyed with hammer. Misery Mire and Turtle Rock
|
destroyed with hammer. Misery Mire and Turtle Rock
|
||||||
@@ -57,7 +55,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
Ether and Bombos Tablet can be activated with Hammer
|
Ether and Bombos Tablet can be activated with Hammer
|
||||||
(and Book). Bombos pads have been added in Ice
|
(and Book). Bombos pads have been added in Ice
|
||||||
Palace, to allow for an alternative to firerod.
|
Palace, to allow for an alternative to firerod.
|
||||||
Vanilla: Swords are in vanilla locations.
|
|
||||||
''')
|
''')
|
||||||
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
|
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
|
||||||
choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'],
|
choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'],
|
||||||
@@ -401,7 +398,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
for player in range(1, multiargs.multi + 1):
|
for player in range(1, multiargs.multi + 1):
|
||||||
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
||||||
|
|
||||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
|
||||||
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer',
|
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer',
|
||||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ difficulties = {
|
|||||||
basicmagic=['Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'],
|
basicmagic=['Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'],
|
||||||
progressivesword=['Progressive Sword'] * 8,
|
progressivesword=['Progressive Sword'] * 8,
|
||||||
basicsword=['Master Sword', 'Tempered Sword', 'Golden Sword', 'Fighter Sword'] * 2,
|
basicsword=['Master Sword', 'Tempered Sword', 'Golden Sword', 'Fighter Sword'] * 2,
|
||||||
progressivebow=["Progressive Bow"] * 2,
|
progressivebow=["Progressive Bow"] * 4,
|
||||||
basicbow=['Bow', 'Silver Bow'] * 2,
|
basicbow=['Bow', 'Silver Bow'] * 2,
|
||||||
timedohko=['Green Clock'] * 25,
|
timedohko=['Green Clock'] * 25,
|
||||||
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||||
@@ -279,7 +279,7 @@ def generate_itempool(world, player: int):
|
|||||||
itempool.extend(itemdiff.bottles)
|
itempool.extend(itemdiff.bottles)
|
||||||
itempool.extend(itemdiff.basicbow)
|
itempool.extend(itemdiff.basicbow)
|
||||||
itempool.extend(itemdiff.basicarmor)
|
itempool.extend(itemdiff.basicarmor)
|
||||||
if world.swords[player] != 'swordless':
|
if not world.swordless[player]:
|
||||||
itempool.extend(itemdiff.basicsword)
|
itempool.extend(itemdiff.basicsword)
|
||||||
itempool.extend(itemdiff.basicmagic)
|
itempool.extend(itemdiff.basicmagic)
|
||||||
itempool.extend(itemdiff.basicglove)
|
itempool.extend(itemdiff.basicglove)
|
||||||
@@ -335,7 +335,7 @@ def generate_itempool(world, player: int):
|
|||||||
possible_weapons = []
|
possible_weapons = []
|
||||||
for item in pool:
|
for item in pool:
|
||||||
if item in ['Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']:
|
if item in ['Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']:
|
||||||
if not found_sword and world.swords[player] != 'swordless':
|
if not found_sword:
|
||||||
found_sword = True
|
found_sword = True
|
||||||
possible_weapons.append(item)
|
possible_weapons.append(item)
|
||||||
if item in ['Progressive Bow', 'Bow'] and not found_bow:
|
if item in ['Progressive Bow', 'Bow'] and not found_bow:
|
||||||
@@ -548,7 +548,7 @@ def get_pool_core(world, player: int):
|
|||||||
timer = world.timer[player]
|
timer = world.timer[player]
|
||||||
goal = world.goal[player]
|
goal = world.goal[player]
|
||||||
mode = world.mode[player]
|
mode = world.mode[player]
|
||||||
swords = world.swords[player]
|
swordless = world.swordless[player]
|
||||||
retro = world.retro[player]
|
retro = world.retro[player]
|
||||||
logic = world.logic[player]
|
logic = world.logic[player]
|
||||||
|
|
||||||
@@ -619,7 +619,7 @@ def get_pool_core(world, player: int):
|
|||||||
|
|
||||||
if want_progressives():
|
if want_progressives():
|
||||||
pool.extend(diff.progressivebow)
|
pool.extend(diff.progressivebow)
|
||||||
elif (swords == 'swordless' or logic == 'noglitches') and goal != 'icerodhunt':
|
elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
|
||||||
swordless_bows = ['Bow', 'Silver Bow']
|
swordless_bows = ['Bow', 'Silver Bow']
|
||||||
if difficulty == "easy":
|
if difficulty == "easy":
|
||||||
swordless_bows *= 2
|
swordless_bows *= 2
|
||||||
@@ -627,33 +627,11 @@ def get_pool_core(world, player: int):
|
|||||||
else:
|
else:
|
||||||
pool.extend(diff.basicbow)
|
pool.extend(diff.basicbow)
|
||||||
|
|
||||||
if swords == 'swordless':
|
if swordless:
|
||||||
pool.extend(diff.swordless)
|
pool.extend(diff.swordless)
|
||||||
elif swords == 'vanilla':
|
|
||||||
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
|
|
||||||
world.random.shuffle(swords_to_use)
|
|
||||||
|
|
||||||
place_item('Link\'s Uncle', swords_to_use.pop())
|
|
||||||
place_item('Blacksmith', swords_to_use.pop())
|
|
||||||
place_item('Pyramid Fairy - Left', swords_to_use.pop())
|
|
||||||
if goal != 'pedestal':
|
|
||||||
place_item('Master Sword Pedestal', swords_to_use.pop())
|
|
||||||
else:
|
|
||||||
swords_to_use.pop()
|
|
||||||
place_item('Master Sword Pedestal', 'Triforce')
|
|
||||||
if swords_to_use:
|
|
||||||
pool.extend(swords_to_use)
|
|
||||||
else:
|
else:
|
||||||
progressive_swords = want_progressives()
|
progressive_swords = want_progressives()
|
||||||
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
|
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
|
||||||
if swords == 'assured' and goal != 'icerodhunt':
|
|
||||||
if progressive_swords:
|
|
||||||
precollected_items.append('Progressive Sword')
|
|
||||||
pool.remove('Progressive Sword')
|
|
||||||
else:
|
|
||||||
precollected_items.append('Fighter Sword')
|
|
||||||
pool.remove('Fighter Sword')
|
|
||||||
pool.extend(['Rupees (50)'])
|
|
||||||
|
|
||||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||||
|
|
||||||
@@ -684,7 +662,7 @@ def get_pool_core(world, player: int):
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
if goal == 'pedestal' and swords != 'vanilla':
|
if goal == 'pedestal':
|
||||||
place_item('Master Sword Pedestal', 'Triforce')
|
place_item('Master Sword Pedestal', 'Triforce')
|
||||||
pool.remove("Rupees (20)")
|
pool.remove("Rupees (20)")
|
||||||
|
|
||||||
|
|||||||
@@ -879,7 +879,7 @@ def patch_rom(world, rom, player, team, enemized):
|
|||||||
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
|
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
|
||||||
|
|
||||||
# set light cones
|
# set light cones
|
||||||
rom.write_byte(0x180038, 0x01 if world.sewer_light_cone[player] else 0x00)
|
rom.write_byte(0x180038, 0x01 if world.mode[player] == "standard" else 0x00)
|
||||||
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
|
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
|
||||||
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
|
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
|
||||||
|
|
||||||
@@ -961,7 +961,7 @@ def patch_rom(world, rom, player, team, enemized):
|
|||||||
|
|
||||||
# Set overflow items for progressive equipment
|
# Set overflow items for progressive equipment
|
||||||
rom.write_bytes(0x180090,
|
rom.write_bytes(0x180090,
|
||||||
[difficulty.progressive_sword_limit if world.swords[player] != 'swordless' else 0,
|
[difficulty.progressive_sword_limit if not world.swordless[player] else 0,
|
||||||
overflow_replacement,
|
overflow_replacement,
|
||||||
difficulty.progressive_shield_limit, overflow_replacement,
|
difficulty.progressive_shield_limit, overflow_replacement,
|
||||||
difficulty.progressive_armor_limit, overflow_replacement,
|
difficulty.progressive_armor_limit, overflow_replacement,
|
||||||
@@ -971,7 +971,7 @@ def patch_rom(world, rom, player, team, enemized):
|
|||||||
rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement])
|
rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement])
|
||||||
|
|
||||||
if difficulty.progressive_bow_limit < 2 and (
|
if difficulty.progressive_bow_limit < 2 and (
|
||||||
world.swords[player] == 'swordless' or world.logic[player] == 'noglitches'):
|
world.swordless[player] or world.logic[player] == 'noglitches'):
|
||||||
rom.write_bytes(0x180098, [2, overflow_replacement])
|
rom.write_bytes(0x180098, [2, overflow_replacement])
|
||||||
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
||||||
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
||||||
@@ -1123,11 +1123,11 @@ def patch_rom(world, rom, player, team, enemized):
|
|||||||
rom.write_byte(0x180029, 0x01) # Smithy quick item give
|
rom.write_byte(0x180029, 0x01) # Smithy quick item give
|
||||||
|
|
||||||
# set swordless mode settings
|
# set swordless mode settings
|
||||||
rom.write_byte(0x18003F, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer can harm ganon
|
rom.write_byte(0x18003F, 0x01 if world.swordless[player] else 0x00) # hammer can harm ganon
|
||||||
rom.write_byte(0x180040, 0x01 if world.swords[player] == 'swordless' else 0x00) # open curtains
|
rom.write_byte(0x180040, 0x01 if world.swordless[player] else 0x00) # open curtains
|
||||||
rom.write_byte(0x180041, 0x01 if world.swords[player] == 'swordless' else 0x00) # swordless medallions
|
rom.write_byte(0x180041, 0x01 if world.swordless[player] else 0x00) # swordless medallions
|
||||||
rom.write_byte(0x180043, 0xFF if world.swords[player] == 'swordless' else 0x00) # starting sword for link
|
rom.write_byte(0x180043, 0xFF if world.swordless[player] else 0x00) # starting sword for link
|
||||||
rom.write_byte(0x180044, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer activates tablets
|
rom.write_byte(0x180044, 0x01 if world.swordless[player] else 0x00) # hammer activates tablets
|
||||||
|
|
||||||
if world.item_functionality[player] == 'easy':
|
if world.item_functionality[player] == 'easy':
|
||||||
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
|
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
|
||||||
@@ -1651,7 +1651,7 @@ def write_custom_shops(rom, world, player):
|
|||||||
if item is None:
|
if item is None:
|
||||||
break
|
break
|
||||||
if not item['item'] in item_table: # item not native to ALTTP
|
if not item['item'] in item_table: # item not native to ALTTP
|
||||||
item_code = 0x21
|
item_code = 0x09 # Hammer
|
||||||
else:
|
else:
|
||||||
item_code = ItemFactory(item['item'], player).code
|
item_code = ItemFactory(item['item'], player).code
|
||||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
||||||
@@ -1775,7 +1775,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
|||||||
option_name: True
|
option_name: True
|
||||||
}
|
}
|
||||||
|
|
||||||
data_dir = local_path("../../data") if is_bundled() else None
|
data_dir = local_path("data") if is_bundled() else None
|
||||||
offsets_array = build_offset_collections(options, data_dir)
|
offsets_array = build_offset_collections(options, data_dir)
|
||||||
restore_maseya_colors(rom, offsets_array)
|
restore_maseya_colors(rom, offsets_array)
|
||||||
if mode == 'default':
|
if mode == 'default':
|
||||||
@@ -2218,7 +2218,7 @@ def write_strings(rom, world, player, team):
|
|||||||
prog_bow_locs = world.find_items('Progressive Bow', player)
|
prog_bow_locs = world.find_items('Progressive Bow', player)
|
||||||
distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None)
|
distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None)
|
||||||
progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
|
progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
|
||||||
world.swords[player] == 'swordless' or world.logic[player] == 'noglitches')
|
world.swordless[player] or world.logic[player] == 'noglitches')
|
||||||
if distinguished_prog_bow_loc:
|
if distinguished_prog_bow_loc:
|
||||||
prog_bow_locs.remove(distinguished_prog_bow_loc)
|
prog_bow_locs.remove(distinguished_prog_bow_loc)
|
||||||
silverarrow_hint = (' %s?' % hint_text(distinguished_prog_bow_loc).replace('Ganon\'s',
|
silverarrow_hint = (' %s?' % hint_text(distinguished_prog_bow_loc).replace('Ganon\'s',
|
||||||
|
|||||||
@@ -505,7 +505,7 @@ def default_rules(world, player):
|
|||||||
|
|
||||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
||||||
|
|
||||||
if world.swords[player] == 'swordless':
|
if world.swordless[player]:
|
||||||
swordless_rules(world, player)
|
swordless_rules(world, player)
|
||||||
|
|
||||||
|
|
||||||
@@ -657,7 +657,7 @@ def inverted_rules(world, player):
|
|||||||
|
|
||||||
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
||||||
|
|
||||||
if world.swords[player] == 'swordless':
|
if world.swordless[player]:
|
||||||
swordless_rules(world, player)
|
swordless_rules(world, player)
|
||||||
|
|
||||||
def no_glitches_rules(world, player):
|
def no_glitches_rules(world, player):
|
||||||
@@ -781,7 +781,7 @@ def add_conditional_lamps(world, player):
|
|||||||
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
|
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
|
||||||
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
|
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
|
||||||
|
|
||||||
if not world.sewer_light_cone[player]:
|
if not world.mode[player] == "standard":
|
||||||
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
|
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
|
||||||
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
|
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
|
||||||
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
|
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
|
"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import zipfile
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
@@ -8,6 +9,7 @@ import json
|
|||||||
import jinja2
|
import jinja2
|
||||||
import Utils
|
import Utils
|
||||||
import shutil
|
import shutil
|
||||||
|
import Options
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from .Technologies import tech_table
|
from .Technologies import tech_table
|
||||||
|
|
||||||
@@ -29,26 +31,41 @@ def generate_mod(world: MultiWorld, player: int):
|
|||||||
global template, locale_template
|
global template, locale_template
|
||||||
with template_load_lock:
|
with template_load_lock:
|
||||||
if not template:
|
if not template:
|
||||||
template = jinja2.Template(open(Utils.local_path("data", "factorio", "mod_template", "data-final-fixes.lua")).read())
|
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
|
||||||
locale_template = jinja2.Template(open(Utils.local_path("data", "factorio", "mod_template", "locale", "en", "locale.cfg")).read())
|
template = jinja2.Template(open(os.path.join(mod_template_folder, "data-final-fixes.lua")).read())
|
||||||
|
locale_template = jinja2.Template(open(os.path.join(mod_template_folder, "locale", "en", "locale.cfg")).read())
|
||||||
|
control_template = jinja2.Template(open(os.path.join(mod_template_folder, "control.lua")).read())
|
||||||
# get data for templates
|
# get data for templates
|
||||||
player_names = {x: world.player_names[x][0] for x in world.player_ids}
|
player_names = {x: world.player_names[x][0] for x in world.player_ids}
|
||||||
locations = []
|
locations = []
|
||||||
for location in world.get_filled_locations(player):
|
for location in world.get_filled_locations(player):
|
||||||
if not location.name.startswith("recipe-"): # introduce this a new location property?
|
if not location.name.startswith("recipe-"): # introduce this as a new location property?
|
||||||
locations.append((location.name, location.item.name, location.item.player))
|
locations.append((location.name, location.item.name, location.item.player))
|
||||||
mod_name = f"archipelago-client-{world.seed}-{player}"
|
mod_name = f"archipelago-client-{world.seed}-{player}"
|
||||||
|
tech_cost = {0: 0.1,
|
||||||
|
1: 0.25,
|
||||||
|
2: 0.5,
|
||||||
|
3: 1,
|
||||||
|
4: 2,
|
||||||
|
5: 5,
|
||||||
|
6: 10}[world.tech_cost[player].value]
|
||||||
template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table,
|
template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table,
|
||||||
"mod_name": mod_name}
|
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
|
||||||
|
"tech_cost_scale": tech_cost,
|
||||||
|
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player]}
|
||||||
|
for factorio_option in Options.factorio_options:
|
||||||
|
template_data[factorio_option] = getattr(world, factorio_option)[player].value
|
||||||
|
control_code = control_template.render(**template_data)
|
||||||
|
data_final_fixes_code = template.render(**template_data)
|
||||||
|
|
||||||
mod_code = template.render(**template_data)
|
mod_dir = Utils.output_path(mod_name)+"_"+Utils.__version__
|
||||||
|
|
||||||
mod_dir = Utils.output_path(mod_name)
|
|
||||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||||
os.makedirs(en_locale_dir, exist_ok=True)
|
os.makedirs(en_locale_dir, exist_ok=True)
|
||||||
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
|
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
|
||||||
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
||||||
f.write(mod_code)
|
f.write(data_final_fixes_code)
|
||||||
|
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
|
||||||
|
f.write(control_code)
|
||||||
locale_content = locale_template.render(**template_data)
|
locale_content = locale_template.render(**template_data)
|
||||||
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
||||||
f.write(locale_content)
|
f.write(locale_content)
|
||||||
@@ -56,3 +73,14 @@ def generate_mod(world: MultiWorld, player: int):
|
|||||||
info["name"] = mod_name
|
info["name"] = mod_name
|
||||||
with open(os.path.join(mod_dir, "info.json"), "wt") as f:
|
with open(os.path.join(mod_dir, "info.json"), "wt") as f:
|
||||||
json.dump(info, f, indent=4)
|
json.dump(info, f, indent=4)
|
||||||
|
|
||||||
|
# zip the result
|
||||||
|
zf_path = os.path.join(mod_dir+".zip")
|
||||||
|
with zipfile.ZipFile(zf_path, compression=zipfile.ZIP_DEFLATED, mode='w') as zf:
|
||||||
|
for root, dirs, files in os.walk(mod_dir):
|
||||||
|
for file in files:
|
||||||
|
zf.write(os.path.join(root, file),
|
||||||
|
os.path.relpath(os.path.join(root, file),
|
||||||
|
os.path.join(mod_dir, '..')))
|
||||||
|
shutil.rmtree(mod_dir)
|
||||||
|
|
||||||
|
|||||||
97
worlds/factorio/Shapes.py
Normal file
97
worlds/factorio/Shapes.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from typing import Dict, List, Set
|
||||||
|
|
||||||
|
from BaseClasses import MultiWorld
|
||||||
|
from Options import TechTreeLayout
|
||||||
|
from worlds.factorio.Technologies import technology_table
|
||||||
|
|
||||||
|
def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
|
||||||
|
prerequisites: Dict[str, Set[str]] = {}
|
||||||
|
layout = world.tech_tree_layout[player].value
|
||||||
|
if layout == TechTreeLayout.option_small_diamonds:
|
||||||
|
slice_size = 4
|
||||||
|
tech_names: List[str] = list(set(technology_table) - world._static_nodes)
|
||||||
|
tech_names.sort()
|
||||||
|
world.random.shuffle(tech_names)
|
||||||
|
while len(tech_names) > slice_size:
|
||||||
|
slice = tech_names[:slice_size]
|
||||||
|
tech_names = tech_names[slice_size:]
|
||||||
|
slice.sort(key=lambda tech_name: len(technology_table[tech_name].ingredients))
|
||||||
|
diamond_0, diamond_1, diamond_2, diamond_3 = slice
|
||||||
|
|
||||||
|
# 0 |
|
||||||
|
# 1 2 |
|
||||||
|
# 3 V
|
||||||
|
prerequisites[diamond_3] = {diamond_1, diamond_2}
|
||||||
|
prerequisites[diamond_2] = prerequisites[diamond_1] = {diamond_0}
|
||||||
|
elif layout == TechTreeLayout.option_medium_diamonds:
|
||||||
|
slice_size = 9
|
||||||
|
tech_names: List[str] = list(set(technology_table) - world._static_nodes)
|
||||||
|
tech_names.sort()
|
||||||
|
world.random.shuffle(tech_names)
|
||||||
|
while len(tech_names) > slice_size:
|
||||||
|
slice = tech_names[:slice_size]
|
||||||
|
tech_names = tech_names[slice_size:]
|
||||||
|
slice.sort(key=lambda tech_name: len(technology_table[tech_name].ingredients))
|
||||||
|
|
||||||
|
# 0 |
|
||||||
|
# 1 2 |
|
||||||
|
# 3 4 5 |
|
||||||
|
# 6 7 |
|
||||||
|
# 8 V
|
||||||
|
|
||||||
|
prerequisites[slice[1]] = {slice[0]}
|
||||||
|
prerequisites[slice[2]] = {slice[0]}
|
||||||
|
|
||||||
|
prerequisites[slice[3]] = {slice[1]}
|
||||||
|
prerequisites[slice[4]] = {slice[1], slice[2]}
|
||||||
|
prerequisites[slice[5]] = {slice[2]}
|
||||||
|
|
||||||
|
prerequisites[slice[6]] = {slice[3], slice[4]}
|
||||||
|
prerequisites[slice[7]] = {slice[4], slice[5]}
|
||||||
|
|
||||||
|
prerequisites[slice[8]] = {slice[6], slice[7]}
|
||||||
|
|
||||||
|
elif layout == TechTreeLayout.option_pyramid:
|
||||||
|
slice_size = 1
|
||||||
|
tech_names: List[str] = list(set(technology_table) - world._static_nodes)
|
||||||
|
tech_names.sort()
|
||||||
|
world.random.shuffle(tech_names)
|
||||||
|
tech_names.sort(key=lambda tech_name: len(technology_table[tech_name].ingredients))
|
||||||
|
previous_slice = []
|
||||||
|
while len(tech_names) > slice_size:
|
||||||
|
slice = tech_names[:slice_size]
|
||||||
|
world.random.shuffle(slice)
|
||||||
|
tech_names = tech_names[slice_size:]
|
||||||
|
for i, tech_name in enumerate(previous_slice):
|
||||||
|
prerequisites.setdefault(slice[i], set()).add(tech_name)
|
||||||
|
prerequisites.setdefault(slice[i + 1], set()).add(tech_name)
|
||||||
|
previous_slice = slice
|
||||||
|
slice_size += 1
|
||||||
|
|
||||||
|
elif layout == TechTreeLayout.option_funnel:
|
||||||
|
|
||||||
|
|
||||||
|
tech_names: List[str] = list(set(technology_table) - world._static_nodes)
|
||||||
|
# find largest inverse pyramid
|
||||||
|
# https://www.wolframalpha.com/input/?i=x+=+1/2+(n++++1)+(2++++n)+solve+for+n
|
||||||
|
import math
|
||||||
|
slice_size = int(0.5*(math.sqrt(8*len(tech_names)+1)-3))
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.info(slice_size)
|
||||||
|
tech_names.sort()
|
||||||
|
world.random.shuffle(tech_names)
|
||||||
|
tech_names.sort(key=lambda tech_name: len(technology_table[tech_name].ingredients))
|
||||||
|
previous_slice = []
|
||||||
|
while slice_size:
|
||||||
|
slice = tech_names[:slice_size]
|
||||||
|
world.random.shuffle(slice)
|
||||||
|
tech_names = tech_names[slice_size:]
|
||||||
|
if previous_slice:
|
||||||
|
for i, tech_name in enumerate(slice):
|
||||||
|
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
|
||||||
|
previous_slice = slice
|
||||||
|
slice_size -= 1
|
||||||
|
|
||||||
|
world.tech_tree_layout_prerequisites[player] = prerequisites
|
||||||
|
return prerequisites
|
||||||
@@ -1,46 +1,136 @@
|
|||||||
|
from __future__ import annotations
|
||||||
# Factorio technologies are imported from a .json document in /data
|
# Factorio technologies are imported from a .json document in /data
|
||||||
from typing import Dict
|
from typing import Dict, Set, FrozenSet
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
|
import logging
|
||||||
|
|
||||||
factorio_id = 2**17
|
factorio_id = 2 ** 17
|
||||||
|
|
||||||
source_file = Utils.local_path("data", "factorio", "techs.json")
|
source_file = Utils.local_path("data", "factorio", "techs.json")
|
||||||
|
recipe_source_file = Utils.local_path("data", "factorio", "recipes.json")
|
||||||
with open(source_file) as f:
|
with open(source_file) as f:
|
||||||
raw = json.load(f)
|
raw = json.load(f)
|
||||||
|
with open(recipe_source_file) as f:
|
||||||
|
raw_recipes = json.load(f)
|
||||||
tech_table = {}
|
tech_table = {}
|
||||||
|
technology_table:Dict[str, Technology] = {}
|
||||||
|
|
||||||
requirements = {}
|
always = lambda state: True
|
||||||
ingredients = {}
|
|
||||||
all_ingredients = set()
|
|
||||||
|
class Technology(): # maybe make subclass of Location?
|
||||||
|
def __init__(self, name, ingredients):
|
||||||
|
self.name = name
|
||||||
|
global factorio_id
|
||||||
|
self.factorio_id = factorio_id
|
||||||
|
factorio_id += 1
|
||||||
|
self.ingredients = ingredients
|
||||||
|
|
||||||
|
def build_rule(self, allowed_packs, player: int):
|
||||||
|
logging.debug(f"Building rules for {self.name}")
|
||||||
|
ingredient_rules = []
|
||||||
|
for ingredient in self.ingredients:
|
||||||
|
if ingredient in allowed_packs:
|
||||||
|
logging.debug(f"Building rules for ingredient {ingredient}")
|
||||||
|
technologies = required_technologies[ingredient] # technologies that unlock the recipes
|
||||||
|
if technologies:
|
||||||
|
logging.debug(f"Required Technologies: {technologies}")
|
||||||
|
ingredient_rules.append(
|
||||||
|
lambda state, technologies=technologies: all(state.has(technology.name, player)
|
||||||
|
for technology in technologies))
|
||||||
|
if ingredient_rules:
|
||||||
|
ingredient_rules = frozenset(ingredient_rules)
|
||||||
|
return lambda state: all(rule(state) for rule in ingredient_rules)
|
||||||
|
|
||||||
|
return always
|
||||||
|
|
||||||
|
def get_prior_technologies(self, allowed_packs) -> Set[Technology]:
|
||||||
|
"""Get Technologies that have to precede this one to resolve tree connections."""
|
||||||
|
technologies = set()
|
||||||
|
for ingredient in self.ingredients:
|
||||||
|
if ingredient in allowed_packs:
|
||||||
|
technologies |= required_technologies[ingredient] # technologies that unlock the recipes
|
||||||
|
return technologies
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.factorio_id
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self.name})"
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe():
|
||||||
|
def __init__(self, name, category, ingredients, products):
|
||||||
|
self.name = name
|
||||||
|
self.category = category
|
||||||
|
self.ingredients = ingredients
|
||||||
|
self.products = products
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self.name})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unlocking_technologies(self) -> Set[Technology]:
|
||||||
|
"""Unlocked by any of the returned technologies. Empty set indicates a starting recipe."""
|
||||||
|
return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
|
||||||
|
|
||||||
# TODO: export this dynamically, or filter it during export
|
|
||||||
starting_ingredient_recipes = {"automation-science-pack"}
|
|
||||||
|
|
||||||
# recipes and technologies can share names in Factorio
|
# recipes and technologies can share names in Factorio
|
||||||
for technology in sorted(raw):
|
for technology_name in sorted(raw):
|
||||||
data = raw[technology]
|
data = raw[technology_name]
|
||||||
tech_table[technology] = factorio_id
|
|
||||||
factorio_id += 1
|
factorio_id += 1
|
||||||
if data["requires"]:
|
current_ingredients = set(data["ingredients"])
|
||||||
requirements[technology] = set(data["requires"])
|
technology = Technology(technology_name, current_ingredients)
|
||||||
current_ingredients = set(data["ingredients"])-starting_ingredient_recipes
|
tech_table[technology_name] = technology.factorio_id
|
||||||
if current_ingredients:
|
technology_table[technology_name] = technology
|
||||||
|
|
||||||
all_ingredients |= current_ingredients
|
recipe_sources: Dict[str, str] = {} # recipe_name -> technology source
|
||||||
current_ingredients = {"recipe-"+ingredient for ingredient in current_ingredients}
|
|
||||||
ingredients[technology] = current_ingredients
|
|
||||||
|
|
||||||
recipe_sources = {}
|
|
||||||
|
|
||||||
for technology, data in raw.items():
|
for technology, data in raw.items():
|
||||||
recipe_source = all_ingredients & set(data["unlocks"])
|
for recipe_name in data["unlocks"]:
|
||||||
for recipe in recipe_source:
|
recipe_sources.setdefault(recipe_name, set()).add(technology)
|
||||||
recipe_sources["recipe-"+recipe] = technology
|
|
||||||
|
|
||||||
all_ingredients = {"recipe-"+ingredient for ingredient in all_ingredients}
|
del (raw)
|
||||||
del(raw)
|
|
||||||
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
||||||
|
|
||||||
|
all_product_sources: Dict[str, Recipe] = {}
|
||||||
|
for recipe_name, recipe_data in raw_recipes.items():
|
||||||
|
# example:
|
||||||
|
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
|
||||||
|
|
||||||
|
recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))
|
||||||
|
if recipe.products != recipe.ingredients: # prevents loop recipes like uranium centrifuging
|
||||||
|
for product_name in recipe.products:
|
||||||
|
all_product_sources[product_name] = recipe
|
||||||
|
|
||||||
|
# build requirements graph for all technology ingredients
|
||||||
|
|
||||||
|
all_ingredient_names: Set[str] = set()
|
||||||
|
for technology in technology_table.values():
|
||||||
|
all_ingredient_names |= technology.ingredients
|
||||||
|
|
||||||
|
|
||||||
|
def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[Technology]:
|
||||||
|
if _done:
|
||||||
|
if ingredient_name in _done:
|
||||||
|
return set()
|
||||||
|
else:
|
||||||
|
_done.add(ingredient_name)
|
||||||
|
else:
|
||||||
|
_done = {ingredient_name}
|
||||||
|
recipe = all_product_sources.get(ingredient_name)
|
||||||
|
if not recipe:
|
||||||
|
return set()
|
||||||
|
current_technologies = recipe.unlocking_technologies.copy()
|
||||||
|
for ingredient_name in recipe.ingredients:
|
||||||
|
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
|
||||||
|
return current_technologies
|
||||||
|
|
||||||
|
|
||||||
|
required_technologies: Dict[str, FrozenSet[Technology]] = {}
|
||||||
|
for ingredient_name in all_ingredient_names:
|
||||||
|
required_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
|
||||||
|
|
||||||
|
advancement_technologies: Set[str] = set()
|
||||||
|
for technologies in required_technologies.values():
|
||||||
|
advancement_technologies |= {technology.name for technology in technologies}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||||
|
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies
|
||||||
from .Technologies import tech_table, requirements, ingredients, all_ingredients, recipe_sources
|
from .Shapes import get_shapes
|
||||||
|
|
||||||
static_nodes = {"automation", "logistics"}
|
|
||||||
|
|
||||||
|
|
||||||
def gen_factorio(world: MultiWorld, player: int):
|
def gen_factorio(world: MultiWorld, player: int):
|
||||||
|
static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option?
|
||||||
for tech_name, tech_id in tech_table.items():
|
for tech_name, tech_id in tech_table.items():
|
||||||
tech_item = Item(tech_name, True, tech_id, player)
|
tech_item = Item(tech_name, tech_name in advancement_technologies, tech_id, player)
|
||||||
tech_item.game = "Factorio"
|
tech_item.game = "Factorio"
|
||||||
if tech_name in static_nodes:
|
if tech_name in static_nodes:
|
||||||
loc = world.get_location(tech_name, player)
|
loc = world.get_location(tech_name, player)
|
||||||
loc.item = tech_item
|
loc.item = tech_item
|
||||||
loc.locked = loc.event = True
|
loc.locked = True
|
||||||
|
loc.event = tech_item.advancement
|
||||||
else:
|
else:
|
||||||
world.itempool.append(tech_item)
|
world.itempool.append(tech_item)
|
||||||
set_rules(world, player)
|
set_rules(world, player)
|
||||||
@@ -30,30 +28,25 @@ def factorio_create_regions(world: MultiWorld, player: int):
|
|||||||
tech = Location(player, tech_name, tech_id, nauvis)
|
tech = Location(player, tech_name, tech_id, nauvis)
|
||||||
nauvis.locations.append(tech)
|
nauvis.locations.append(tech)
|
||||||
tech.game = "Factorio"
|
tech.game = "Factorio"
|
||||||
for ingredient in all_ingredients: # register science packs as events
|
|
||||||
ingredient_location = Location(player, ingredient, 0, nauvis)
|
|
||||||
ingredient_location.item = Item(ingredient, True, 0, player)
|
|
||||||
ingredient_location.event = ingredient_location.locked = True
|
|
||||||
menu.locations.append(ingredient_location)
|
|
||||||
crash.connect(nauvis)
|
crash.connect(nauvis)
|
||||||
world.regions += [menu, nauvis]
|
world.regions += [menu, nauvis]
|
||||||
|
|
||||||
|
|
||||||
def set_rules(world: MultiWorld, player: int):
|
def set_rules(world: MultiWorld, player: int):
|
||||||
|
shapes = get_shapes(world, player)
|
||||||
if world.logic[player] != 'nologic':
|
if world.logic[player] != 'nologic':
|
||||||
from worlds.generic import Rules
|
from worlds.generic import Rules
|
||||||
for tech_name in tech_table:
|
allowed_packs = world.max_science_pack[player].get_allowed_packs()
|
||||||
# vanilla layout, to be implemented
|
for tech_name, technology in technology_table.items():
|
||||||
# rules = requirements.get(tech_name, set()) | ingredients.get(tech_name, set())
|
|
||||||
# loose nodes
|
# loose nodes
|
||||||
rules = ingredients.get(tech_name, set())
|
location = world.get_location(tech_name, player)
|
||||||
if rules:
|
Rules.set_rule(location, technology.build_rule(allowed_packs, player))
|
||||||
location = world.get_location(tech_name, player)
|
prequisites = shapes.get(tech_name)
|
||||||
Rules.set_rule(location, lambda state, rules=rules: all(state.has(rule, player) for rule in rules))
|
if prequisites:
|
||||||
|
locations = {world.get_location(requisite, player) for requisite in prequisites}
|
||||||
|
Rules.add_rule(location, lambda state,
|
||||||
|
locations=locations: all(state.can_reach(loc) for loc in locations))
|
||||||
|
|
||||||
for recipe, technology in recipe_sources.items():
|
# get all technologies
|
||||||
Rules.set_rule(world.get_location(recipe, player), lambda state, tech=technology: state.has(tech, player))
|
world.completion_condition[player] = lambda state: all(state.has(technology, player)
|
||||||
|
for technology in advancement_technologies)
|
||||||
|
|
||||||
world.completion_condition[player] = lambda state: all(state.has(ingredient, player)
|
|
||||||
for ingredient in all_ingredients)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user