mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-14 03:23:48 -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]
|
||||
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):
|
||||
# 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_locations = []
|
||||
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):
|
||||
def set_player_attr(attr, val):
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('tech_tree_layout_prerequisites', {})
|
||||
set_player_attr('_region_cache', {})
|
||||
set_player_attr('shuffle', "vanilla")
|
||||
set_player_attr('logic', "noglitches")
|
||||
set_player_attr('mode', 'open')
|
||||
set_player_attr('swords', 'random')
|
||||
set_player_attr('swordless', False)
|
||||
set_player_attr('difficulty', 'normal')
|
||||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
@@ -80,11 +93,6 @@ class MultiWorld():
|
||||
set_player_attr('powder_patch_required', False)
|
||||
set_player_attr('ganon_at_pyramid', 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_front', None)
|
||||
set_player_attr('can_access_trock_big_chest', None)
|
||||
@@ -136,13 +144,9 @@ class MultiWorld():
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
set_player_attr(hk_option, False)
|
||||
|
||||
self.worlds = []
|
||||
#for i in range(players):
|
||||
# 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()}
|
||||
# self.worlds = []
|
||||
# for i in range(players):
|
||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
@@ -726,7 +730,7 @@ class CollectionState(object):
|
||||
|
||||
def can_retrieve_tablet(self, player:int) -> bool:
|
||||
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)))
|
||||
|
||||
def has_sword(self, player: int) -> bool:
|
||||
@@ -747,7 +751,7 @@ class CollectionState(object):
|
||||
def can_melt_things(self, player: int) -> bool:
|
||||
return self.has('Fire Rod', player) or \
|
||||
(self.has('Bombos', player) and
|
||||
(self.world.swords[player] == "swordless" or
|
||||
(self.world.swordless[player] or
|
||||
self.has_sword(player)))
|
||||
|
||||
def can_avoid_lasers(self, player: int) -> bool:
|
||||
@@ -1111,7 +1115,7 @@ class Location():
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return getattr(self, "_hint_text", self.name)
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
|
||||
class Item():
|
||||
location: Optional[Location] = None
|
||||
@@ -1313,7 +1317,7 @@ class Spoiler(object):
|
||||
'dark_room_logic': self.world.dark_room_logic,
|
||||
'mode': self.world.mode,
|
||||
'retro': self.world.retro,
|
||||
'weapons': self.world.swords,
|
||||
'swordless': self.world.swordless,
|
||||
'goal': self.world.goal,
|
||||
'shuffle': self.world.shuffle,
|
||||
'item_pool': self.world.difficulty,
|
||||
@@ -1412,7 +1416,7 @@ class Spoiler(object):
|
||||
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||
outfile.write('Retro: %s\n' %
|
||||
('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])
|
||||
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
||||
outfile.write("Pieces available for Triforce: %s\n" %
|
||||
|
||||
@@ -311,12 +311,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
args['players'].sort()
|
||||
current_team = -1
|
||||
logger.info('Players:')
|
||||
for team, slot, name in args['players']:
|
||||
if team != current_team:
|
||||
logger.info(f' Team #{team + 1}')
|
||||
current_team = team
|
||||
logger.info(' %s (Player %d)' % (name, slot))
|
||||
if args["datapackage_version"] > network_data_package["version"]:
|
||||
for network_player in args['players']:
|
||||
if network_player.team != current_team:
|
||||
logger.info(f' Team #{network_player.team + 1}')
|
||||
current_team = network_player.team
|
||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
|
||||
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
@@ -356,7 +356,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
if msgs:
|
||||
await ctx.send_msgs(msgs)
|
||||
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.
|
||||
# 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 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):
|
||||
command_processor = FactorioCommandProcessor
|
||||
@@ -76,7 +81,8 @@ async def game_watcher(ctx: FactorioContext):
|
||||
researches_done_file = os.path.join(script_folder, "research_done.json")
|
||||
if os.path.exists(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:
|
||||
while 1:
|
||||
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 asyncio.sleep(1)
|
||||
else:
|
||||
research_logger.info("Did not find Factorio Bridge file.")
|
||||
await asyncio.sleep(5)
|
||||
bridge_counter += 1
|
||||
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:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
@@ -137,11 +146,12 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
if ctx.rcon_client:
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
item_id = ctx.items_received[ctx.send_index].item
|
||||
item_name = lookup_id_to_name[item_id]
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
|
||||
response = ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
|
||||
if response:
|
||||
factorio_server_logger.info(response)
|
||||
if item_id not in lookup_id_to_name:
|
||||
logging.error(f"Cannot send unknown item ID: {item_id}")
|
||||
else:
|
||||
item_name = lookup_id_to_name[item_id]
|
||||
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
|
||||
|
||||
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('--founditems', default=False, action='store_true',
|
||||
help='Show items found by other players for themselves.')
|
||||
parser.add_argument('--disable_web_ui', default=False, action='store_true',
|
||||
help="Turn off emitting a webserver for the webbrowser based user interface.")
|
||||
parser.add_argument('--web_ui', default=False, action='store_true',
|
||||
help="Emit a webserver for the webbrowser based user interface.")
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
if args.diff_file:
|
||||
@@ -1002,7 +1002,7 @@ async def main():
|
||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||
|
||||
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
|
||||
while True:
|
||||
port = randrange(49152, 65535)
|
||||
@@ -1015,7 +1015,7 @@ async def main():
|
||||
|
||||
ctx = Context(args.snes, args.connect, args.password, args.founditems, port)
|
||||
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),
|
||||
'localhost', port, ping_timeout=None, ping_interval=None)
|
||||
await ui_socket
|
||||
|
||||
204
Main.py
204
Main.py
@@ -10,8 +10,7 @@ import pickle
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
||||
from worlds.alttp import ALttPLocation
|
||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups
|
||||
from worlds.alttp.Items import ItemFactory, item_name_groups
|
||||
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
|
||||
lookup_vanilla_location_to_entrance
|
||||
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.ItemPool import generate_itempool, difficulties, fill_prizes
|
||||
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.factorio import gen_factorio, factorio_create_regions
|
||||
from worlds.factorio.Mod import generate_mod
|
||||
@@ -70,7 +69,7 @@ def main(args, seed=None):
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
world.mode = args.mode.copy()
|
||||
world.swords = args.swords.copy()
|
||||
world.swordless = args.swordless.copy()
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
@@ -135,6 +134,8 @@ def main(args, seed=None):
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
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.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:
|
||||
rom_name = future.result()
|
||||
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
|
||||
slot, team, rom_name in rom_names}
|
||||
|
||||
@@ -498,24 +504,27 @@ def main(args, seed=None):
|
||||
for player, name in enumerate(team, 1):
|
||||
if player not in world.alttp_player_ids:
|
||||
connect_names[name] = (i, player)
|
||||
multidata = zlib.compress(pickle.dumps({"names": parsed_names,
|
||||
"connect_names": connect_names,
|
||||
"remote_items": {player for player in range(1, world.players + 1) if
|
||||
world.remote_items[player] or
|
||||
world.game[player] != "A Link to the Past"},
|
||||
"locations": {
|
||||
(location.address, location.player):
|
||||
(location.item.code, location.item.player)
|
||||
for location in world.get_filled_locations() if
|
||||
type(location.address) is int},
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"version": tuple(_version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
}), 9)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps({
|
||||
"games": games,
|
||||
"names": parsed_names,
|
||||
"connect_names": connect_names,
|
||||
"remote_items": {player for player in range(1, world.players + 1) if
|
||||
world.remote_items[player] or
|
||||
world.game[player] != "A Link to the Past"},
|
||||
"locations": {
|
||||
(location.address, location.player):
|
||||
(location.item.code, location.item.player)
|
||||
for location in world.get_filled_locations() if
|
||||
type(location.address) is int},
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"version": tuple(_version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
}), 9)
|
||||
|
||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
@@ -542,155 +551,8 @@ def main(args, seed=None):
|
||||
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):
|
||||
"""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
|
||||
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
|
||||
state_cache = [None]
|
||||
|
||||
@@ -31,7 +31,7 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||
_version_tuple, restricted_loads, Version
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||
|
||||
colorama.init()
|
||||
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.save_dirty = False
|
||||
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):
|
||||
with open(multidatapath, 'rb') as f:
|
||||
@@ -119,21 +120,23 @@ class Context(Node):
|
||||
self._load(self._decompress(data), use_embedded_server_options)
|
||||
self.data_filename = multidatapath
|
||||
|
||||
def _decompress(self, data: bytes) -> dict:
|
||||
@staticmethod
|
||||
def _decompress(data: bytes) -> dict:
|
||||
format_version = data[0]
|
||||
if format_version != 1:
|
||||
raise Exception("Incompatible multidata.")
|
||||
return restricted_loads(zlib.decompress(data[1:]))
|
||||
|
||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > Utils._version_tuple:
|
||||
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
|
||||
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 = {}
|
||||
for team, player, version in clients_ver:
|
||||
self.minimum_client_versions[team, player] = Utils.Version(*version)
|
||||
for player, version in clients_ver.items():
|
||||
self.minimum_client_versions[player] = Utils.Version(*version)
|
||||
|
||||
for team, names in enumerate(decoded_obj['names']):
|
||||
for player, name in enumerate(names, 1):
|
||||
@@ -144,12 +147,13 @@ class Context(Node):
|
||||
self.locations = decoded_obj['locations']
|
||||
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()}
|
||||
self.games = decoded_obj["games"]
|
||||
if use_embedded_server_options:
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
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):
|
||||
for key, value in server_options.items():
|
||||
@@ -331,11 +335,14 @@ async def server(websocket, path, ctx: Context):
|
||||
ctx.endpoints.append(client)
|
||||
|
||||
try:
|
||||
logging.info("Incoming")
|
||||
if ctx.log_network:
|
||||
logging.info("Incoming connection")
|
||||
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:
|
||||
logging.info(data)
|
||||
if ctx.log_network:
|
||||
logging.info(f"Incoming message: {data}")
|
||||
for msg in decode(data):
|
||||
await process_client_cmd(ctx, client, msg)
|
||||
except Exception as e:
|
||||
@@ -350,7 +357,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
await ctx.send_msgs(client, [{
|
||||
'cmd': 'RoomInfo',
|
||||
'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],
|
||||
# tags are for additional features in the communication.
|
||||
# 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):
|
||||
parts = []
|
||||
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
||||
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)
|
||||
if net_item.player == receiving_player:
|
||||
NetUtils.add_json_text(parts, " found their ")
|
||||
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_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, net_item.location, type=NetUtils.JSONTypes.location_id)
|
||||
NetUtils.add_json_text(parts, ")")
|
||||
@@ -794,7 +806,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_missing_checks(self.ctx, self.client)
|
||||
|
||||
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")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
@@ -967,6 +979,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
errors.add('InvalidSlot')
|
||||
else:
|
||||
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
|
||||
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
|
||||
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.team = team
|
||||
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']:
|
||||
errors.add('IncompatibleVersion')
|
||||
|
||||
if ctx.compatibility == 1 and "AP" not in args['tags']:
|
||||
errors.add('IncompatibleVersion')
|
||||
#only exact version match allowed
|
||||
# only exact version match allowed
|
||||
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||
errors.add('IncompatibleVersion')
|
||||
if errors:
|
||||
@@ -1004,7 +1019,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
"team": client.team, "slot": client.slot,
|
||||
"players": ctx.get_players_package(),
|
||||
"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)
|
||||
if 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
|
||||
#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()
|
||||
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,
|
||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility)
|
||||
|
||||
ctx.log_network = args.log_network
|
||||
data_filename = args.multidata
|
||||
|
||||
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["pre_rolled"] = vars(settings).copy()
|
||||
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"]:
|
||||
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)
|
||||
for k, v in vars(settings).items():
|
||||
if v is not None:
|
||||
@@ -294,7 +298,8 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
name_counter[name] += 1
|
||||
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],
|
||||
NUMBER=(name_counter[name] if name_counter[name] > 1 else ''),
|
||||
NUMBER=(name_counter[name] if name_counter[
|
||||
name] > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
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',
|
||||
'none': 'none',
|
||||
'basic': 'basic',
|
||||
'normal': 'normal',
|
||||
'full': 'full',
|
||||
'chaos': 'chaos',
|
||||
'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:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
|
||||
logging.debug(f'Applying {new_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.')
|
||||
return weights
|
||||
|
||||
|
||||
def roll_linked_options(weights: dict) -> dict:
|
||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||
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"])
|
||||
if "rom_options" in option_set:
|
||||
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
|
||||
else:
|
||||
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
|
||||
return weights
|
||||
|
||||
|
||||
def roll_triggers(weights: dict) -> dict:
|
||||
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.
|
||||
for option_set in weights["triggers"]:
|
||||
for i, option_set in enumerate(weights["triggers"]):
|
||||
try:
|
||||
key = get_choice("option_name", option_set)
|
||||
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 "options" in option_set:
|
||||
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
|
||||
|
||||
if "rom_options" in option_set:
|
||||
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[key] = result
|
||||
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
|
||||
return weights
|
||||
|
||||
|
||||
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:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
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
|
||||
bosses = []
|
||||
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:
|
||||
remainder_shuffle = boss_shuffle_options[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.")
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
|
||||
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"]:
|
||||
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():
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
|
||||
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:
|
||||
raise Exception(f"Unsupported game {ret.game}")
|
||||
return ret
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
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'
|
||||
|
||||
goal = get_choice('goals', weights, 'ganon')
|
||||
ret.goal = {'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'
|
||||
}[goal]
|
||||
|
||||
if goal in legacy_goals:
|
||||
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
|
||||
goal = legacy_goals[goal]
|
||||
ret.goal = goals[goal]
|
||||
|
||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||
# 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.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[get_choice('weapons', weights, 'assured')]
|
||||
ret.swordless = get_choice('swordless', weights, False)
|
||||
|
||||
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.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice('tile_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.sprite = "Link"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -99,6 +99,7 @@ class Node:
|
||||
def __init__(self):
|
||||
self.endpoints = []
|
||||
super(Node, self).__init__()
|
||||
self.log_network = 0
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
@@ -114,6 +115,9 @@ class Node:
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
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):
|
||||
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
|
||||
@@ -123,6 +127,9 @@ class Node:
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
|
||||
63
Options.py
63
Options.py
@@ -23,6 +23,7 @@ class AssembleOptions(type):
|
||||
class Option(metaclass=AssembleOptions):
|
||||
value: int
|
||||
name_lookup: typing.Dict[int, str]
|
||||
default = 0
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.get_option_name()})"
|
||||
@@ -47,6 +48,7 @@ class Option(metaclass=AssembleOptions):
|
||||
class Toggle(Option):
|
||||
option_false = 0
|
||||
option_true = 1
|
||||
default = 0
|
||||
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
@@ -86,6 +88,7 @@ class Toggle(Option):
|
||||
def get_option_name(self):
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class Choice(Option):
|
||||
def __init__(self, value: int):
|
||||
self.value: int = value
|
||||
@@ -101,7 +104,9 @@ class Choice(Option):
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
@@ -231,7 +236,61 @@ hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
|
||||
"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__":
|
||||
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.
|
||||
|
||||
Currently, the following games are supported:
|
||||
* 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).
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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).
|
||||
|
||||
|
||||
13
Utils.py
13
Utils.py
@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
|
||||
minor: int
|
||||
build: int
|
||||
|
||||
__version__ = "0.0.2"
|
||||
__version__ = "0.0.3"
|
||||
_version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
@@ -84,9 +84,13 @@ def local_path(*path):
|
||||
# cx_Freeze
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||
else:
|
||||
# we are running in a normal Python environment
|
||||
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)
|
||||
|
||||
@@ -190,6 +194,7 @@ def get_default_options() -> dict:
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
"log_network": 0
|
||||
},
|
||||
"multi_mystery_options": {
|
||||
"teams": 1,
|
||||
@@ -385,7 +390,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
def find_class(self, module, name):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus"}:
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
||||
import NetUtils
|
||||
return getattr(NetUtils, name)
|
||||
# Forbid everything else.
|
||||
|
||||
@@ -46,6 +46,7 @@ app.config["PONY"] = {
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "simple"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||
|
||||
app.autoversion = True
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import zlib
|
||||
import pickle
|
||||
|
||||
|
||||
from .models import *
|
||||
|
||||
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):
|
||||
@@ -27,7 +27,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
self.ctx.save()
|
||||
self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}")
|
||||
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.save()
|
||||
self.output(f"Registered Youtube Stream for {user}")
|
||||
@@ -81,16 +81,16 @@ class WebHostContext(Context):
|
||||
def init_save(self, enabled: bool = True):
|
||||
self.saving = enabled
|
||||
if self.saving:
|
||||
existing_savegame = Room.get(id=self.room_id).multisave
|
||||
if existing_savegame:
|
||||
self.set_save(existing_savegame)
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self._start_async_saving()
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
|
||||
@db_session
|
||||
def _save(self, exit_save:bool = False) -> bool:
|
||||
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
|
||||
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()
|
||||
|
||||
@@ -16,7 +16,7 @@ def download_patch(room_id, patch_id):
|
||||
room = Room.get(id=room_id)
|
||||
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)
|
||||
|
||||
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) %}
|
||||
{% if room.seed.patches %}
|
||||
<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) }}">
|
||||
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -8,6 +8,7 @@ from uuid import UUID
|
||||
from worlds.alttp import Items, Regions
|
||||
from WebHostLib import app, cache, Room
|
||||
from NetUtils import Hint
|
||||
from Utils import restricted_loads
|
||||
|
||||
|
||||
def get_id(item_name):
|
||||
@@ -253,7 +254,7 @@ for item_name, data in Items.item_table.items():
|
||||
big_key_ids[area] = data[2]
|
||||
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):
|
||||
@@ -295,9 +296,9 @@ def get_static_room_data(room: Room):
|
||||
result = _multidata_cache.get(room.seed.id, None)
|
||||
if 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
|
||||
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
|
||||
locations = multidata['locations']
|
||||
names = multidata["names"]
|
||||
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():
|
||||
seed_checks_in_area[area] += len(checks)
|
||||
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"][f'{playernumber}'][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
|
||||
for areaname in ordered_areas}
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||
for areaname in ordered_areas}
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
||||
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)}
|
||||
for _, (item_id, item_player) in multidata["locations"]:
|
||||
for _, (item_id, item_player) in locations.items():
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
||||
if item_id in ids_small_key:
|
||||
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
|
||||
return result
|
||||
|
||||
@@ -348,7 +343,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
abort(404)
|
||||
|
||||
# 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]
|
||||
seed_checks_in_area = seed_checks_in_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}
|
||||
|
||||
# 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:
|
||||
for item_id in starting_items:
|
||||
attribute_item_solo(inventory, item_id)
|
||||
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
|
||||
# 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}")
|
||||
# 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
|
||||
|
||||
# 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
|
||||
if ms_team != tracked_team or ms_player != tracked_player:
|
||||
continue
|
||||
@@ -484,7 +484,8 @@ def getTracker(tracker: UUID):
|
||||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
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)}
|
||||
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}
|
||||
for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
precollected_items = room.seed.multidata.get("precollected_items", None)
|
||||
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if "hints" in room.multisave:
|
||||
for key, hintdata in room.multisave["hints"]:
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
if "hints" in multisave:
|
||||
for key, hintdata in multisave["hints"]:
|
||||
for hint in hintdata:
|
||||
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:
|
||||
precollected = precollected_items[player - 1]
|
||||
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]["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:
|
||||
inventory[team][player][106] = 1 # Triforce
|
||||
|
||||
@@ -525,7 +530,7 @@ def getTracker(tracker: UUID):
|
||||
|
||||
activity_timers = {}
|
||||
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)
|
||||
|
||||
player_names = {}
|
||||
@@ -533,12 +538,12 @@ def getTracker(tracker: UUID):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[(team, player)] = name
|
||||
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
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||
|
||||
video = {}
|
||||
for (team, player), data in room.multisave.get("video", []):
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
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 zipfile
|
||||
import logging
|
||||
import MultiServer
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
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."
|
||||
elif file.filename.endswith(".apbp"):
|
||||
splitted = file.filename.split("/")[-1][3:].split("P", 1)
|
||||
player = int(splitted[1].split(".")[0].split("_")[0])
|
||||
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
|
||||
player_id, player_name = splitted[1].split(".")[0].split("_")
|
||||
patches.add(Patch(data=zfile.open(file, "r").read(), player_name=player_name, player_id=player_id))
|
||||
elif file.filename.endswith(".txt"):
|
||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||
elif file.filename.endswith(".archipelago"):
|
||||
try:
|
||||
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
|
||||
multidata = zfile.open(file).read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
else:
|
||||
multidata = zfile.open(file).read()
|
||||
if multidata:
|
||||
commit() # commit patches
|
||||
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.")
|
||||
else:
|
||||
try:
|
||||
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
|
||||
multidata = file.read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
raise
|
||||
else:
|
||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||
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
|
||||
script.on_event(defines.events.on_tick, function(event)
|
||||
if event.tick%600 == 0 then
|
||||
dumpTech()
|
||||
dumpTech(game.forces["player"])
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
game.print("Research done")
|
||||
dumpTech()
|
||||
local technology = event.research
|
||||
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)
|
||||
|
||||
function dumpTech()
|
||||
|
||||
local force = game.forces["player"]
|
||||
function dumpTech(force)
|
||||
local data_collection = {}
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.researched and string.find(tech_name, "ap-") == 1 then
|
||||
@@ -30,7 +54,7 @@ function dumpGameInfo()
|
||||
local data_collection = {}
|
||||
local force = game.forces["player"]
|
||||
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 unlocks = {}
|
||||
tech_data["unlocks"] = unlocks
|
||||
@@ -89,7 +113,8 @@ commands.add_command("ap-get-info-dump", "Dump Game Info, used by Archipelago.",
|
||||
end)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
require('lib')
|
||||
|
||||
local technologies = data.raw["technology"]
|
||||
local original_tech
|
||||
local new_tree_copy
|
||||
allowed_ingredients = {}
|
||||
{%- for ingredient in allowed_science_packs %}
|
||||
allowed_ingredients["{{ingredient}}"]= 1
|
||||
{% endfor %}
|
||||
local template_tech = table.deepcopy(technologies["automation"])
|
||||
{#- ensure the copy unlocks nothing #}
|
||||
template_tech.unlocks = {}
|
||||
template_tech.upgrade = false
|
||||
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 #}
|
||||
{%- for original_tech_name, item_name, receiving_player in locations %}
|
||||
original_tech = technologies["{{original_tech_name}}"]
|
||||
{#- the tech researched by the local player #}
|
||||
new_tree_copy = table.deepcopy(template_tech)
|
||||
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 #}
|
||||
original_tech.enabled = false
|
||||
{#- copy original tech costs #}
|
||||
new_tree_copy.unit = table.deepcopy(original_tech.unit)
|
||||
{% if item_name in tech_table %}
|
||||
prep_copy(new_tree_copy, original_tech)
|
||||
{% if tech_cost != 1 %}
|
||||
if new_tree_copy.unit.count then
|
||||
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||
end
|
||||
{% endif %}
|
||||
{% if item_name in tech_table and visibility %}
|
||||
{#- copy Factorio Technology Icon #}
|
||||
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
|
||||
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.icon_size = 512
|
||||
{% 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}
|
||||
|
||||
{% endfor %}
|
||||
@@ -1,8 +1,17 @@
|
||||
|
||||
[technology-name]
|
||||
{% 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 }}
|
||||
{%- else %}
|
||||
ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
[technology-description]
|
||||
{% 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 %}
|
||||
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
|
||||
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
|
||||
compatibility: 2
|
||||
# log all server traffic, mostly for dev use
|
||||
log_network: 0
|
||||
# Options for MultiMystery.py
|
||||
multi_mystery_options:
|
||||
# Teams
|
||||
|
||||
@@ -27,6 +27,7 @@ game:
|
||||
A Link to the Past: 1
|
||||
Hollow Knight: 1
|
||||
Factorio: 1
|
||||
Minecraft: 1
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
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:
|
||||
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.
|
||||
# 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:
|
||||
### Logic Section ###
|
||||
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version
|
||||
@@ -172,11 +201,9 @@ retro:
|
||||
hints:
|
||||
'on': 50 # Hint tiles sometimes give item location hints
|
||||
'off': 0 # Hint tiles provide gameplay tips
|
||||
weapons: # Specifically, swords
|
||||
randomized: 0 # Swords are placed randomly throughout the world
|
||||
assured: 50 # 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 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
swordless:
|
||||
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
off: 1
|
||||
item_pool:
|
||||
easy: 0 # Doubled upgrades, progressives, and etc
|
||||
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
|
||||
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
|
||||
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
|
||||
### Shop Settings ###
|
||||
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
random: 0 # 0 to 30 evenly distributed
|
||||
shop_shuffle:
|
||||
none: 50
|
||||
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
|
||||
retro:
|
||||
- on # Never play retro seeds
|
||||
weapons:
|
||||
- swordless # Never play a swordless seed
|
||||
swordless:
|
||||
- on # Never play a swordless seed
|
||||
linked_options:
|
||||
- name: crosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
@@ -326,9 +355,9 @@ linked_options:
|
||||
- name: enemizer
|
||||
options:
|
||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||
simple: 1
|
||||
basic: 1
|
||||
full: 1
|
||||
random: 1
|
||||
chaos: 1
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
@@ -339,12 +368,46 @@ linked_options:
|
||||
expert: 1
|
||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||
# 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:
|
||||
# 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_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)
|
||||
options: # then inserts these options
|
||||
swords: assured
|
||||
swordless: off
|
||||
### 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
|
||||
vanilla: 50 # Everything should be like in vanilla
|
||||
|
||||
@@ -7,4 +7,5 @@ prompt_toolkit>=3.0.18
|
||||
appdirs>=1.4.4
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
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",
|
||||
"MultiMystery.py": "ArchipelagoMultiMystery",
|
||||
"MultiServer.py": "ArchipelagoServer",
|
||||
"gui.py": "ArchipelagoLttPCreator",
|
||||
"Mystery.py": "ArchipelagoMystery",
|
||||
"LttPAdjuster.py": "ArchipelagoLttPAdjuster",
|
||||
"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 .hk.Items import lookup_id_to_name as hk
|
||||
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 .hk import Locations
|
||||
|
||||
lookup_any_location_id_to_name = {**Regions.lookup_id_to_name, **Locations.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()}
|
||||
|
||||
|
||||
|
||||
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,
|
||||
"version": 2}
|
||||
"version": 4}
|
||||
|
||||
|
||||
@enum.unique
|
||||
class Games(str, enum.Enum):
|
||||
HK = "Hollow Knight"
|
||||
LTTP = "A Link to the Past"
|
||||
Factorio = "Factorio"
|
||||
|
||||
Minecraft = "Minecraft"
|
||||
|
||||
@@ -80,7 +80,7 @@ def KholdstareDefeatRule(state, player: int):
|
||||
state.has('Fire Rod', player) or
|
||||
(
|
||||
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
|
||||
(
|
||||
@@ -89,7 +89,7 @@ def KholdstareDefeatRule(state, player: int):
|
||||
(
|
||||
state.has('Fire Rod', player) and
|
||||
state.has('Bombos', player) and
|
||||
state.world.swords[player] == 'swordless' and
|
||||
state.world.swordless[player] and
|
||||
state.can_extend_magic(player, 16)
|
||||
)
|
||||
)
|
||||
@@ -113,7 +113,7 @@ def AgahnimDefeatRule(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 \
|
||||
state.has_fire_source(player) and \
|
||||
state.has('Silver Bow', player) and \
|
||||
@@ -241,7 +241,7 @@ def place_bosses(world, player: int):
|
||||
if shuffle_mode == "none":
|
||||
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
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
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
|
||||
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='''\
|
||||
Select sword placement. (default: %(default)s)
|
||||
Random: All swords placed randomly.
|
||||
Assured: Start game with a sword already.
|
||||
Toggles Swordless Mode
|
||||
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
|
||||
Tower are removed, Agahnim\'s Tower barrier can be
|
||||
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
|
||||
(and Book). Bombos pads have been added in Ice
|
||||
Palace, to allow for an alternative to firerod.
|
||||
Vanilla: Swords are in vanilla locations.
|
||||
''')
|
||||
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
|
||||
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):
|
||||
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',
|
||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
|
||||
@@ -70,7 +70,7 @@ difficulties = {
|
||||
basicmagic=['Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'],
|
||||
progressivesword=['Progressive Sword'] * 8,
|
||||
basicsword=['Master Sword', 'Tempered Sword', 'Golden Sword', 'Fighter Sword'] * 2,
|
||||
progressivebow=["Progressive Bow"] * 2,
|
||||
progressivebow=["Progressive Bow"] * 4,
|
||||
basicbow=['Bow', 'Silver Bow'] * 2,
|
||||
timedohko=['Green Clock'] * 25,
|
||||
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.basicbow)
|
||||
itempool.extend(itemdiff.basicarmor)
|
||||
if world.swords[player] != 'swordless':
|
||||
if not world.swordless[player]:
|
||||
itempool.extend(itemdiff.basicsword)
|
||||
itempool.extend(itemdiff.basicmagic)
|
||||
itempool.extend(itemdiff.basicglove)
|
||||
@@ -335,7 +335,7 @@ def generate_itempool(world, player: int):
|
||||
possible_weapons = []
|
||||
for item in pool:
|
||||
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
|
||||
possible_weapons.append(item)
|
||||
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]
|
||||
goal = world.goal[player]
|
||||
mode = world.mode[player]
|
||||
swords = world.swords[player]
|
||||
swordless = world.swordless[player]
|
||||
retro = world.retro[player]
|
||||
logic = world.logic[player]
|
||||
|
||||
@@ -619,7 +619,7 @@ def get_pool_core(world, player: int):
|
||||
|
||||
if want_progressives():
|
||||
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']
|
||||
if difficulty == "easy":
|
||||
swordless_bows *= 2
|
||||
@@ -627,33 +627,11 @@ def get_pool_core(world, player: int):
|
||||
else:
|
||||
pool.extend(diff.basicbow)
|
||||
|
||||
if swords == 'swordless':
|
||||
if 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:
|
||||
progressive_swords = want_progressives()
|
||||
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)
|
||||
|
||||
@@ -684,7 +662,7 @@ def get_pool_core(world, player: int):
|
||||
else:
|
||||
break
|
||||
|
||||
if goal == 'pedestal' and swords != 'vanilla':
|
||||
if goal == 'pedestal':
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
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])
|
||||
|
||||
# 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(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
|
||||
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,
|
||||
difficulty.progressive_shield_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])
|
||||
|
||||
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_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
||||
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
|
||||
|
||||
# set swordless mode settings
|
||||
rom.write_byte(0x18003F, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer can harm ganon
|
||||
rom.write_byte(0x180040, 0x01 if world.swords[player] == 'swordless' else 0x00) # open curtains
|
||||
rom.write_byte(0x180041, 0x01 if world.swords[player] == 'swordless' else 0x00) # swordless medallions
|
||||
rom.write_byte(0x180043, 0xFF if world.swords[player] == 'swordless' else 0x00) # starting sword for link
|
||||
rom.write_byte(0x180044, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer activates tablets
|
||||
rom.write_byte(0x18003F, 0x01 if world.swordless[player] else 0x00) # hammer can harm ganon
|
||||
rom.write_byte(0x180040, 0x01 if world.swordless[player] else 0x00) # open curtains
|
||||
rom.write_byte(0x180041, 0x01 if world.swordless[player] else 0x00) # swordless medallions
|
||||
rom.write_byte(0x180043, 0xFF if world.swordless[player] else 0x00) # starting sword for link
|
||||
rom.write_byte(0x180044, 0x01 if world.swordless[player] else 0x00) # hammer activates tablets
|
||||
|
||||
if world.item_functionality[player] == 'easy':
|
||||
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
|
||||
@@ -1651,7 +1651,7 @@ def write_custom_shops(rom, world, player):
|
||||
if item is None:
|
||||
break
|
||||
if not item['item'] in item_table: # item not native to ALTTP
|
||||
item_code = 0x21
|
||||
item_code = 0x09 # Hammer
|
||||
else:
|
||||
item_code = ItemFactory(item['item'], player).code
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
restore_maseya_colors(rom, offsets_array)
|
||||
if mode == 'default':
|
||||
@@ -2218,7 +2218,7 @@ def write_strings(rom, world, player, team):
|
||||
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)
|
||||
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:
|
||||
prog_bow_locs.remove(distinguished_prog_bow_loc)
|
||||
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])
|
||||
|
||||
if world.swords[player] == 'swordless':
|
||||
if world.swordless[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])
|
||||
|
||||
if world.swords[player] == 'swordless':
|
||||
if world.swordless[player]:
|
||||
swordless_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 - 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_entrance('Sewers Back Door', 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"""
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Optional
|
||||
import threading
|
||||
import json
|
||||
@@ -8,6 +9,7 @@ import json
|
||||
import jinja2
|
||||
import Utils
|
||||
import shutil
|
||||
import Options
|
||||
from BaseClasses import MultiWorld
|
||||
from .Technologies import tech_table
|
||||
|
||||
@@ -29,26 +31,41 @@ def generate_mod(world: MultiWorld, player: int):
|
||||
global template, locale_template
|
||||
with template_load_lock:
|
||||
if not template:
|
||||
template = jinja2.Template(open(Utils.local_path("data", "factorio", "mod_template", "data-final-fixes.lua")).read())
|
||||
locale_template = jinja2.Template(open(Utils.local_path("data", "factorio", "mod_template", "locale", "en", "locale.cfg")).read())
|
||||
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
|
||||
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
|
||||
player_names = {x: world.player_names[x][0] for x in world.player_ids}
|
||||
locations = []
|
||||
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))
|
||||
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,
|
||||
"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)
|
||||
mod_dir = Utils.output_path(mod_name)+"_"+Utils.__version__
|
||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||
os.makedirs(en_locale_dir, 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:
|
||||
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)
|
||||
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
||||
f.write(locale_content)
|
||||
@@ -56,3 +73,14 @@ def generate_mod(world: MultiWorld, player: int):
|
||||
info["name"] = mod_name
|
||||
with open(os.path.join(mod_dir, "info.json"), "wt") as f:
|
||||
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
|
||||
from typing import Dict
|
||||
import os
|
||||
from typing import Dict, Set, FrozenSet
|
||||
import json
|
||||
|
||||
import Utils
|
||||
import logging
|
||||
|
||||
factorio_id = 2**17
|
||||
|
||||
factorio_id = 2 ** 17
|
||||
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:
|
||||
raw = json.load(f)
|
||||
with open(recipe_source_file) as f:
|
||||
raw_recipes = json.load(f)
|
||||
tech_table = {}
|
||||
technology_table:Dict[str, Technology] = {}
|
||||
|
||||
requirements = {}
|
||||
ingredients = {}
|
||||
all_ingredients = set()
|
||||
always = lambda state: True
|
||||
|
||||
|
||||
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
|
||||
for technology in sorted(raw):
|
||||
data = raw[technology]
|
||||
tech_table[technology] = factorio_id
|
||||
for technology_name in sorted(raw):
|
||||
data = raw[technology_name]
|
||||
factorio_id += 1
|
||||
if data["requires"]:
|
||||
requirements[technology] = set(data["requires"])
|
||||
current_ingredients = set(data["ingredients"])-starting_ingredient_recipes
|
||||
if current_ingredients:
|
||||
current_ingredients = set(data["ingredients"])
|
||||
technology = Technology(technology_name, current_ingredients)
|
||||
tech_table[technology_name] = technology.factorio_id
|
||||
technology_table[technology_name] = technology
|
||||
|
||||
all_ingredients |= current_ingredients
|
||||
current_ingredients = {"recipe-"+ingredient for ingredient in current_ingredients}
|
||||
ingredients[technology] = current_ingredients
|
||||
|
||||
recipe_sources = {}
|
||||
recipe_sources: Dict[str, str] = {} # recipe_name -> technology source
|
||||
|
||||
for technology, data in raw.items():
|
||||
recipe_source = all_ingredients & set(data["unlocks"])
|
||||
for recipe in recipe_source:
|
||||
recipe_sources["recipe-"+recipe] = technology
|
||||
for recipe_name in data["unlocks"]:
|
||||
recipe_sources.setdefault(recipe_name, set()).add(technology)
|
||||
|
||||
all_ingredients = {"recipe-"+ingredient for ingredient in all_ingredients}
|
||||
del(raw)
|
||||
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
||||
del (raw)
|
||||
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 .Technologies import tech_table, requirements, ingredients, all_ingredients, recipe_sources
|
||||
|
||||
static_nodes = {"automation", "logistics"}
|
||||
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies
|
||||
from .Shapes import get_shapes
|
||||
|
||||
|
||||
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():
|
||||
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"
|
||||
if tech_name in static_nodes:
|
||||
loc = world.get_location(tech_name, player)
|
||||
loc.item = tech_item
|
||||
loc.locked = loc.event = True
|
||||
loc.locked = True
|
||||
loc.event = tech_item.advancement
|
||||
else:
|
||||
world.itempool.append(tech_item)
|
||||
set_rules(world, player)
|
||||
@@ -30,30 +28,25 @@ def factorio_create_regions(world: MultiWorld, player: int):
|
||||
tech = Location(player, tech_name, tech_id, nauvis)
|
||||
nauvis.locations.append(tech)
|
||||
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)
|
||||
world.regions += [menu, nauvis]
|
||||
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
shapes = get_shapes(world, player)
|
||||
if world.logic[player] != 'nologic':
|
||||
from worlds.generic import Rules
|
||||
for tech_name in tech_table:
|
||||
# vanilla layout, to be implemented
|
||||
# rules = requirements.get(tech_name, set()) | ingredients.get(tech_name, set())
|
||||
allowed_packs = world.max_science_pack[player].get_allowed_packs()
|
||||
for tech_name, technology in technology_table.items():
|
||||
# loose nodes
|
||||
rules = ingredients.get(tech_name, set())
|
||||
if rules:
|
||||
location = world.get_location(tech_name, player)
|
||||
Rules.set_rule(location, lambda state, rules=rules: all(state.has(rule, player) for rule in rules))
|
||||
location = world.get_location(tech_name, player)
|
||||
Rules.set_rule(location, technology.build_rule(allowed_packs, player))
|
||||
prequisites = shapes.get(tech_name)
|
||||
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():
|
||||
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(ingredient, player)
|
||||
for ingredient in all_ingredients)
|
||||
# get all technologies
|
||||
world.completion_condition[player] = lambda state: all(state.has(technology, player)
|
||||
for technology in advancement_technologies)
|
||||
|
||||
Reference in New Issue
Block a user