mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4813fcac08 | ||
|
|
e50db61030 | ||
|
|
f8c3b695d0 | ||
|
|
431b64c574 | ||
|
|
f06d160615 | ||
|
|
909172cbad | ||
|
|
382c6d0445 | ||
|
|
4efd27694a | ||
|
|
fa24fd31d0 | ||
|
|
c55983af5f | ||
|
|
9c3d12dc55 | ||
|
|
37755cd362 | ||
|
|
eb02d65dbb | ||
|
|
4d38f44da3 | ||
|
|
212abc2b5a | ||
|
|
3797c20488 | ||
|
|
2f7e532f4f | ||
|
|
eb2a3009f4 | ||
|
|
5c9aa09c80 | ||
|
|
e5bbcb8d27 | ||
|
|
cf488e5a5d | ||
|
|
36ef9e8a72 | ||
|
|
e7d254aed7 | ||
|
|
298f2f652a | ||
|
|
5cb2689609 | ||
|
|
9ab5ec426d | ||
|
|
77d3bf9172 | ||
|
|
328f132498 | ||
|
|
de2ead3a9b | ||
|
|
ff96b391b9 | ||
|
|
4a75d27261 | ||
|
|
05e464e379 | ||
|
|
e8e141b206 | ||
|
|
bed8fe82cf | ||
|
|
3a1d33f499 | ||
|
|
6ea68dd290 | ||
|
|
97030590c2 | ||
|
|
60f64cc46b | ||
|
|
5087b78c28 | ||
|
|
95358bc523 | ||
|
|
4fc1ce77ac | ||
|
|
b8c7d6a72f | ||
|
|
e04fbd1d77 | ||
|
|
fb8229fda5 | ||
|
|
569e0e3004 | ||
|
|
73ed18c11d | ||
|
|
65df153947 | ||
|
|
2dd6dcab20 | ||
|
|
4494207717 | ||
|
|
88265c5585 | ||
|
|
501c55cc26 | ||
|
|
a5efed83b9 | ||
|
|
e7a746c06c | ||
|
|
063997610d | ||
|
|
432ae5865d | ||
|
|
73bc5fb376 | ||
|
|
a7c9474a37 | ||
|
|
0cf9baef4b | ||
|
|
6a06117786 | ||
|
|
ee30914b2c | ||
|
|
a995627e98 | ||
|
|
7884c6cd97 | ||
|
|
4fe10b88b3 | ||
|
|
b7327138f3 | ||
|
|
433981fd3d | ||
|
|
2df7e4e537 | ||
|
|
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 |
162
BaseClasses.py
162
BaseClasses.py
@@ -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,22 @@ 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])
|
||||
self.remote_items = self.AttributeProxy(lambda player: self.game[player] != "A Link to the Past")
|
||||
|
||||
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)
|
||||
@@ -74,17 +88,11 @@ class MultiWorld():
|
||||
set_player_attr('retro', False)
|
||||
set_player_attr('hints', True)
|
||||
set_player_attr('player_names', [])
|
||||
set_player_attr('remote_items', False)
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
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)
|
||||
@@ -135,14 +143,12 @@ class MultiWorld():
|
||||
import Options
|
||||
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.custom_data = {}
|
||||
for player in range(1, players+1):
|
||||
self.custom_data[player] = {}
|
||||
# self.worlds = []
|
||||
# for i in range(players):
|
||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
@@ -163,6 +169,11 @@ class MultiWorld():
|
||||
def factorio_player_ids(self):
|
||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
|
||||
|
||||
@property
|
||||
def minecraft_player_ids(self):
|
||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
|
||||
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||
|
||||
@@ -227,7 +238,8 @@ class MultiWorld():
|
||||
ret = CollectionState(self)
|
||||
|
||||
def soft_collect(item):
|
||||
if item.name.startswith('Progressive '):
|
||||
if item.game == "A Link to the Past" and item.name.startswith('Progressive '):
|
||||
# ALttP items
|
||||
if 'Sword' in item.name:
|
||||
if ret.has('Golden Sword', item.player):
|
||||
pass
|
||||
@@ -317,7 +329,7 @@ class MultiWorld():
|
||||
if location.can_fill(self.state, item, False):
|
||||
location.item = item
|
||||
item.location = location
|
||||
item.world = self # try to not have this here anymore
|
||||
item.world = self # try to not have this here anymore
|
||||
if collect:
|
||||
self.state.collect(item, location.event, location)
|
||||
|
||||
@@ -399,24 +411,24 @@ class MultiWorld():
|
||||
if self.has_beaten_game(self.state):
|
||||
return True
|
||||
state = CollectionState(self)
|
||||
prog_locations = {location for location in self.get_locations() if location.item is not None and (
|
||||
location.item.advancement or location.event) and location not in state.locations_checked}
|
||||
prog_locations = {location for location in self.get_locations() if location.item
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
while prog_locations:
|
||||
sphere = []
|
||||
sphere = set()
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
for location in prog_locations:
|
||||
if location.can_reach(state):
|
||||
sphere.append(location)
|
||||
sphere.add(location)
|
||||
|
||||
if not sphere:
|
||||
# ran out of places and did not finish yet, quit
|
||||
return False
|
||||
|
||||
for location in sphere:
|
||||
prog_locations.remove(location)
|
||||
state.collect(location.item, True, location)
|
||||
prog_locations -= sphere
|
||||
|
||||
if self.has_beaten_game(state):
|
||||
return True
|
||||
@@ -726,7 +738,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 +759,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:
|
||||
@@ -800,6 +812,91 @@ class CollectionState(object):
|
||||
rules.append(self.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
|
||||
# Minecraft logic functions
|
||||
def has_iron_ingots(self, player: int):
|
||||
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
|
||||
|
||||
def has_gold_ingots(self, player: int):
|
||||
return self.has('Ingot Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
|
||||
|
||||
def has_diamond_pickaxe(self, player: int):
|
||||
return self.has('Progressive Tools', player, 3) and self.has_iron_ingots(player)
|
||||
|
||||
def craft_crossbow(self, player: int):
|
||||
return self.has('Archery', player) and self.has_iron_ingots(player)
|
||||
|
||||
def has_bottle_mc(self, player: int):
|
||||
return self.has('Bottles', player) and self.has('Ingot Crafting', player)
|
||||
|
||||
def can_enchant(self, player: int):
|
||||
return self.has('Enchanting', player) and self.has_diamond_pickaxe(player) # mine obsidian and lapis
|
||||
|
||||
def can_use_anvil(self, player: int):
|
||||
return self.has('Enchanting', player) and self.has('Resource Blocks', player) and self.has_iron_ingots(player)
|
||||
|
||||
def fortress_loot(self, player: int): # saddles, blaze rods, wither skulls
|
||||
return self.can_reach('Nether Fortress', 'Region', player) and self.basic_combat(player)
|
||||
|
||||
def can_brew_potions(self, player: int):
|
||||
return self.fortress_loot(player) and self.has('Brewing', player) and self.has_bottle_mc(player)
|
||||
|
||||
def can_piglin_trade(self, player: int):
|
||||
return self.has_gold_ingots(player) and (self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region', player))
|
||||
|
||||
def enter_stronghold(self, player: int):
|
||||
return self.fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def combat_difficulty(self, player: int):
|
||||
return self.world.combat_difficulty[player].get_option_name()
|
||||
|
||||
def can_adventure(self, player: int):
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player)
|
||||
elif self.combat_difficulty(player) == 'hard':
|
||||
return True
|
||||
return self.has('Progressive Weapons', player) and (self.has('Ingot Crafting', player) or self.has('Campfire', player))
|
||||
|
||||
def basic_combat(self, player: int):
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \
|
||||
self.has('Shield', player) and self.has_iron_ingots(player)
|
||||
elif self.combat_difficulty(player) == 'hard':
|
||||
return True
|
||||
return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self.has_iron_ingots(player)
|
||||
|
||||
def complete_raid(self, player: int):
|
||||
reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player)
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return reach_regions and \
|
||||
self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \
|
||||
self.has('Shield', player) and self.has('Archery', player) and \
|
||||
self.has('Progressive Tools', player, 2) and self.has_iron_ingots(player)
|
||||
elif self.combat_difficulty(player) == 'hard': # might be too hard?
|
||||
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
|
||||
(self.has('Progressive Armor', player) or self.has('Shield', player))
|
||||
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
|
||||
self.has('Progressive Armor', player) and self.has('Shield', player)
|
||||
|
||||
def can_kill_wither(self, player: int):
|
||||
build_wither = self.fortress_loot(player) and (self.can_reach('The Nether', 'Region', player) or self.can_piglin_trade(player))
|
||||
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return build_wither and normal_kill and self.has('Archery', player)
|
||||
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
|
||||
return build_wither and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
|
||||
return build_wither and normal_kill
|
||||
|
||||
def can_kill_ender_dragon(self, player: int):
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.has('Archery', player) and \
|
||||
self.can_brew_potions(player) and self.can_enchant(player)
|
||||
if self.combat_difficulty(player) == 'hard':
|
||||
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
||||
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
|
||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
|
||||
|
||||
|
||||
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
@@ -870,7 +967,7 @@ class CollectionState(object):
|
||||
def remove(self, item):
|
||||
if item.advancement:
|
||||
to_remove = item.name
|
||||
if to_remove.startswith('Progressive '):
|
||||
if item.game == "A Link to the Past" and to_remove.startswith('Progressive '):
|
||||
if 'Sword' in to_remove:
|
||||
if self.has('Golden Sword', item.player):
|
||||
to_remove = 'Golden Sword'
|
||||
@@ -897,7 +994,7 @@ class CollectionState(object):
|
||||
elif self.has('Blue Shield', item.player):
|
||||
to_remove = 'Blue Shield'
|
||||
else:
|
||||
to_remove = 'None'
|
||||
to_remove = None
|
||||
elif 'Bow' in item.name:
|
||||
if self.has('Silver Bow', item.player):
|
||||
to_remove = 'Silver Bow'
|
||||
@@ -906,7 +1003,7 @@ class CollectionState(object):
|
||||
else:
|
||||
to_remove = None
|
||||
|
||||
if to_remove is not None:
|
||||
if to_remove:
|
||||
|
||||
self.prog_items[to_remove, item.player] -= 1
|
||||
if self.prog_items[to_remove, item.player] < 1:
|
||||
@@ -1111,7 +1208,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 +1410,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,
|
||||
@@ -1397,6 +1494,11 @@ class Spoiler(object):
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
res = getattr(self.world, hk_option)[player]
|
||||
outfile.write(f'{hk_option+":":33}{res}\n')
|
||||
if player in self.world.minecraft_player_ids:
|
||||
import Options
|
||||
for mc_option in Options.minecraft_options:
|
||||
res = getattr(self.world, mc_option)[player]
|
||||
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
||||
if player in self.world.alttp_player_ids:
|
||||
for team in range(self.world.teams):
|
||||
outfile.write('%s%s\n' % (
|
||||
@@ -1412,7 +1514,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" %
|
||||
|
||||
@@ -129,7 +129,7 @@ class CommonContext():
|
||||
self.input_requests = 0
|
||||
|
||||
# game state
|
||||
self.player_names: typing.Dict[int: str] = {}
|
||||
self.player_names: typing.Dict[int: str] = {0: "Server"}
|
||||
self.exit_event = asyncio.Event()
|
||||
self.watcher_event = asyncio.Event()
|
||||
|
||||
@@ -184,7 +184,6 @@ class CommonContext():
|
||||
async def disconnect(self):
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.server.socket.close()
|
||||
self.ui_node.send_connection_status(self)
|
||||
if self.server_task is not None:
|
||||
await self.server_task
|
||||
|
||||
@@ -195,6 +194,7 @@ class CommonContext():
|
||||
|
||||
def consume_players_package(self, package: typing.List[tuple]):
|
||||
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
|
||||
self.player_names[0] = "Server"
|
||||
|
||||
def event_invalid_slot(self):
|
||||
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
|
||||
@@ -213,6 +213,14 @@ class CommonContext():
|
||||
await self.disconnect()
|
||||
self.server_task = asyncio.create_task(server_loop(self, address))
|
||||
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
|
||||
|
||||
async def server_loop(ctx: CommonContext, address=None):
|
||||
ui_node = getattr(ctx, "ui_node", None)
|
||||
@@ -311,12 +319,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 +364,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.
|
||||
@@ -394,12 +402,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
ctx.hint_points = args['hint_points']
|
||||
|
||||
elif cmd == 'Print':
|
||||
logger.info(args["text"])
|
||||
ctx.on_print(args)
|
||||
|
||||
elif cmd == 'PrintJSON':
|
||||
if not ctx.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
logger.info(ctx.jsontotextparser(args["data"]))
|
||||
ctx.on_print_json(args)
|
||||
|
||||
elif cmd == 'InvalidArguments':
|
||||
logger.warning(f"Invalid Arguments: {args['text']}")
|
||||
|
||||
@@ -2,16 +2,18 @@ import os
|
||||
import logging
|
||||
import json
|
||||
import string
|
||||
import copy
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import colorama
|
||||
import asyncio
|
||||
from queue import Queue, Empty
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
||||
from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
import random
|
||||
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus
|
||||
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
|
||||
@@ -33,10 +35,9 @@ if not os.path.exists(executable):
|
||||
else:
|
||||
raise FileNotFoundError(executable)
|
||||
|
||||
script_folder = options["factorio_options"]["script-output"]
|
||||
|
||||
threadpool = ThreadPoolExecutor(10)
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
@mark_raw
|
||||
def _cmd_factorio(self, text: str) -> bool:
|
||||
@@ -48,6 +49,12 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _cmd_connect(self, address: str = "") -> bool:
|
||||
"""Connect to a MultiWorld Server"""
|
||||
if not self.ctx.auth:
|
||||
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
||||
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
||||
|
||||
|
||||
class FactorioContext(CommonContext):
|
||||
command_processor = FactorioCommandProcessor
|
||||
@@ -56,13 +63,11 @@ class FactorioContext(CommonContext):
|
||||
super(FactorioContext, self).__init__(*args, **kwargs)
|
||||
self.send_index = 0
|
||||
self.rcon_client = None
|
||||
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
if self.auth is None:
|
||||
logging.info('Enter the name of your slot to join this game:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils._version_tuple,
|
||||
@@ -70,34 +75,61 @@ class FactorioContext(CommonContext):
|
||||
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
||||
}])
|
||||
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
if self.rcon_client:
|
||||
cleaned_text = args['text'].replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
||||
|
||||
async def game_watcher(ctx: FactorioContext):
|
||||
research_logger = logging.getLogger("FactorioWatcher")
|
||||
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
|
||||
def on_print_json(self, args: dict):
|
||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
if self.rcon_client:
|
||||
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
||||
|
||||
async def game_watcher(ctx: FactorioContext, bridge_file: str):
|
||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
bridge_counter = 0
|
||||
try:
|
||||
while 1:
|
||||
if os.path.exists(researches_done_file):
|
||||
research_logger.info("Found Factorio Bridge file.")
|
||||
if os.path.exists(bridge_file):
|
||||
bridge_logger.info("Found Factorio Bridge file.")
|
||||
while 1:
|
||||
with open(researches_done_file) as f:
|
||||
with open(bridge_file) as f:
|
||||
data = json.load(f)
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in data if tech_name.startswith("ap-")}
|
||||
research_data = data["research_done"]
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
victory = data["victory"]
|
||||
ctx.auth = data["slot_name"]
|
||||
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if ctx.locations_checked != research_data:
|
||||
research_logger.info(f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
bridge_logger.info(f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
ctx.locations_checked = research_data
|
||||
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:
|
||||
bridge_logger.info(
|
||||
"Did not find Factorio Bridge file, "
|
||||
"waiting for mod to run, which requires the server to run, "
|
||||
"which requires a player to be connected.")
|
||||
bridge_counter = 0
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
|
||||
|
||||
def stream_factorio_output(pipe, queue):
|
||||
def queuer():
|
||||
while 1:
|
||||
@@ -124,6 +156,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
factorio_queue = Queue()
|
||||
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
||||
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
||||
script_folder = None
|
||||
try:
|
||||
while 1:
|
||||
while not factorio_queue.empty():
|
||||
@@ -134,17 +167,28 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
# trigger lua interface confirmation
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/ap-sync")
|
||||
if not script_folder and "Write data path:" in msg:
|
||||
script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
|
||||
bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json")
|
||||
if os.path.exists(bridge_file):
|
||||
os.remove(bridge_file)
|
||||
logging.info(f"Bridge File Path: {bridge_file}")
|
||||
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
||||
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)
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
item_id = transfer_item.item
|
||||
player_name = ctx.player_names[transfer_item.player]
|
||||
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 from {player_name}.")
|
||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
|
||||
ctx.send_index += 1
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
@@ -158,14 +202,13 @@ async def main():
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
await asyncio.sleep(3)
|
||||
watcher_task = asyncio.create_task(game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
ctx.snes_reconnect_address = None
|
||||
|
||||
await asyncio.gather(watcher_task, input_task, factorio_server_task)
|
||||
await asyncio.gather(input_task, factorio_server_task)
|
||||
|
||||
if ctx.server is not None and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
|
||||
5
Gui.py
5
Gui.py
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# module is planned to be removed
|
||||
from argparse import Namespace
|
||||
from glob import glob
|
||||
import json
|
||||
@@ -1745,7 +1746,8 @@ def update_sprites(task, on_finish=None):
|
||||
try:
|
||||
task.update_status("Determining needed sprites")
|
||||
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
|
||||
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
|
||||
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
|
||||
for sprite in sprites_arr if sprite["author"] != "Nintendo"]
|
||||
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
|
||||
|
||||
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
|
||||
@@ -1909,6 +1911,7 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||
|
||||
return image.zoom(2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if "update_sprites" in sys.argv:
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -3,7 +3,7 @@ MIT License
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2021 Berserker66
|
||||
Copyright (c) 2021 CaitSith2
|
||||
Copyright (c) 2020 LegendaryLinux
|
||||
Copyright (c) 2021 LegendaryLinux
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -5,6 +5,10 @@ import logging
|
||||
import textwrap
|
||||
import sys
|
||||
import time
|
||||
from tkinter import Tk
|
||||
|
||||
from Gui import update_sprites
|
||||
from GuiUtils import BackgroundTaskProgress
|
||||
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings
|
||||
from Utils import output_path
|
||||
@@ -16,46 +20,67 @@ class AdjusterWorld(object):
|
||||
self.sprite_pool = {1: sprite_pool}
|
||||
self.rom_seeds = {1: random}
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
def _get_help_string(self, action):
|
||||
return textwrap.dedent(action.help)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP rom to adjust.')
|
||||
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
|
||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?',
|
||||
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
||||
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||
help='''\
|
||||
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
|
||||
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||
help='''\
|
||||
Hide the triforce hud in certain circumstances.
|
||||
hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
|
||||
(Both can be revealed when speaking to Murahalda)
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--enableflashing', help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)', action='store_false', dest="reduceflashing")
|
||||
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
parser.add_argument('--enableflashing',
|
||||
help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)',
|
||||
action='store_false', dest="reduceflashing")
|
||||
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?',
|
||||
choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
help='''\
|
||||
Select the rate at which the heart beep sound is played at
|
||||
low health. (default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?',
|
||||
choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--link_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--shield_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--sword_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--hud_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--ow_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--link_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--shield_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--sword_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--hud_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--uw_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
@@ -64,8 +89,11 @@ def main():
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--names', default='', type=str)
|
||||
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.update_sprites:
|
||||
run_sprite_update()
|
||||
sys.exit()
|
||||
# set up logger
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
||||
args.loglevel]
|
||||
@@ -99,13 +127,13 @@ def adjust(args):
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||
palettes_options={}
|
||||
palettes_options['dungeon']=args.uw_palettes
|
||||
palettes_options = {}
|
||||
palettes_options['dungeon'] = args.uw_palettes
|
||||
|
||||
palettes_options['overworld']=args.ow_palettes
|
||||
palettes_options['hud']=args.hud_palettes
|
||||
palettes_options['sword']=args.sword_palettes
|
||||
palettes_options['shield']=args.shield_palettes
|
||||
palettes_options['overworld'] = args.ow_palettes
|
||||
palettes_options['hud'] = args.hud_palettes
|
||||
palettes_options['sword'] = args.sword_palettes
|
||||
palettes_options['shield'] = args.shield_palettes
|
||||
# palettes_options['link']=args.link_palettesvera
|
||||
|
||||
racerom = rom.read_byte(0x180213) > 0
|
||||
@@ -123,6 +151,7 @@ def adjust(args):
|
||||
|
||||
return args, path
|
||||
|
||||
|
||||
def adjustGUI():
|
||||
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \
|
||||
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
|
||||
@@ -148,6 +177,7 @@ def adjustGUI():
|
||||
def RomSelect2():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")])
|
||||
romVar2.set(rom)
|
||||
|
||||
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
|
||||
romDialogFrame.pack(side=TOP, expand=True, fill=X)
|
||||
baseRomLabel2.pack(side=LEFT)
|
||||
@@ -198,5 +228,16 @@ def adjustGUI():
|
||||
adjustWindow.mainloop()
|
||||
|
||||
|
||||
def run_sprite_update():
|
||||
import threading
|
||||
done = threading.Event()
|
||||
top = Tk()
|
||||
top.withdraw()
|
||||
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||
while not done.isSet():
|
||||
top.update()
|
||||
print("Done updating sprites")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -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
|
||||
|
||||
248
Main.py
248
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,12 +22,14 @@ 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
|
||||
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
|
||||
from worlds.minecraft.Regions import minecraft_create_regions
|
||||
from worlds.generic.Rules import locality_rules
|
||||
from worlds import Games
|
||||
from worlds import Games, lookup_any_item_name_to_id
|
||||
import Patch
|
||||
|
||||
seeddigits = 20
|
||||
@@ -70,7 +71,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()
|
||||
@@ -88,7 +89,6 @@ def main(args, seed=None):
|
||||
|
||||
world.hints = args.hints.copy()
|
||||
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
@@ -135,6 +135,10 @@ 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, {}))
|
||||
for minecraft_option in Options.minecraft_options:
|
||||
setattr(world, minecraft_option, getattr(args, minecraft_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)}
|
||||
@@ -166,15 +170,15 @@ def main(args, seed=None):
|
||||
world.player_names[player].append(name)
|
||||
|
||||
logger.info('')
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
|
||||
world.push_precollected(item)
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
item = ItemFactory(tok.strip(), player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
# enforce pre-defined local items.
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
@@ -206,12 +210,15 @@ def main(args, seed=None):
|
||||
for player in world.factorio_player_ids:
|
||||
factorio_create_regions(world, player)
|
||||
|
||||
for player in world.minecraft_player_ids:
|
||||
minecraft_create_regions(world, player)
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
if world.open_pyramid[player] == 'goal':
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
||||
elif world.open_pyramid[player] == 'auto':
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
|
||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon)
|
||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
|
||||
else:
|
||||
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
|
||||
|
||||
@@ -265,6 +272,9 @@ def main(args, seed=None):
|
||||
for player in world.factorio_player_ids:
|
||||
gen_factorio(world, player)
|
||||
|
||||
for player in world.minecraft_player_ids:
|
||||
gen_minecraft(world, player)
|
||||
|
||||
logger.info("Running Item Plando")
|
||||
|
||||
for item in world.itempool:
|
||||
@@ -304,9 +314,7 @@ def main(args, seed=None):
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
logger.info('Generating output files.')
|
||||
|
||||
outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
rom_names = []
|
||||
|
||||
def _gen_rom(team: int, player: int):
|
||||
@@ -416,7 +424,8 @@ def main(args, seed=None):
|
||||
for player in world.alttp_player_ids:
|
||||
rom_futures.append(pool.submit(_gen_rom, team, player))
|
||||
for player in world.factorio_player_ids:
|
||||
mod_futures.append(pool.submit(generate_mod, world, player))
|
||||
mod_futures.append(pool.submit(generate_mod, world, player,
|
||||
str(args.outputname if args.outputname else world.seed)))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
@@ -479,9 +488,9 @@ def main(args, seed=None):
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
precollected_items = [[] for player in range(world.players)]
|
||||
precollected_items = {player: [] for player in range(1, world.players+1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player - 1].append(item.code)
|
||||
precollected_items[item.player].append(item.code)
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
@@ -490,7 +499,13 @@ def main(args, seed=None):
|
||||
for future in roms:
|
||||
rom_name = future.result()
|
||||
rom_names.append(rom_name)
|
||||
minimum_versions = {"server": (0, 0, 2)}
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 0, 4), "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 +513,34 @@ 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)
|
||||
for slot in world.hk_player_ids:
|
||||
slots_data = slot_data[slot] = {}
|
||||
for option_name in Options.hollow_knight_options:
|
||||
option = getattr(world, option_name)[slot]
|
||||
slots_data[option_name] = int(option.value)
|
||||
for slot in world.minecraft_player_ids:
|
||||
slot_data[slot] = fill_minecraft_slot_data(world, slot)
|
||||
multidata = zlib.compress(pickle.dumps({
|
||||
"slot_data" : slot_data,
|
||||
"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]},
|
||||
"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,
|
||||
"seed_name": str(args.outputname if args.outputname else world.seed)
|
||||
}), 9)
|
||||
|
||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
@@ -532,9 +557,11 @@ def main(args, seed=None):
|
||||
if multidata_task:
|
||||
multidata_task.result() # retrieve exception if one exists
|
||||
pool.shutdown() # wait for all queued tasks to complete
|
||||
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
|
||||
generate_mc_data(world, player, str(args.outputname if args.outputname else world.seed))
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
create_playthrough(world)
|
||||
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
@@ -542,155 +569,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]
|
||||
|
||||
@@ -5,6 +5,7 @@ import threading
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
|
||||
|
||||
def feedback(text: str):
|
||||
@@ -24,7 +25,7 @@ if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
|
||||
from Utils import get_public_ipv4, get_options
|
||||
|
||||
from Mystery import get_seed_name
|
||||
from Patch import create_patch_file
|
||||
|
||||
options = get_options()
|
||||
@@ -89,10 +90,11 @@ if __name__ == "__main__":
|
||||
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
|
||||
else:
|
||||
logging.info(f"{target_player_count} Players found.")
|
||||
|
||||
seed_name = get_seed_name(random)
|
||||
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
|
||||
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
|
||||
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\""
|
||||
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\" " \
|
||||
f"--seed_name {seed_name}"
|
||||
|
||||
if create_spoiler:
|
||||
command += " --create_spoiler"
|
||||
@@ -117,15 +119,9 @@ if __name__ == "__main__":
|
||||
start = time.perf_counter()
|
||||
text = subprocess.check_output(command, shell=True).decode()
|
||||
logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
|
||||
seedname = ""
|
||||
|
||||
for segment in text.split():
|
||||
if segment.startswith("M"):
|
||||
seedname = segment
|
||||
break
|
||||
|
||||
multidataname = f"AP_{seedname}.archipelago"
|
||||
spoilername = f"AP_{seedname}_Spoiler.txt"
|
||||
multidataname = f"AP_{seed_name}.archipelago"
|
||||
spoilername = f"AP_{seed_name}_Spoiler.txt"
|
||||
romfilename = ""
|
||||
|
||||
if player_name:
|
||||
@@ -162,7 +158,7 @@ if __name__ == "__main__":
|
||||
logging.info(f"Removed {file} which is now present in the zipfile")
|
||||
|
||||
|
||||
zipname = os.path.join(output_path, f"AP_{seedname}.{typical_zip_ending}")
|
||||
zipname = os.path.join(output_path, f"AP_{seed_name}.{typical_zip_ending}")
|
||||
|
||||
logging.info(f"Creating zipfile {zipname}")
|
||||
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
|
||||
@@ -186,7 +182,7 @@ if __name__ == "__main__":
|
||||
futures = []
|
||||
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
|
||||
for file in os.listdir(output_path):
|
||||
if seedname in file:
|
||||
if seed_name in file:
|
||||
if file.endswith(".sfc"):
|
||||
futures.append(pool.submit(_handle_sfc_file, file))
|
||||
elif file.endswith(".apbp"):
|
||||
|
||||
@@ -29,9 +29,9 @@ from worlds.alttp import Items, Regions
|
||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_item_name_to_id, \
|
||||
lookup_any_location_id_to_name, lookup_any_location_name_to_id
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
_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,9 @@ 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] = {}
|
||||
self.seed_name = ""
|
||||
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
with open(multidatapath, 'rb') as f:
|
||||
@@ -119,37 +121,48 @@ 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):
|
||||
self.player_names[(team, player)] = name
|
||||
|
||||
self.seed_name = decoded_obj["seed_name"]
|
||||
self.connect_names = decoded_obj['connect_names']
|
||||
self.remote_items = decoded_obj['remote_items']
|
||||
self.locations = decoded_obj['locations']
|
||||
self.slot_data = decoded_obj['slot_data']
|
||||
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"]
|
||||
# award remote-items start inventory:
|
||||
for team in range(len(decoded_obj['names'])):
|
||||
for slot, item_codes in decoded_obj["precollected_items"].items():
|
||||
if slot in self.remote_items:
|
||||
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||
|
||||
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 +344,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 +366,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.
|
||||
@@ -360,7 +376,8 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'remaining_mode': ctx.remaining_mode,
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_version': network_data_package["version"]
|
||||
'datapackage_version': network_data_package["version"],
|
||||
'seed_name': ctx.seed_name
|
||||
}])
|
||||
|
||||
|
||||
@@ -463,8 +480,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
get_received_items(ctx, team, target_player).append(new_item)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
|
||||
@@ -515,7 +532,7 @@ def collect_hints_location(ctx: Context, team: int, slot: int, location: str) ->
|
||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{lookup_any_item_id_to_name[hint.item]} is " \
|
||||
f"at {get_location_name_from_address(hint.location)} " \
|
||||
f"at {get_location_name_from_id(hint.location)} " \
|
||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||
|
||||
if hint.entrance:
|
||||
@@ -525,10 +542,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 +816,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_location_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 +989,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,14 +1007,12 @@ 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
|
||||
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||
# only exact version match allowed
|
||||
if ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||
errors.add('IncompatibleVersion')
|
||||
if errors:
|
||||
logging.info(f"A client connection was refused due to: {errors}")
|
||||
@@ -1004,7 +1027,9 @@ 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),
|
||||
"slot_data": ctx.slot_data.get(client.slot, {})
|
||||
}]
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
|
||||
@@ -1030,20 +1055,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
elif cmd == 'LocationScouts':
|
||||
locs = []
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
|
||||
return
|
||||
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
||||
target_item, target_player = ctx.locations[location, client.slot]
|
||||
locs.append(NetworkItem(target_item, location, target_player))
|
||||
|
||||
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
|
||||
item_type = [i[1] for i in Items.item_table.values() if type(i[2]) is int and i[2] == target_item]
|
||||
if item_type:
|
||||
target_item = replacements.get(item_type[0], target_item)
|
||||
|
||||
locs.append([target_item, location, target_player])
|
||||
|
||||
# logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||
|
||||
elif cmd == 'StatusUpdate':
|
||||
@@ -1309,6 +1326,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 +1363,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:
|
||||
|
||||
148
Mystery.py
148
Mystery.py
@@ -54,13 +54,15 @@ def mystery_argparse():
|
||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||
parser.add_argument('--plando', default="bosses",
|
||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||
|
||||
parser.add_argument('--seed_name')
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||
return args
|
||||
|
||||
def get_seed_name(random):
|
||||
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
|
||||
|
||||
def main(args=None, callback=ERmain):
|
||||
if not args:
|
||||
@@ -68,9 +70,8 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
random.seed(seed)
|
||||
|
||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
||||
seed_name = args.seed_name if args.seed_name else get_seed_name(random)
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
|
||||
|
||||
if args.race:
|
||||
random.seed() # reset to time-based random source
|
||||
@@ -112,7 +113,7 @@ def main(args=None, callback=ERmain):
|
||||
erargs.glitch_triforce = args.glitch_triforce
|
||||
erargs.race = args.race
|
||||
erargs.skip_playthrough = args.skip_playthrough
|
||||
erargs.outputname = seedname
|
||||
erargs.outputname = seed_name
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.teams = args.teams
|
||||
|
||||
@@ -195,14 +196,18 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
pre_rolled = dict()
|
||||
pre_rolled["original_seed_number"] = seed
|
||||
pre_rolled["original_seed_name"] = seedname
|
||||
pre_rolled["original_seed_name"] = seed_name
|
||||
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 +299,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 +321,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 +370,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 +383,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 +393,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 +409,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 +435,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 +466,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 +482,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.")
|
||||
|
||||
@@ -480,17 +528,42 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
if isinstance(itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
ret.startinventory = startitems
|
||||
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, weights, plando_options)
|
||||
elif ret.game == "Hollow Knight":
|
||||
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:
|
||||
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
|
||||
setattr(ret, option_name, option.from_any(weights[option_name]))
|
||||
else:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||
else:
|
||||
setattr(ret, option_name, option(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(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 +606,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 +654,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 +665,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)
|
||||
@@ -647,23 +711,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if not ret.required_medallions[index]:
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
if item.startswith(('Progressive ', 'Small Key ', 'Rupee', 'Piece of Heart', 'Boss Heart Container',
|
||||
'Sanctuary Heart Container', 'Arrow', 'Bombs ', 'Bomb ', 'Bottle')) and isinstance(
|
||||
itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights, True)
|
||||
|
||||
ret.remote_items = get_choice('remote_items', weights, False)
|
||||
|
||||
if get_choice("local_keys", weights, "l" in dungeon_items):
|
||||
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
|
||||
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
|
||||
@@ -772,5 +821,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.quickswap = True
|
||||
ret.sprite = "Link"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
32
NetUtils.py
32
NetUtils.py
@@ -9,6 +9,7 @@ import websockets
|
||||
|
||||
from Utils import Version
|
||||
|
||||
|
||||
class JSONMessagePart(typing.TypedDict, total=False):
|
||||
text: str
|
||||
# optional
|
||||
@@ -18,7 +19,6 @@ class JSONMessagePart(typing.TypedDict, total=False):
|
||||
found: bool
|
||||
|
||||
|
||||
|
||||
class ClientStatus(enum.IntEnum):
|
||||
CLIENT_UNKNOWN = 0
|
||||
CLIENT_CONNECTED = 5
|
||||
@@ -61,10 +61,12 @@ _encode = JSONEncoder(
|
||||
def encode(obj):
|
||||
return _encode(_scan_for_TypedTuples(obj))
|
||||
|
||||
|
||||
def get_any_version(data: dict) -> Version:
|
||||
data = {key.lower(): value for key, value in data.items()} # .NET version classes have capitalized keys
|
||||
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
|
||||
|
||||
|
||||
whitelist = {"NetworkPlayer": NetworkPlayer,
|
||||
"NetworkItem": NetworkItem,
|
||||
}
|
||||
@@ -73,6 +75,7 @@ custom_hooks = {
|
||||
"Version": get_any_version
|
||||
}
|
||||
|
||||
|
||||
def _object_hook(o: typing.Any) -> typing.Any:
|
||||
if isinstance(o, dict):
|
||||
hook = custom_hooks.get(o.get("class", None), None)
|
||||
@@ -82,7 +85,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
|
||||
if cls:
|
||||
for key in tuple(o):
|
||||
if key not in cls._fields:
|
||||
del(o[key])
|
||||
del (o[key])
|
||||
return cls(**o)
|
||||
|
||||
return o
|
||||
@@ -99,6 +102,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 +118,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 +130,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:
|
||||
@@ -144,11 +154,16 @@ class HandlerMeta(type):
|
||||
handlers = attrs["handlers"] = {}
|
||||
trigger: str = "_handle_"
|
||||
for base in bases:
|
||||
handlers.update(base.commands)
|
||||
handlers.update(base.handlers)
|
||||
handlers.update({handler_name[len(trigger):]: method for handler_name, method in attrs.items() if
|
||||
handler_name.startswith(trigger)})
|
||||
|
||||
orig_init = attrs.get('__init__', None)
|
||||
if not orig_init:
|
||||
for base in bases:
|
||||
orig_init = getattr(base, '__init__', None)
|
||||
if orig_init:
|
||||
break
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# turn functions into bound methods
|
||||
@@ -160,6 +175,7 @@ class HandlerMeta(type):
|
||||
attrs['__init__'] = __init__
|
||||
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class JSONTypes(str, enum.Enum):
|
||||
color = "color"
|
||||
text = "text"
|
||||
@@ -171,6 +187,7 @@ class JSONTypes(str, enum.Enum):
|
||||
location_id = "location_id"
|
||||
entrance_name = "entrance_name"
|
||||
|
||||
|
||||
class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
@@ -229,6 +246,11 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
return self._handle_color(node)
|
||||
|
||||
|
||||
class RawJSONtoTextParser(JSONtoTextParser):
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
return self._handle_text(node)
|
||||
|
||||
|
||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||
@@ -274,7 +296,7 @@ class Hint(typing.NamedTuple):
|
||||
add_json_text(parts, " is at ")
|
||||
add_json_text(parts, self.location, type="location_id")
|
||||
add_json_text(parts, " in ")
|
||||
add_json_text(parts, self.finding_player, type ="player_id")
|
||||
add_json_text(parts, self.finding_player, type="player_id")
|
||||
if self.entrance:
|
||||
add_json_text(parts, "'s World at ")
|
||||
add_json_text(parts, self.entrance, type="entrance_name")
|
||||
@@ -285,4 +307,4 @@ class Hint(typing.NamedTuple):
|
||||
else:
|
||||
add_json_text(parts, ".")
|
||||
|
||||
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
|
||||
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
|
||||
|
||||
133
Options.py
133
Options.py
@@ -3,7 +3,7 @@ import typing
|
||||
|
||||
|
||||
class AssembleOptions(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
options = attrs["options"] = {}
|
||||
name_lookup = attrs["name_lookup"] = {}
|
||||
for base in bases:
|
||||
@@ -17,12 +17,13 @@ class AssembleOptions(type):
|
||||
# apply aliases, without name_lookup
|
||||
options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("alias_")})
|
||||
return super(AssembleOptions, cls).__new__(cls, name, bases, attrs)
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
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
|
||||
@@ -100,8 +103,41 @@ class Choice(Option):
|
||||
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
return cls.from_text(data)
|
||||
def from_any(cls, data: typing.Any) -> Choice:
|
||||
if type(data) == int and data in cls.options.values():
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
|
||||
class OptionNameSet(Option):
|
||||
default = frozenset()
|
||||
|
||||
def __init__(self, value: typing.Set[str]):
|
||||
self.value: typing.Set[str] = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> OptionNameSet:
|
||||
return cls({option.strip() for option in text.split(",")})
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> OptionNameSet:
|
||||
if type(data) == set:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
|
||||
class OptionDict(Option):
|
||||
default = {}
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
self.value: typing.Dict[str, typing.Any] = value
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||
if type(data) == dict:
|
||||
return cls(data)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -231,7 +267,94 @@ 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 = 1
|
||||
|
||||
|
||||
class FactorioStartItems(OptionDict):
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"tech_cost": TechCost,
|
||||
"free_samples": FreeSamples,
|
||||
"visibility": Visibility,
|
||||
"random_tech_ingredients": Toggle,
|
||||
"starting_items": FactorioStartItems}
|
||||
|
||||
|
||||
class AdvancementGoal(Choice):
|
||||
option_few = 0
|
||||
option_normal = 1
|
||||
option_many = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class CombatDifficulty(Choice):
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
default = 1
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
"include_hard_advancements": Toggle,
|
||||
"include_insane_advancements": Toggle,
|
||||
"include_postgame_advancements": Toggle,
|
||||
"shuffle_structures": Toggle
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# [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
|
||||
* Minecraft
|
||||
|
||||
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 +27,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).
|
||||
|
||||
|
||||
20
Utils.py
20
Utils.py
@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
|
||||
minor: int
|
||||
build: int
|
||||
|
||||
__version__ = "0.0.2"
|
||||
__version__ = "0.1.0"
|
||||
_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)
|
||||
|
||||
@@ -166,7 +170,6 @@ def get_default_options() -> dict:
|
||||
},
|
||||
"factorio_options": {
|
||||
"executable": "factorio\\bin\\x64\\factorio",
|
||||
"script-output": "factorio\\script-output",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
@@ -190,6 +193,7 @@ def get_default_options() -> dict:
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
"log_network": 0
|
||||
},
|
||||
"multi_mystery_options": {
|
||||
"teams": 1,
|
||||
@@ -267,14 +271,14 @@ def get_options() -> dict:
|
||||
return get_options.options
|
||||
|
||||
|
||||
def get_item_name_from_id(code):
|
||||
def get_item_name_from_id(code: int) -> str:
|
||||
from worlds import lookup_any_item_id_to_name
|
||||
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
|
||||
|
||||
|
||||
def get_location_name_from_address(address):
|
||||
def get_location_name_from_id(code: int) -> str:
|
||||
from worlds import lookup_any_location_id_to_name
|
||||
return lookup_any_location_id_to_name.get(address, f'Unknown location (ID:{address})')
|
||||
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
|
||||
|
||||
|
||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||
@@ -385,7 +389,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"
|
||||
|
||||
137
WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md
Normal file
137
WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Minecraft Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
### Server Host
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Players
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Dedicated Server Setup
|
||||
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
|
||||
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
|
||||
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
|
||||
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
|
||||
|
||||
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
|
||||
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
|
||||
- This will create the appropriate directories for you to place the files in the following step.
|
||||
|
||||
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
|
||||
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!
|
||||
|
||||
### Basic Player Setup
|
||||
- Purchase and install Minecraft from the above link.
|
||||
|
||||
**You're Done**.
|
||||
|
||||
Players only need to have a Vanilla unmodified version of Minecraft to play!
|
||||
|
||||
### Advanced Player Setup
|
||||
***This is not required to play a randomized minecraft game.***
|
||||
however this recommended as it helps make the experience more enjoyable.
|
||||
|
||||
#### Recomended Mods
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Install and run Minecraft from the link above at least once.
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install client**.
|
||||
- Start Minecraft forge at least once to create the directories needed for the next steps.
|
||||
3. Navigate to your minecraft install directory and place desired mods `.jar` file the in the `mods` directory.
|
||||
- The default install directories are as follows.
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how
|
||||
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
|
||||
each player to enjoy an experience customized for their taste, and different players in the same multiworld
|
||||
can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
A basic minecraft yaml will look like this.
|
||||
```yaml
|
||||
description: Template Name
|
||||
# Your name in-game. Spaces will be replaced with underscores and
|
||||
# there is a 16 character limit
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
|
||||
# Shared Options supported by all games:
|
||||
accessibility: locations
|
||||
progression_balancing: off
|
||||
# Minecraft Specific Options
|
||||
|
||||
# Number of advancements required (out of 92 total) to spawn the
|
||||
# Ender Dragon and complete the game.
|
||||
advancement_goal:
|
||||
few: 0 #30
|
||||
normal: 1 #50
|
||||
many: 0 #70
|
||||
|
||||
# Modifies the level of items logically required for exploring
|
||||
# dangerous areas and fighting bosses.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
|
||||
# Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Junk-fills extremely difficult advancements;
|
||||
# this is only How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Some advancements require defeating the Ender Dragon;
|
||||
# this will junk-fill them so you won't have to finish to send some items.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
#enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
shuffle_structures:
|
||||
on: 1
|
||||
off: 0
|
||||
```
|
||||
|
||||
For more detail on what each setting does check the default `PlayerSettings.yaml` that comes with the Archipelago install.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Minecraft data file
|
||||
**Only one yaml file needs to be submitted per minecraft world regardless of how many players play on it.**
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your data file, or with a zip file containing
|
||||
everyone's data files. Your data file should have a `.apmc` extension.
|
||||
|
||||
Put your data file in your forge servers `APData` folder. Make sure to remove any previous data file that was in there
|
||||
previously.
|
||||
|
||||
### Connect to the MultiServer
|
||||
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
|
||||
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
|
||||
|
||||
Once in game type `/connect <AP-Address> (<Password>)` where `<AP-Address>` is the address of the
|
||||
Archipelago server. `(<Password>)`
|
||||
is only required if the Archipleago server you are using has a password set.
|
||||
|
||||
### Play the game
|
||||
When the console tells you that you have joined the room, you're ready to begin playing. Congratulations
|
||||
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
|
||||
forge server.
|
||||
|
||||
121
WebHostLib/static/assets/tutorial/minecraft/minecraft_es.md
Normal file
121
WebHostLib/static/assets/tutorial/minecraft/minecraft_es.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Guia instalación de Minecraft Randomizer
|
||||
|
||||
## Software Requerido
|
||||
|
||||
### Servidor
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Jugadores
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Procedimiento de instalación
|
||||
|
||||
### Instalación de servidor dedicado
|
||||
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
|
||||
1. Descarga el instalador de **Minecraft Forge** 1.16.15 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
|
||||
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
|
||||
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
|
||||
|
||||
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
|
||||
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
|
||||
- Esto creara la estructura de directorios apropiada para el siguiente paso
|
||||
|
||||
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
|
||||
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
|
||||
|
||||
### Instalación basica para jugadores
|
||||
- Compra e instala Minecraft a traves del tercer enlace.
|
||||
**Y listo!**.
|
||||
Los jugadores solo necesitan una version no modificada de Minecraft para jugar!
|
||||
|
||||
### Instalación avanzada para jugadores
|
||||
***Esto no es requerido para jugar a minecraft randomizado.***
|
||||
Sin embargo lo recomendamos porque hace la experiencia mas llevadera.
|
||||
|
||||
#### Recomended Mods
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Instala y ejecuta Minecraft al menos una vez.
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elige **install client**.
|
||||
- Ejecuta Minecraft forge al menos una vez para generar los directorios necesarios para el siguiente paso.
|
||||
3. Navega a la carpeta de instalación de Minecraft y colocal los mods que quieras en el directorio `mods`
|
||||
- Los directorios por defecto de instalación son:
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Configura tu fichero YAML
|
||||
|
||||
### Que es un fichero YAML y potque necesito uno?
|
||||
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu juego.
|
||||
Cada jugador de un multiworld entregara u propio fichero YAML.
|
||||
Esto permite que cada jugador disfrute de una experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld
|
||||
pueden tener diferentes opciones
|
||||
|
||||
### Where do I get a YAML file?
|
||||
Un fichero basico yaml para minecraft tendra este aspecto.
|
||||
```yaml
|
||||
# Usado para describir tu yaml. Util si tienes multiples ficheros
|
||||
description: Template Name
|
||||
# Tu nombre en el juego. Los espacios son reemplazados por guiones bajos, limitado a 16 caracteres
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
accessibility: locations
|
||||
# Recomendado no activar esto ya que el pool de objetos de Minecraft es bastante escueto, ademas hay muchas maneras alternativas de obtener los objetivos de Minecraft.
|
||||
progression_balancing: off
|
||||
# Cuantos avances se necesitan para hacer aparecer el Ender Dragon y acabar el juego. few = 30, normal = 50 , many = 70
|
||||
advancement_goal:
|
||||
few: 0
|
||||
normal: 1
|
||||
many: 0
|
||||
# Modifica el nivel de objetos lógicamente requeridos para explorar areas peligrosas y pelear contra jefes.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
# Avances que sean tediosos o basados en suerte tendran simplemente experiencia o cosas no necesarias
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Los avances extremadamente difíciles no seran requeridos; esto afecta a How Did We Get Here? y Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Los avances posteriores a Ender Dragon no tendrán objetos necesarios para que otros jugadores en el caso de un MW acaben su partida.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Actualmente desactivado; permite la mezcla de pueblos, puestos, fortalezas, bastiones y cuidades.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
```
|
||||
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
### Obten tu ficheros de datos Minecraft
|
||||
**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.**
|
||||
|
||||
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld (no confundir con hospedar el mundo minecraft).
|
||||
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
|
||||
Tu fichero de datos tiene una extensión `.apmc`.
|
||||
|
||||
Pon tu fichero de datos en el directorio `APData` de tu forge server. Asegurate de eliminar los que hubiera anteriormente
|
||||
|
||||
|
||||
### Conectar al multiserver
|
||||
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
|
||||
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
|
||||
|
||||
Una vez en juego introduce `/connect <AP-Address> (<Password>)` donde `<AP-Address>` es la dirección del servidor
|
||||
Archipelago. `(<Password>)`
|
||||
solo se necesita si el servidor Archipleago tiene un password activo.
|
||||
|
||||
### Jugar al juego
|
||||
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades
|
||||
por unirte exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor forge.
|
||||
|
||||
114
WebHostLib/static/assets/tutorial/minecraft/minecraft_sv.md
Normal file
114
WebHostLib/static/assets/tutorial/minecraft/minecraft_sv.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Minecraft Randomizer Uppsättningsguide
|
||||
|
||||
## Nödvändig Mjukvara
|
||||
|
||||
### Server Värd
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Spelare
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Installationsprocedurer
|
||||
|
||||
### Tillägnad
|
||||
Bara en person behöver göra denna uppsättning och vara värd för en server för alla andra spelare att koppla till.
|
||||
1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste rekommenderade versionen.
|
||||
|
||||
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera server**.
|
||||
- På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är viktigt för nästa steg.
|
||||
|
||||
3. Navigera till vart du har installerat servern och öppna `forge-1.16.5-xx.x.x-installer.jar`
|
||||
- Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta till `eula=true` för att acceptera Minecrafts EULA.
|
||||
- Detta kommer skapa de lämpliga katalogerna för dig att placera filerna i de följande steget.
|
||||
|
||||
4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge server.
|
||||
- Kör servern igen. Den kommer ladda up och generera den nödvändiga katalogen `APData` för när du är redo att spela!
|
||||
|
||||
### Grundläggande Spelaruppsättning
|
||||
- Köp och installera Minecraft från länken ovanför.
|
||||
|
||||
**Du är klar**.
|
||||
|
||||
Andra spelare behöver endast ha en 'Vanilla' omodifierad version av Minecraft för att kunna spela!
|
||||
|
||||
### Avancerad Spelaruppsättning
|
||||
***Detta är inte nödvändigt för att spela ett slumpmässigt Minecraftspel.***
|
||||
Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer trevligt.
|
||||
|
||||
#### Rekommenderade Moddar
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Installera och Kör Minecraft från länken ovanför minst en gång.
|
||||
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera klient**.
|
||||
- Starta Minecraft Forge minst en gång för att skapa katalogerna som behövs för de nästa stegen.
|
||||
3. Navigera till din Minecraft installationskatalog och placera de önskade moddarna med `.jar` i `mods` -katalogen.
|
||||
- Standardinstallationskatalogerna är som följande;
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Konfigurera Din YAML-fil
|
||||
|
||||
### Vad är en YAML-fil och varför behöver jag en?
|
||||
Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur
|
||||
den borde generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter
|
||||
varje spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld
|
||||
kan ha helt olika alternativ.
|
||||
|
||||
### Vart kan jag få tag i en YAML-fil?
|
||||
En grundläggande Minecraft YAML kommer se ut så här.
|
||||
```yaml
|
||||
description: Template Name
|
||||
# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns.
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
accessibility: locations
|
||||
progression_balancing: off
|
||||
advancement_goal:
|
||||
few: 0
|
||||
normal: 1
|
||||
many: 0
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
shuffle_structures:
|
||||
on: 1
|
||||
off: 0
|
||||
```
|
||||
|
||||
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med Archipelago-installationen.
|
||||
|
||||
## Gå med i ett Multivärld-spel
|
||||
|
||||
### Skaffa din Minecraft data-fil
|
||||
**Endast en YAML-fil behöver användats per Minecraft-värld oavsett hur många spelare det är som spelar.**
|
||||
|
||||
När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När detta
|
||||
är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som innehåller allas data-filer.
|
||||
Din data-fil borde ha en `.apmc` -extension.
|
||||
|
||||
Lägg din data-fil i dina forge-servrar `APData` -mapp. Se till att ta bort alla tidigare data-filer som var i där förut.
|
||||
|
||||
### Koppla till Multiservern
|
||||
Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status
|
||||
genom att skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient.
|
||||
När du är inne i spelet, skriv `/connect <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
|
||||
Archipelago-servern. `(<Lösenord>)` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt lösenord.
|
||||
|
||||
### Spela spelet
|
||||
När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis
|
||||
att du har lykats med att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla
|
||||
in till din forge-server.
|
||||
|
||||
@@ -85,5 +85,40 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Minecraft",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Tutorial",
|
||||
"description": "A guide to setting up the Archipelago Minecraft software on your computer. This guide covers single-player, multiworld, and related software.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "minecraft/minecraft_en.md",
|
||||
"link": "minecraft/minecraft/en",
|
||||
"authors": [
|
||||
"Kono Tyran"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "Spanish",
|
||||
"filename": "minecraft/minecraft_es.md",
|
||||
"link": "minecraft/minecraft/es",
|
||||
"authors": [
|
||||
"Edos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "Swedish",
|
||||
"filename": "minecraft/minecraft_sv.md",
|
||||
"link": "minecraft/minecraft/sv",
|
||||
"authors": [
|
||||
"Albinum"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2020 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2021 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a>
|
||||
-
|
||||
|
||||
@@ -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]
|
||||
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,16 +493,20 @@ 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]
|
||||
precollected = precollected_items[player]
|
||||
for item_id in precollected:
|
||||
attribute_item(inventory, team, player, item_id)
|
||||
for location in locations_checked:
|
||||
@@ -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
|
||||
|
||||
7
data/default.apsprite
Normal file
7
data/default.apsprite
Normal file
@@ -0,0 +1,7 @@
|
||||
author: Nintendo
|
||||
data: null
|
||||
game: A Link to the Past
|
||||
min_format_version: 1
|
||||
name: Link
|
||||
format_version: 1
|
||||
sprite_version: 1
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Berserker55
|
||||
Copyright (c) 2021 Berserker55 and Dewiniaid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
-- for testing
|
||||
script.on_event(defines.events.on_tick, function(event)
|
||||
if event.tick%600 == 0 then
|
||||
dumpTech()
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
game.print("Research done")
|
||||
dumpTech()
|
||||
end)
|
||||
|
||||
function dumpTech()
|
||||
|
||||
local force = game.forces["player"]
|
||||
local data_collection = {}
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.researched and string.find(tech_name, "ap-") == 1 then
|
||||
data_collection[tech_name] = tech.researched
|
||||
end
|
||||
end
|
||||
game.write_file("research_done.json", game.table_to_json(data_collection), false)
|
||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.print("Sent progress to Archipelago.")
|
||||
end
|
||||
|
||||
function dumpGameInfo()
|
||||
-- dump Game Information that the Archipelago Randomizer needs.
|
||||
local data_collection = {}
|
||||
local force = game.forces["player"]
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.enabled then
|
||||
local tech_data = {}
|
||||
local unlocks = {}
|
||||
tech_data["unlocks"] = unlocks
|
||||
local requires = {}
|
||||
tech_data["requires"] = requires
|
||||
local ingredients = {}
|
||||
tech_data["ingredients"] = ingredients
|
||||
for tech_requirement, _ in pairs(tech.prerequisites) do
|
||||
table.insert(requires, tech_requirement)
|
||||
end
|
||||
for _, modifier in pairs(tech.effects) do
|
||||
if modifier.type == "unlock-recipe" then
|
||||
table.insert(unlocks, modifier.recipe)
|
||||
end
|
||||
end
|
||||
for _, ingredient in pairs(tech.research_unit_ingredients) do
|
||||
table.insert(ingredients, ingredient.name)
|
||||
end
|
||||
data_collection[tech_name] = tech_data
|
||||
|
||||
end
|
||||
game.write_file("techs.json", game.table_to_json(data_collection), false)
|
||||
game.print("Exported Tech Data")
|
||||
end
|
||||
data_collection = {}
|
||||
for recipe_name, recipe in pairs(force.recipes) do
|
||||
local recipe_data = {}
|
||||
recipe_data["ingredients"] = {}
|
||||
recipe_data["products"] = {}
|
||||
recipe_data["category"] = recipe.category
|
||||
for _, ingredient in pairs(recipe.ingredients) do
|
||||
table.insert(recipe_data["ingredients"], ingredient.name)
|
||||
end
|
||||
for _, product in pairs(recipe.products) do
|
||||
table.insert(recipe_data["products"], product.name)
|
||||
end
|
||||
data_collection[recipe_name] = recipe_data
|
||||
end
|
||||
game.write_file("recipes.json", game.table_to_json(data_collection), false)
|
||||
game.print("Exported Recipe Data")
|
||||
-- data.raw can't be accessed from control.lua, need to find a better method
|
||||
-- data_collection = {}
|
||||
-- for machine_name, machine in pairs(data.raw["assembling_machine"]) do
|
||||
-- local machine_data = {}
|
||||
-- machine_data["categories"] = table.deepcopy(machine.crafting_categories)
|
||||
-- data_collection[machine.name] = machine_data
|
||||
-- end
|
||||
-- game.write_file("machines.json", game.table_to_json(data_collection), false)
|
||||
-- game.print("Exported Machine Data")
|
||||
end
|
||||
|
||||
-- add / commands
|
||||
|
||||
commands.add_command("ap-get-info-dump", "Dump Game Info, used by Archipelago.", function(call)
|
||||
dumpGameInfo()
|
||||
end)
|
||||
|
||||
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
||||
dumpTech()
|
||||
end)
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
local force = game.forces["player"]
|
||||
local tech_name = call.parameter
|
||||
local tech = force.technologies[tech_name]
|
||||
if tech ~= nil then
|
||||
if tech.researched ~= true then
|
||||
tech.researched = true
|
||||
game.print({"", "Received ", tech.localised_name, " from Archipelago"})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
end
|
||||
else
|
||||
game.print("Unknown Technology " .. tech_name)
|
||||
end
|
||||
end)
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "archipelago-client",
|
||||
"version": "0.0.1",
|
||||
"title": "Archipelago",
|
||||
"author": "Berserker",
|
||||
"author": "Berserker and Dewiniaid",
|
||||
"homepage": "https://archipelago.gg",
|
||||
"description": "Integration client for the Archipelago Randomizer",
|
||||
"factorio_version": "1.1"
|
||||
|
||||
23
data/factorio/mod/lib.lua
Normal file
23
data/factorio/mod/lib.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
function filter_ingredients(ingredients, ingredient_filter)
|
||||
local new_ingredient_list = {}
|
||||
for _, ingredient_table in pairs(ingredients) do
|
||||
if ingredient_filter[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
|
||||
BIN
data/factorio/mod/thumbnail.png
Normal file
BIN
data/factorio/mod/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
237
data/factorio/mod_template/control.lua
Normal file
237
data/factorio/mod_template/control.lua
Normal file
@@ -0,0 +1,237 @@
|
||||
{% macro dict_to_lua(dict) -%}
|
||||
{
|
||||
{% for key, value in dict.items() %}
|
||||
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
{%- endmacro %}
|
||||
require "lib"
|
||||
require "util"
|
||||
|
||||
FREE_SAMPLES = {{ free_samples }}
|
||||
SLOT_NAME = "{{ slot_name }}"
|
||||
--SUPPRESS_INVENTORY_EVENTS = false
|
||||
|
||||
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
|
||||
function on_force_created(event)
|
||||
--event.force appears to be LuaForce.name, not LuaForce
|
||||
game.forces[event.force].research_queue_enabled = true
|
||||
local data = {}
|
||||
data['earned_samples'] = {{ dict_to_lua(starting_items) }}
|
||||
data["victory"] = 0
|
||||
global.forcedata[event.force] = data
|
||||
end
|
||||
script.on_event(defines.events.on_force_created, on_force_created)
|
||||
|
||||
-- Destroy force data. This doesn't appear to be currently possible with the Factorio API, but here for completeness.
|
||||
function on_force_destroyed(event)
|
||||
global.forcedata[event.force.name] = nil
|
||||
end
|
||||
|
||||
-- Initialize player data, either from them joining the game or them already being part of the game when the mod was
|
||||
-- added.`
|
||||
function on_player_created(event)
|
||||
local player = game.players[event.player_index]
|
||||
-- FIXME: This (probably) fires before any other mod has a chance to change the player's force
|
||||
-- For now, they will (probably) always be on the 'player' force when this event fires.
|
||||
local data = {}
|
||||
data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples'])
|
||||
global.playerdata[player.index] = data
|
||||
update_player(player.index) -- Attempt to send pending free samples, if relevant.
|
||||
end
|
||||
script.on_event(defines.events.on_player_created, on_player_created)
|
||||
|
||||
function on_player_removed(event)
|
||||
global.playerdata[event.player_index] = nil
|
||||
end
|
||||
script.on_event(defines.events.on_player_removed, on_player_removed)
|
||||
|
||||
function on_rocket_launched(event)
|
||||
global.forcedata[event.rocket.force.name]['victory'] = 1
|
||||
dumpInfo(event.rocket.force)
|
||||
end
|
||||
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
|
||||
|
||||
-- Updates a player, attempting to send them any pending samples (if relevant)
|
||||
function update_player(index)
|
||||
local player = game.players[index]
|
||||
if not player or not player.valid then -- Do nothing if we reference an invalid player somehow
|
||||
return
|
||||
end
|
||||
local character = player.character or player.cutscene_character
|
||||
if not character or not character.valid then
|
||||
return
|
||||
end
|
||||
local data = global.playerdata[index]
|
||||
local samples = data['pending_samples']
|
||||
local sent
|
||||
--player.print(serpent.block(data['pending_samples']))
|
||||
local stack = {}
|
||||
--SUPPRESS_INVENTORY_EVENTS = true
|
||||
for name, count in pairs(samples) do
|
||||
stack.name = name
|
||||
stack.count = count
|
||||
if character.can_insert(stack) then
|
||||
sent = character.insert(stack)
|
||||
else
|
||||
sent = 0
|
||||
end
|
||||
if sent > 0 then
|
||||
player.print("Received " .. sent .. "x [item=" .. name .. "]")
|
||||
data.suppress_full_inventory_message = false
|
||||
end
|
||||
if sent ~= count then -- Couldn't full send.
|
||||
if not data.suppress_full_inventory_message then
|
||||
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
|
||||
end
|
||||
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
|
||||
samples[name] = count - sent -- Buffer the remaining items
|
||||
break -- Stop trying to send other things
|
||||
else
|
||||
samples[name] = nil -- Remove from the list
|
||||
end
|
||||
end
|
||||
--SUPPRESS_INVENTORY_EVENTS = false
|
||||
end
|
||||
|
||||
-- Update players upon them connecting, since updates while they're offline are suppressed.
|
||||
script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end)
|
||||
|
||||
function update_player_event(event)
|
||||
--if not SUPPRESS_INVENTORY_EVENTS then
|
||||
update_player(event.player_index)
|
||||
--end
|
||||
end
|
||||
|
||||
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
|
||||
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
t[name] = (t[name] or 0) + count
|
||||
end
|
||||
-- Add to global table of earned samples for future new players
|
||||
add_to_table(global.forcedata[force.name]['earned_samples'])
|
||||
-- Add to existing players
|
||||
for _, player in pairs(force.players) do
|
||||
add_to_table(global.playerdata[player.index]['pending_samples'])
|
||||
update_player(player.index)
|
||||
end
|
||||
end
|
||||
|
||||
script.on_init(function()
|
||||
global.forcedata = {}
|
||||
global.playerdata = {}
|
||||
-- Fire dummy events for all currently existing forces.
|
||||
local e = {}
|
||||
for name, _ in pairs(game.forces) do
|
||||
e.force = name
|
||||
on_force_created(e)
|
||||
end
|
||||
e.force = nil
|
||||
|
||||
-- Fire dummy events for all currently existing players.
|
||||
for index, _ in pairs(game.players) do
|
||||
e.player_index = index
|
||||
on_player_created(e)
|
||||
end
|
||||
end)
|
||||
|
||||
-- for testing
|
||||
script.on_event(defines.events.on_tick, function(event)
|
||||
if event.tick%600 == 300 then
|
||||
dumpInfo(game.forces["player"])
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
local technology = event.research
|
||||
dumpInfo(technology.force)
|
||||
if FREE_SAMPLES == 0 then
|
||||
return -- Nothing else to do
|
||||
end
|
||||
if not technology.effects then
|
||||
return -- No technology effects, so nothing to do.
|
||||
end
|
||||
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
|
||||
local name = result.name
|
||||
local count
|
||||
if FREE_SAMPLES == 1 then
|
||||
count = result.amount
|
||||
else
|
||||
count = get_any_stack_size(result.name)
|
||||
if FREE_SAMPLES == 2 then
|
||||
count = math.ceil(count / 2)
|
||||
end
|
||||
end
|
||||
add_samples(technology.force, name, count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function dumpInfo(force)
|
||||
local research_done = {}
|
||||
local data_collection = {
|
||||
["research_done"] = research_done,
|
||||
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
||||
["slot_name"] = SLOT_NAME
|
||||
}
|
||||
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.researched and string.find(tech_name, "ap%-") == 1 then
|
||||
research_done[tech_name] = tech.researched
|
||||
end
|
||||
end
|
||||
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.print("Sent progress to Archipelago.")
|
||||
end
|
||||
|
||||
|
||||
|
||||
function chain_lookup(table, ...)
|
||||
for _, k in ipairs{...} do
|
||||
table = table[k]
|
||||
if not table then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
-- add / commands
|
||||
|
||||
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
||||
if call.player_index == nil then
|
||||
dumpInfo(game.forces.player)
|
||||
else
|
||||
dumpInfo(game.players[call.player_index].force)
|
||||
end
|
||||
game.print("Wrote bridge file.")
|
||||
end)
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
local force = game.forces["player"]
|
||||
chunks = {}
|
||||
for substring in call.parameter:gmatch("%S+") do -- split on " "
|
||||
table.insert(chunks, substring)
|
||||
end
|
||||
local tech_name = chunks[1]
|
||||
local source = chunks[2] or "Archipelago"
|
||||
local tech = force.technologies[tech_name]
|
||||
if tech ~= nil then
|
||||
if tech.researched ~= true then
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
end
|
||||
else
|
||||
game.print("Unknown Technology " .. tech_name)
|
||||
end
|
||||
end)
|
||||
@@ -1,23 +1,50 @@
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
|
||||
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }}
|
||||
|
||||
local technologies = data.raw["technology"]
|
||||
local original_tech
|
||||
local new_tree_copy
|
||||
allowed_ingredients = {}
|
||||
{%- for tech_name, technology in custom_data["custom_technologies"].items() %}
|
||||
allowed_ingredients["{{ tech_name }}"] = {
|
||||
{%- for ingredient in technology.ingredients %}
|
||||
["{{ingredient}}"] = 1,
|
||||
{%- endfor %}
|
||||
}
|
||||
{% 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)
|
||||
local ingredient_filter = allowed_ingredients[old_tech.name]
|
||||
if ingredient_filter ~= nil then
|
||||
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
|
||||
|
||||
{# 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 +55,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,18 @@
|
||||
|
||||
[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
7
data/sprites/custom/link.apsprite
Normal file
7
data/sprites/custom/link.apsprite
Normal file
@@ -0,0 +1,7 @@
|
||||
author: Nintendo
|
||||
data: null
|
||||
game: A Link to the Past
|
||||
min_format_version: 1
|
||||
name: Link
|
||||
format_version: 1
|
||||
sprite_version: 1
|
||||
@@ -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
|
||||
@@ -112,5 +114,4 @@ lttp_options:
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
factorio_options:
|
||||
executable: "factorio\\bin\\x64\\factorio"
|
||||
script-output: "factorio\\script-output"
|
||||
executable: "factorio\\bin\\x64\\factorio"
|
||||
@@ -52,7 +52,7 @@ Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks:
|
||||
|
||||
[Run]
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
@@ -52,7 +52,7 @@ Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks:
|
||||
|
||||
[Run]
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
@@ -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,70 @@ 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.
|
||||
# Can be uncommented to use it
|
||||
# startinventory: # Begin the file with the listed items/upgrades
|
||||
# Please only use items for the correct game, use triggers if need to be have seperated lists.
|
||||
# Pegasus Boots: on
|
||||
# Bomb Upgrade (+10): 4
|
||||
# Arrow Upgrade (+10): 4
|
||||
# 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
|
||||
visibility:
|
||||
none: 0
|
||||
sending: 1
|
||||
random_tech_ingredients:
|
||||
on: 1
|
||||
off: 0
|
||||
starting_items:
|
||||
burner-mining-drill: 19
|
||||
stone-furnace: 19
|
||||
# Minecraft options:
|
||||
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
|
||||
few: 0 # 30 advancements
|
||||
normal: 1 # 50
|
||||
many: 0 # 70
|
||||
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
|
||||
on: 0
|
||||
off: 1
|
||||
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
|
||||
on: 0
|
||||
off: 1
|
||||
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
on: 0
|
||||
off: 1
|
||||
# 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
|
||||
@@ -87,7 +152,8 @@ progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
|
||||
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
|
||||
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
|
||||
restricted: 0 # Less strict than simple
|
||||
full: 0 # Less strict than restricted
|
||||
@@ -172,11 +238,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 +299,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
|
||||
@@ -287,11 +353,6 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o
|
||||
# - "Moon Pearl"
|
||||
# - "Small Keys"
|
||||
# - "Big Keys"
|
||||
# Can be uncommented to use it
|
||||
# startinventory: # Begin the file with the listed items/upgrades
|
||||
# Pegasus Boots: on
|
||||
# Bomb Upgrade (+10): 4
|
||||
# Arrow Upgrade (+10): 4
|
||||
glitch_boots:
|
||||
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
|
||||
off: 0
|
||||
@@ -301,8 +362,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 +387,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 +400,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
|
||||
|
||||
@@ -2,9 +2,10 @@ colorama>=0.4.4
|
||||
websockets>=8.1
|
||||
PyYAML>=5.4.1
|
||||
fuzzywuzzy>=0.18.0
|
||||
bsdiff4>=1.2.0
|
||||
bsdiff4>=1.2.1
|
||||
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
|
||||
11
setup.py
11
setup.py
@@ -48,8 +48,10 @@ def manifest_creation():
|
||||
path = os.path.join(dirpath, filename)
|
||||
hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path)
|
||||
import json
|
||||
from Utils import _version_tuple
|
||||
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
|
||||
"hashes": {path: hash.result() for path, hash in hashes.items()}}
|
||||
"hashes": {path: hash.result() for path, hash in hashes.items()},
|
||||
"version": _version_tuple}
|
||||
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
||||
print("Created Manifest")
|
||||
|
||||
@@ -57,7 +59,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"}
|
||||
@@ -67,7 +68,7 @@ exes = []
|
||||
for script, scriptname in scripts.items():
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script=script,
|
||||
targetName=scriptname + ("" if sys.platform == "linux" else ".exe"),
|
||||
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
|
||||
icon=icon,
|
||||
))
|
||||
|
||||
@@ -142,7 +143,7 @@ for file in os.listdir(alttpr_sprites_folder):
|
||||
|
||||
if signtool:
|
||||
for exe in exes:
|
||||
print(f"Signing {exe.targetName}")
|
||||
os.system(signtool + exe.targetName)
|
||||
print(f"Signing {exe.target_name}")
|
||||
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
||||
|
||||
manifest_creation()
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import unittest
|
||||
import pathlib
|
||||
|
||||
import Utils
|
||||
|
||||
file_path = pathlib.Path(__file__).parent.parent
|
||||
Utils.local_path.cached_path = file_path
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
world: MultiWorld
|
||||
_state_cache = {}
|
||||
|
||||
1145
test/minecraft/TestAdvancements.py
Normal file
1145
test/minecraft/TestAdvancements.py
Normal file
File diff suppressed because it is too large
Load Diff
92
test/minecraft/TestEntrances.py
Normal file
92
test/minecraft/TestEntrances.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from test.minecraft.TestMinecraft import TestMinecraft
|
||||
|
||||
class TestEntrances(TestMinecraft):
|
||||
|
||||
def testPortals(self):
|
||||
self.run_entrance_tests([
|
||||
['Nether Portal', False, []],
|
||||
['Nether Portal', False, [], ['Flint and Steel']],
|
||||
['Nether Portal', False, [], ['Ingot Crafting']],
|
||||
['Nether Portal', False, [], ['Progressive Tools']],
|
||||
['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']],
|
||||
|
||||
['End Portal', False, []],
|
||||
['End Portal', False, [], ['Brewing']],
|
||||
['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
|
||||
['End Portal', False, [], ['Flint and Steel']],
|
||||
['End Portal', False, [], ['Ingot Crafting']],
|
||||
['End Portal', False, [], ['Progressive Tools']],
|
||||
['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['End Portal', False, [], ['Progressive Weapons']],
|
||||
['End Portal', False, [], ['Progressive Armor', 'Shield']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
])
|
||||
|
||||
def testStructures(self):
|
||||
self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent
|
||||
['Overworld Structure 1', False, []],
|
||||
['Overworld Structure 1', False, [], ['Progressive Weapons']],
|
||||
['Overworld Structure 1', False, [], ['Ingot Crafting', 'Campfire']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Ingot Crafting']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']],
|
||||
|
||||
['Overworld Structure 2', False, []],
|
||||
['Overworld Structure 2', False, [], ['Progressive Weapons']],
|
||||
['Overworld Structure 2', False, [], ['Ingot Crafting', 'Campfire']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Ingot Crafting']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']],
|
||||
|
||||
['Nether Structure 1', False, []],
|
||||
['Nether Structure 1', False, [], ['Flint and Steel']],
|
||||
['Nether Structure 1', False, [], ['Ingot Crafting']],
|
||||
['Nether Structure 1', False, [], ['Progressive Tools']],
|
||||
['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Structure 1', False, [], ['Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
|
||||
['Nether Structure 2', False, []],
|
||||
['Nether Structure 2', False, [], ['Flint and Steel']],
|
||||
['Nether Structure 2', False, [], ['Ingot Crafting']],
|
||||
['Nether Structure 2', False, [], ['Progressive Tools']],
|
||||
['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Structure 2', False, [], ['Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
|
||||
['The End Structure', False, []],
|
||||
['The End Structure', False, [], ['Brewing']],
|
||||
['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
|
||||
['The End Structure', False, [], ['Flint and Steel']],
|
||||
['The End Structure', False, [], ['Ingot Crafting']],
|
||||
['The End Structure', False, [], ['Progressive Tools']],
|
||||
['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['The End Structure', False, [], ['Progressive Weapons']],
|
||||
['The End Structure', False, [], ['Progressive Armor', 'Shield']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
|
||||
])
|
||||
57
test/minecraft/TestMinecraft.py
Normal file
57
test/minecraft/TestMinecraft.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from test.TestBase import TestBase
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.minecraft import minecraft_gen_item_pool
|
||||
from worlds.minecraft.Regions import minecraft_create_regions, link_minecraft_structures
|
||||
from worlds.minecraft.Rules import set_rules
|
||||
from worlds.minecraft.Items import MinecraftItem, item_table
|
||||
import Options
|
||||
|
||||
# Converts the name of an item into an item object
|
||||
def MCItemFactory(items, player: int):
|
||||
ret = []
|
||||
singleton = False
|
||||
if isinstance(items, str):
|
||||
items = [items]
|
||||
singleton = True
|
||||
for item in items:
|
||||
if item in item_table:
|
||||
ret.append(MinecraftItem(item, item_table[item].progression, item_table[item].code, player))
|
||||
else:
|
||||
raise Exception(f"Unknown item {item}")
|
||||
|
||||
if singleton:
|
||||
return ret[0]
|
||||
return ret
|
||||
|
||||
class TestMinecraft(TestBase):
|
||||
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.game[1] = "Minecraft"
|
||||
exclusion_pools = ['hard', 'insane', 'postgame']
|
||||
for pool in exclusion_pools:
|
||||
setattr(self.world, f"include_{pool}_advancements", [False, False])
|
||||
setattr(self.world, "advancement_goal", [0, Options.AdvancementGoal(value=0)])
|
||||
setattr(self.world, "shuffle_structures", [False, False])
|
||||
setattr(self.world, "combat_difficulty", [0, Options.CombatDifficulty(value=1)])
|
||||
minecraft_create_regions(self.world, 1)
|
||||
link_minecraft_structures(self.world, 1)
|
||||
minecraft_gen_item_pool(self.world, 1)
|
||||
set_rules(self.world, 1)
|
||||
|
||||
def _get_items(self, item_pool, all_except):
|
||||
if all_except and len(all_except) > 0:
|
||||
items = self.world.itempool[:]
|
||||
items = [item for item in items if
|
||||
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
|
||||
items.extend(MCItemFactory(item_pool[0], 1))
|
||||
else:
|
||||
items = MCItemFactory(item_pool[0], 1)
|
||||
return self.get_state(items)
|
||||
|
||||
def _get_items_partial(self, item_pool, missing_item):
|
||||
new_items = item_pool[0].copy()
|
||||
new_items.remove(missing_item)
|
||||
items = MCItemFactory(new_items, 1)
|
||||
return self.get_state(items)
|
||||
|
||||
@@ -8,25 +8,31 @@ __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()}
|
||||
from .minecraft.Items import lookup_id_to_name as mc
|
||||
|
||||
lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name, **mc}
|
||||
assert len(alttp) + len(hk) + len(Technologies.lookup_id_to_name) + len(mc) == 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
|
||||
from .minecraft import Locations as Advancements
|
||||
|
||||
lookup_any_location_id_to_name = {**Regions.lookup_id_to_name, **Locations.lookup_id_to_name,
|
||||
**Technologies.lookup_id_to_name}
|
||||
**Technologies.lookup_id_to_name, **Advancements.lookup_id_to_name}
|
||||
assert len(Regions.lookup_id_to_name) + len(Locations.lookup_id_to_name) + \
|
||||
len(Technologies.lookup_id_to_name) + len(Advancements.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": 6}
|
||||
|
||||
|
||||
@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'],
|
||||
@@ -175,7 +172,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
slightly biased to placing progression items with
|
||||
less restrictions.
|
||||
''')
|
||||
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed'],
|
||||
help='''\
|
||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||
Full: Mix cave and dungeon entrances freely while limiting
|
||||
@@ -355,7 +352,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
Torches means additionally easily accessible Torches that can be lit with Fire Rod are considered doable.
|
||||
None means full traversal through dark rooms without tools is considered doable.''')
|
||||
parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true")
|
||||
parser.add_argument('--remote_items', default=defval(False), action='store_true')
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
@@ -401,7 +397,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',
|
||||
@@ -412,7 +408,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
"triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots",
|
||||
"required_medallions",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'restrict_dungeon_item_on_boss', 'reduceflashing', 'game',
|
||||
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes', 'triforcehud']:
|
||||
|
||||
@@ -60,6 +60,8 @@ def link_entrances(world, player):
|
||||
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
|
||||
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
|
||||
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
||||
elif world.shuffle[player] == 'dungeonscrossed':
|
||||
crossed_shuffle_dungeons(world, player)
|
||||
elif world.shuffle[player] == 'simple':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
@@ -1117,7 +1119,7 @@ def link_inverted_entrances(world, player):
|
||||
dw_entrances = list(Inverted_DW_Dungeon_Entrances)
|
||||
|
||||
# randomize which desert ledge door is a must-exit
|
||||
if world.random.randint(0, 1) == 0:
|
||||
if world.random.randint(0, 1):
|
||||
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)')
|
||||
lw_entrances.append('Desert Palace Entrance (West)')
|
||||
else:
|
||||
@@ -1152,7 +1154,7 @@ def link_inverted_entrances(world, player):
|
||||
|
||||
if aga_door in lw_entrances:
|
||||
lw_entrances.remove(aga_door)
|
||||
elif aga_door in dw_entrances:
|
||||
else:
|
||||
dw_entrances.remove(aga_door)
|
||||
|
||||
connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
|
||||
@@ -1161,7 +1163,8 @@ def link_inverted_entrances(world, player):
|
||||
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
|
||||
|
||||
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
||||
|
||||
elif world.shuffle[player] == 'dungeonscrossed':
|
||||
inverted_crossed_shuffle_dungeons(world, player)
|
||||
elif world.shuffle[player] == 'simple':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
@@ -1368,11 +1371,8 @@ def link_inverted_entrances(world, player):
|
||||
# tavern back door cannot be shuffled yet
|
||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||
|
||||
caves.append(tuple(world.random.sample(
|
||||
['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3)))
|
||||
lw_entrances.append('Hyrule Castle Entrance (South)')
|
||||
|
||||
|
||||
if not world.shuffle_ganon:
|
||||
connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
|
||||
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
|
||||
@@ -1386,7 +1386,7 @@ def link_inverted_entrances(world, player):
|
||||
aga_doors = [i for i in all_entrances_aga]
|
||||
world.random.shuffle(aga_doors)
|
||||
aga_door = aga_doors.pop()
|
||||
|
||||
|
||||
if aga_door in hc_ledge_entrances:
|
||||
lw_entrances.remove(aga_door)
|
||||
hc_ledge_entrances.remove(aga_door)
|
||||
@@ -1403,7 +1403,7 @@ def link_inverted_entrances(world, player):
|
||||
|
||||
connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
|
||||
caves.remove('Inverted Agahnims Tower Exit')
|
||||
|
||||
|
||||
# place links house
|
||||
links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if
|
||||
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
||||
@@ -1433,11 +1433,14 @@ def link_inverted_entrances(world, player):
|
||||
except ValueError:
|
||||
pass
|
||||
else: # if the cave wasn't placed we get here
|
||||
connect_caves(world, lw_entrances, [], old_man_house, player)
|
||||
connect_caves(world, lw_entrances, [], old_man_house, player)
|
||||
else:
|
||||
connect_caves(world, dw_entrances, [], old_man_house, player)
|
||||
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
|
||||
|
||||
# put all HC exits in LW in inverted full shuffle
|
||||
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player)
|
||||
|
||||
# place old man, has limited options
|
||||
# exit has to come from specific set of doors, the entrance is free to move about
|
||||
old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances]
|
||||
@@ -2027,17 +2030,10 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player):
|
||||
world.random.shuffle(lw_entrances)
|
||||
world.random.shuffle(dw_entrances)
|
||||
world.random.shuffle(caves)
|
||||
while caves:
|
||||
# connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill
|
||||
cave_candidate = (None, 0)
|
||||
for i, cave in enumerate(caves):
|
||||
if isinstance(cave, str):
|
||||
cave = (cave,)
|
||||
if len(cave) > cave_candidate[1]:
|
||||
cave_candidate = (i, len(cave))
|
||||
cave = caves.pop(cave_candidate[0])
|
||||
|
||||
target = lw_entrances if world.random.randint(0, 1) == 0 else dw_entrances
|
||||
# connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill
|
||||
caves.sort(key=lambda cave: 1 if isinstance(cave, str) else len(cave), reverse=True)
|
||||
for cave in caves:
|
||||
target = lw_entrances if world.random.randint(0, 1) else dw_entrances
|
||||
if isinstance(cave, str):
|
||||
cave = (cave,)
|
||||
|
||||
@@ -2048,6 +2044,7 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player):
|
||||
|
||||
for exit in cave:
|
||||
connect_two_way(world, target.pop(), exit, player)
|
||||
caves.clear() # emulating old behaviour of popping caves from the list in-place
|
||||
|
||||
|
||||
def connect_doors(world, doors, targets, player):
|
||||
@@ -2200,6 +2197,90 @@ def simple_shuffle_dungeons(world, player):
|
||||
connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player)
|
||||
connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player)
|
||||
|
||||
def crossed_shuffle_dungeons(world, player: int):
|
||||
lw_entrances = LW_Dungeon_Entrances.copy()
|
||||
dw_entrances = DW_Dungeon_Entrances.copy()
|
||||
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
dungeon_exits = Dungeon_Exits_Base.copy()
|
||||
dungeon_entrances = lw_entrances+dw_entrances
|
||||
|
||||
if not world.shuffle_ganon:
|
||||
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
|
||||
else:
|
||||
dungeon_entrances.append('Ganons Tower')
|
||||
dungeon_exits.append('Ganons Tower Exit')
|
||||
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
|
||||
dungeon_entrances.append('Hyrule Castle Entrance (South)')
|
||||
|
||||
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
|
||||
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
|
||||
|
||||
if world.mode[player] == 'standard':
|
||||
connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||
|
||||
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
|
||||
assert not dungeon_exits # make sure all exits are accounted for
|
||||
|
||||
def inverted_crossed_shuffle_dungeons(world, player: int):
|
||||
|
||||
lw_entrances = Inverted_LW_Dungeon_Entrances.copy()
|
||||
dw_entrances = Inverted_DW_Dungeon_Entrances.copy()
|
||||
lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit)
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
dungeon_exits = Inverted_Dungeon_Exits_Base.copy()
|
||||
dungeon_entrances = lw_entrances+dw_entrances
|
||||
|
||||
# randomize which desert ledge door is a must-exit
|
||||
if world.random.randint(0, 1):
|
||||
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)')
|
||||
dungeon_entrances.append('Desert Palace Entrance (West)')
|
||||
else:
|
||||
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (West)')
|
||||
dungeon_entrances.append('Desert Palace Entrance (North)')
|
||||
|
||||
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
|
||||
dungeon_entrances.append('Hyrule Castle Entrance (South)')
|
||||
|
||||
if not world.shuffle_ganon:
|
||||
connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
|
||||
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
|
||||
else:
|
||||
dungeon_entrances.append('Inverted Ganons Tower')
|
||||
dungeon_exits.append('Inverted Ganons Tower Exit')
|
||||
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower']
|
||||
|
||||
# shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit
|
||||
world.random.shuffle(dungeon_entrances)
|
||||
aga_door = dungeon_entrances.pop()
|
||||
|
||||
if aga_door in hc_ledge_entrances:
|
||||
hc_ledge_entrances.remove(aga_door)
|
||||
world.random.shuffle(hc_ledge_entrances)
|
||||
hc_ledge_must_exit = hc_ledge_entrances.pop()
|
||||
dungeon_entrances.remove(hc_ledge_must_exit)
|
||||
lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit)
|
||||
|
||||
connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
|
||||
dungeon_exits.remove('Inverted Agahnims Tower Exit')
|
||||
|
||||
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
|
||||
|
||||
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
|
||||
assert not dungeon_exits # make sure all exits are accounted for
|
||||
|
||||
def unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits):
|
||||
def shuffle_lists_in_list(ls):
|
||||
|
||||
@@ -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)")
|
||||
|
||||
|
||||
@@ -676,10 +676,12 @@ location_table: typing.Dict[str,
|
||||
|
||||
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
|
||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, -1: "cheat console"}
|
||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()},
|
||||
-1: "Cheat Console", -2: "Server"}
|
||||
lookup_id_to_name.update(shop_table_by_location_id)
|
||||
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
|
||||
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, "cheat console": -1}
|
||||
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()},
|
||||
"Cheat Console": -1, "Server": -2}
|
||||
lookup_name_to_id.update(shop_table_by_location)
|
||||
|
||||
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
|
||||
|
||||
@@ -14,6 +14,7 @@ import subprocess
|
||||
import threading
|
||||
import xxtea
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Optional
|
||||
|
||||
from BaseClasses import CollectionState, Region
|
||||
@@ -22,10 +23,12 @@ from worlds.alttp.Shops import ShopType
|
||||
from worlds.alttp.Dungeons import dungeon_music_addresses
|
||||
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
|
||||
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
|
||||
from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, \
|
||||
from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
|
||||
Blind_texts, \
|
||||
BombShop2_texts, junk_texts
|
||||
|
||||
from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, \
|
||||
from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \
|
||||
DeathMountain_texts, \
|
||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_bundled
|
||||
@@ -41,6 +44,7 @@ except:
|
||||
|
||||
enemizer_logger = logging.getLogger("Enemizer")
|
||||
|
||||
|
||||
class LocalRom(object):
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
@@ -428,8 +432,11 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli):
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
tile_list_lock = threading.Lock()
|
||||
_tile_collection_table = []
|
||||
|
||||
|
||||
def _populate_tile_sets():
|
||||
with tile_list_lock:
|
||||
if not _tile_collection_table:
|
||||
@@ -442,6 +449,7 @@ def _populate_tile_sets():
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_tileset_from_file, os.path.join(dir, file))
|
||||
|
||||
|
||||
class TileSet:
|
||||
def __init__(self, filename):
|
||||
with open(filename, 'rt', encoding='utf-8-sig') as file:
|
||||
@@ -497,25 +505,24 @@ def _populate_sprite_table():
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
class Sprite(object):
|
||||
palette = (255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
|
||||
89, 71, 54, 104, 59, 74, 10, 239, 18, 92, 42, 113, 21, 24, 122,
|
||||
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
|
||||
89, 128, 105, 145, 118, 184, 38, 127, 67, 92, 42, 153, 17, 24, 122,
|
||||
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
|
||||
89, 87, 16, 126, 69, 243, 109, 185, 126, 92, 42, 39, 34, 24, 122,
|
||||
255, 127, 126, 35, 218, 17, 158, 54, 165, 20, 255, 1, 120, 16, 151,
|
||||
61, 71, 54, 104, 59, 74, 10, 239, 18, 126, 86, 114, 24, 24, 122)
|
||||
|
||||
glove_palette = (246, 82, 118, 3)
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
palette_size = 120
|
||||
glove_size = 4
|
||||
author_name: Optional[str] = None
|
||||
base_data: bytes
|
||||
|
||||
def __init__(self, filename):
|
||||
if not hasattr(Sprite, "base_data"):
|
||||
self.get_vanilla_sprite_data()
|
||||
with open(filename, 'rb') as file:
|
||||
filedata = bytearray(file.read())
|
||||
filedata = file.read()
|
||||
self.name = os.path.basename(filename)
|
||||
self.valid = True
|
||||
if len(filedata) == 0x7000:
|
||||
if filename.endswith(".apsprite"):
|
||||
self.from_ap_sprite(filedata)
|
||||
elif len(filedata) == 0x7000:
|
||||
# sprite file with graphics and without palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
elif len(filedata) == 0x7078:
|
||||
@@ -534,26 +541,79 @@ class Sprite(object):
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
result = self.parse_zspr(filedata, 1)
|
||||
if result is None:
|
||||
self.valid = False
|
||||
return
|
||||
(sprite, palette, self.name, self.author_name) = result
|
||||
if self.name == "":
|
||||
self.name = os.path.split(filename)[1].split(".")[0]
|
||||
if len(sprite) != 0x7000:
|
||||
self.valid = False
|
||||
return
|
||||
self.sprite = sprite
|
||||
if len(palette) == 0:
|
||||
pass
|
||||
elif len(palette) == 0x78:
|
||||
self.palette = palette
|
||||
elif len(palette) == 0x7C:
|
||||
self.palette = palette[:0x78]
|
||||
self.glove_palette = palette[0x78:]
|
||||
else:
|
||||
self.valid = False
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
from Patch import get_base_rom_path
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
filedata = filedata.decode("utf-8-sig")
|
||||
import yaml
|
||||
obj = yaml.safe_load(filedata)
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
name = getattr(self, "_author_game_display", "")
|
||||
if not name:
|
||||
name = self.author_name
|
||||
|
||||
# At this point, may need some filtering to displayable characters
|
||||
return name
|
||||
|
||||
def to_ap_sprite(self, path):
|
||||
from .. import Games
|
||||
import yaml
|
||||
payload = {"format_version": 1,
|
||||
"min_format_version": 1,
|
||||
"sprite_version": 1,
|
||||
"name": self.name,
|
||||
"author": self.author_name,
|
||||
"game": Games.LTTP.value,
|
||||
"data": self.get_delta()}
|
||||
with open(path, "w") as f:
|
||||
f.write(yaml.safe_dump(payload))
|
||||
|
||||
def get_delta(self):
|
||||
modified_data = self.sprite + self.palette + self.glove_palette
|
||||
return bsdiff4.diff(Sprite.base_data, modified_data)
|
||||
|
||||
def from_zspr(self, filedata, filename):
|
||||
result = self.parse_zspr(filedata, 1)
|
||||
if result is None:
|
||||
self.valid = False
|
||||
return
|
||||
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
||||
if self.name == "":
|
||||
self.name = os.path.split(filename)[1].split(".")[0]
|
||||
|
||||
if len(sprite) != 0x7000:
|
||||
self.valid = False
|
||||
return
|
||||
self.sprite = sprite
|
||||
if len(palette) == 0:
|
||||
pass
|
||||
elif len(palette) == 0x78:
|
||||
self.palette = palette
|
||||
elif len(palette) == 0x7C:
|
||||
self.palette = palette[:0x78]
|
||||
self.glove_palette = palette[0x78:]
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@@ -569,7 +629,7 @@ class Sprite(object):
|
||||
|
||||
@staticmethod
|
||||
def default_link_sprite():
|
||||
return Sprite(local_path('../../data', 'default.zspr'))
|
||||
return Sprite(local_path('data', 'default.apsprite'))
|
||||
|
||||
def decode8(self, pos):
|
||||
arr = [[0 for _ in range(8)] for _ in range(8)]
|
||||
@@ -602,13 +662,14 @@ class Sprite(object):
|
||||
arr[y + 8][x + 8] = bottom_right[y][x]
|
||||
return arr
|
||||
|
||||
def parse_zspr(self, filedata, expected_kind):
|
||||
logger = logging.getLogger('')
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger('ZSPR')
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
return None
|
||||
(version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from(
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error('Error parsing ZSPR file: Version %g not supported', version)
|
||||
@@ -631,6 +692,7 @@ class Sprite(object):
|
||||
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
@@ -645,7 +707,7 @@ class Sprite(object):
|
||||
logger.error('Error parsing ZSPR file: Unexpected end of file')
|
||||
return None
|
||||
|
||||
return (sprite, palette, sprite_name, author_name)
|
||||
return (sprite, palette, sprite_name, author_name, author_credits_name)
|
||||
|
||||
def decode_palette(self):
|
||||
"Returns the palettes as an array of arrays of 15 colors"
|
||||
@@ -657,7 +719,7 @@ class Sprite(object):
|
||||
return pair[1] << 8 | pair[0]
|
||||
|
||||
def expand_color(i):
|
||||
return ((i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8)
|
||||
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
||||
|
||||
# turn palette data into a list of RGB tuples with 8 bit values
|
||||
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
||||
@@ -689,6 +751,14 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028,
|
||||
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3,
|
||||
0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
|
||||
|
||||
def get_nonnative_item_sprite(game):
|
||||
game_to_id = {
|
||||
"Factorio": 0x09, # Hammer
|
||||
"Hollow Knight": 0x21, # Bug Catching Net
|
||||
"Minecraft": 0x13, # Shovel
|
||||
}
|
||||
return game_to_id.get(game, 0x6B) # default to Power Star
|
||||
|
||||
def patch_rom(world, rom, player, team, enemized):
|
||||
local_random = world.rom_seeds[player]
|
||||
|
||||
@@ -711,10 +781,7 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
|
||||
if location.item is not None:
|
||||
if location.item.game != "A Link to the Past":
|
||||
if location.item.game == "Factorio":
|
||||
itemid = 0x09 # Hammer Sprite
|
||||
else:
|
||||
itemid = 0x21 # Bug Catching Net
|
||||
itemid = get_nonnative_item_sprite(location.item.game)
|
||||
# Keys in their native dungeon should use the orignal item code for keys
|
||||
elif location.parent_region.dungeon:
|
||||
if location.parent_region.dungeon.is_dungeon_item(location.item):
|
||||
@@ -751,7 +818,6 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
for music_address in music_addresses:
|
||||
rom.write_byte(music_address, music)
|
||||
|
||||
|
||||
if world.mapshuffle[player]:
|
||||
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
||||
|
||||
@@ -879,7 +945,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 +1027,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 +1037,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
|
||||
@@ -1070,7 +1136,6 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
byte = int(rom.read_byte(address))
|
||||
rom.write_byte(address, prize_replacements.get(byte, byte))
|
||||
|
||||
|
||||
# Fill in item substitutions table
|
||||
rom.write_bytes(0x184000, [
|
||||
# original_item, limit, replacement_item, filler
|
||||
@@ -1123,11 +1188,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
|
||||
@@ -1146,9 +1211,12 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
|
||||
# Set up requested clock settings
|
||||
if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']:
|
||||
rom.write_int32(0x180200, world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180204, world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180208, world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180200,
|
||||
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180204,
|
||||
world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180208,
|
||||
world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
|
||||
else:
|
||||
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
|
||||
@@ -1507,7 +1575,8 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
rom.write_byte(0xEFD95, digging_game_rng)
|
||||
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
|
||||
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
|
||||
rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room
|
||||
rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[
|
||||
player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room
|
||||
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
|
||||
|
||||
# remove shield from uncle
|
||||
@@ -1579,7 +1648,6 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
rom.write_byte(0x4BA1D, tile_set.get_len())
|
||||
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
||||
|
||||
|
||||
write_strings(rom, world, player, team)
|
||||
|
||||
rom.write_byte(0x18637C, 1 if world.remote_items[player] else 0)
|
||||
@@ -1650,8 +1718,21 @@ def write_custom_shops(rom, world, player):
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if item is None:
|
||||
break
|
||||
if not item['item'] in item_table: # item not native to ALTTP
|
||||
item_code = 0x21
|
||||
if not item['item'] in item_table: # item not native to ALTTP
|
||||
# This is a terrible way to do this, please fix later
|
||||
from worlds.hk.Items import lookup_id_to_name as hk_lookup
|
||||
from worlds.factorio.Technologies import lookup_id_to_name as factorio_lookup
|
||||
from worlds.minecraft.Items import lookup_id_to_name as mc_lookup
|
||||
item_name = item['item']
|
||||
if item_name in hk_lookup.values():
|
||||
item_game = 'Hollow Knight'
|
||||
elif item_name in factorio_lookup.values():
|
||||
item_game = 'Factorio'
|
||||
elif item_name in mc_lookup.values():
|
||||
item_game = 'Minecraft'
|
||||
else:
|
||||
item_game = 'Generic'
|
||||
item_code = get_nonnative_item_sprite(item_game)
|
||||
else:
|
||||
item_code = ItemFactory(item['item'], player).code
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
||||
@@ -1691,7 +1772,8 @@ def hud_format_text(text):
|
||||
|
||||
|
||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options,
|
||||
world=None, player=1, allow_random_on_event=False, reduceflashing=False, triforcehud:str = None):
|
||||
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
||||
triforcehud: str = None):
|
||||
local_random = random if not world else world.rom_seeds[player]
|
||||
|
||||
# enable instant item menu
|
||||
@@ -1716,22 +1798,22 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
||||
else:
|
||||
rom.write_byte(0x180048, 0x08)
|
||||
|
||||
|
||||
# Reduce flashing by nopping out instructions
|
||||
if reduceflashing:
|
||||
rom.write_bytes(0x17E07, [0x06]) # reduce amount of colors changed, add this branch if we need to reduce more ""+ [0x80] + [(0x81-0x08)]""
|
||||
rom.write_bytes(0x17EAB, [0xD0, 0x03, 0xA9, 0x40, 0x29, 0x60]) # nullifies aga lightning, cutscene, vitreous, bat, ether
|
||||
rom.write_bytes(0x17E07, [
|
||||
0x06]) # reduce amount of colors changed, add this branch if we need to reduce more ""+ [0x80] + [(0x81-0x08)]""
|
||||
rom.write_bytes(0x17EAB,
|
||||
[0xD0, 0x03, 0xA9, 0x40, 0x29, 0x60]) # nullifies aga lightning, cutscene, vitreous, bat, ether
|
||||
# ONLY write to black values with this low pale blue to indicate flashing, that's IT. ""BNE + : LDA #$2940 : + : RTS""
|
||||
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
|
||||
rom.write_bytes(0x3FA7B, [0x80, 0xac-0x7b]) # branch from palette writing lightning on death mountain
|
||||
rom.write_byte(0x10817F, 0x01) # internal rom option
|
||||
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
|
||||
rom.write_bytes(0x3FA7B, [0x80, 0xac - 0x7b]) # branch from palette writing lightning on death mountain
|
||||
rom.write_byte(0x10817F, 0x01) # internal rom option
|
||||
else:
|
||||
rom.write_bytes(0x17E07, [0x00])
|
||||
rom.write_bytes(0x17E07, [0x00])
|
||||
rom.write_bytes(0x17EAB, [0x85, 0x00, 0x29, 0x1F, 0x00, 0x18])
|
||||
rom.write_bytes(0x123FE, [0x32]) # original weather flash value
|
||||
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
|
||||
rom.write_byte(0x10817F, 0x00) # internal rom option
|
||||
|
||||
rom.write_bytes(0x123FE, [0x32]) # original weather flash value
|
||||
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
|
||||
rom.write_byte(0x10817F, 0x00) # internal rom option
|
||||
|
||||
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
|
||||
|
||||
@@ -1766,7 +1848,8 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
||||
|
||||
if triforcehud:
|
||||
# set triforcehud
|
||||
triforce_flag = (rom.read_byte(0x180167) & 0x80) | {'normal': 0x00, 'hide_goal': 0x01, 'hide_required': 0x02, 'hide_both': 0x03}[triforcehud]
|
||||
triforce_flag = (rom.read_byte(0x180167) & 0x80) | \
|
||||
{'normal': 0x00, 'hide_goal': 0x01, 'hide_required': 0x02, 'hide_both': 0x03}[triforcehud]
|
||||
rom.write_byte(0x180167, triforce_flag)
|
||||
|
||||
if z3pr:
|
||||
@@ -1775,7 +1858,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':
|
||||
@@ -2014,7 +2097,7 @@ def write_strings(rom, world, player, team):
|
||||
tt.removeUnwantedText()
|
||||
|
||||
# Let's keep this guy's text accurate to the shuffle setting.
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']:
|
||||
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||
|
||||
@@ -2045,7 +2128,7 @@ def write_strings(rom, world, player, team):
|
||||
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
|
||||
# Bottle Vendor hint
|
||||
vendor_location = world.get_location("Bottle Merchant", player)
|
||||
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?"\
|
||||
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
|
||||
f"\n ≥ I want\n no way!\n{{CHOICE}}"
|
||||
|
||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
|
||||
@@ -2072,7 +2155,7 @@ def write_strings(rom, world, player, team):
|
||||
break
|
||||
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
|
||||
entrances_to_hint.update(InconvenientOtherEntrances)
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
hint_count = 0
|
||||
elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
|
||||
hint_count = 2
|
||||
@@ -2114,7 +2197,7 @@ def write_strings(rom, world, player, team):
|
||||
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
|
||||
else:
|
||||
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
||||
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0
|
||||
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 0
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
if hint_count:
|
||||
@@ -2128,10 +2211,10 @@ def write_strings(rom, world, player, team):
|
||||
|
||||
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
|
||||
locations_to_hint = InconvenientLocations.copy()
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
locations_to_hint.extend(InconvenientVanillaLocations)
|
||||
local_random.shuffle(locations_to_hint)
|
||||
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5
|
||||
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 5
|
||||
for location in locations_to_hint[:hint_count]:
|
||||
if location == 'Swamp Left':
|
||||
if local_random.randint(0, 1):
|
||||
@@ -2190,7 +2273,7 @@ def write_strings(rom, world, player, team):
|
||||
if world.bigkeyshuffle[player]:
|
||||
items_to_hint.extend(BigKeys)
|
||||
local_random.shuffle(items_to_hint)
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8
|
||||
while hint_count > 0 and items_to_hint:
|
||||
this_item = items_to_hint.pop(0)
|
||||
this_location = world.find_items(this_item, player)
|
||||
@@ -2218,7 +2301,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',
|
||||
@@ -2394,7 +2477,7 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_byte(0xDC21D, 0x6B) # inverted mode flute activation (skip weathervane overlay)
|
||||
rom.write_bytes(0x48DB3, [0xF8, 0x01]) # inverted mode (bird X)
|
||||
rom.write_byte(0x48D5E, 0x01) # inverted mode (rock X)
|
||||
rom.write_bytes(0x48CC1+36, bytes([0xF8]*12)) # (rock X)
|
||||
rom.write_bytes(0x48CC1 + 36, bytes([0xF8] * 12)) # (rock X)
|
||||
rom.write_int16s(snes_to_pc(0x02E849),
|
||||
[0x0043, 0x0056, 0x0058, 0x006C, 0x006F, 0x0070, 0x007B, 0x007F, 0x001B]) # dw flute
|
||||
rom.write_int16(snes_to_pc(0x02E8D5), 0x07C8)
|
||||
@@ -2407,7 +2490,7 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_byte(0xDBB73 + 0x36, 0x24)
|
||||
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
|
||||
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
rom.write_byte(0x15B8C, 0x6C)
|
||||
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
|
||||
rom.write_byte(0xDBB73 + 0x52, 0x01)
|
||||
@@ -2465,7 +2548,7 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
|
||||
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
|
||||
# keep the old man spawn point at old man house unless shuffle is vanilla
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']:
|
||||
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
|
||||
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
|
||||
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
|
||||
@@ -2528,7 +2611,7 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
|
||||
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
|
||||
rom.write_int16(snes_to_pc(0x308320), 0x001B)
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
rom.write_byte(snes_to_pc(0x308340), 0x7B)
|
||||
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
|
||||
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
|
||||
@@ -2565,10 +2648,10 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
|
||||
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
|
||||
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
rom.write_byte(0xDBB73 + 0x35, 0x36)
|
||||
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
|
||||
rom.write_byte(0x15B8C + 0x37, 0x1B)
|
||||
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
|
||||
|
||||
@@ -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)
|
||||
@@ -916,7 +916,9 @@ def set_trock_key_rules(world, player):
|
||||
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
|
||||
else:
|
||||
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
|
||||
world.push_item(world.get_location('Turtle Rock - Big Key Chest', player), ItemFactory('Small Key (Turtle Rock)', player), False)
|
||||
item = ItemFactory('Small Key (Turtle Rock)', player)
|
||||
item.world = world
|
||||
world.push_item(world.get_location('Turtle Rock - Big Key Chest', player), item, False)
|
||||
world.get_location('Turtle Rock - Big Key Chest', player).event = True
|
||||
toss_junk_item(world, player)
|
||||
|
||||
|
||||
@@ -218,7 +218,9 @@ def ShopSlotFill(world):
|
||||
continue
|
||||
|
||||
item_name = location.item.name
|
||||
if any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
if location.item.game != "A Link to the Past":
|
||||
price = world.random.randrange(1, 28)
|
||||
elif any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
price = world.random.randrange(1, 7)
|
||||
elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
|
||||
price = world.random.randrange(2, 14)
|
||||
|
||||
@@ -91,25 +91,6 @@ from BaseClasses import Location, Item
|
||||
# self.dark_room_logic = "lamp"
|
||||
# self.restrict_dungeon_item_on_boss = False
|
||||
#
|
||||
# @property
|
||||
# def sewer_light_cone(self):
|
||||
# return self.mode == "standard"
|
||||
#
|
||||
# @property
|
||||
# def fix_trock_doors(self):
|
||||
# return self.shuffle != 'vanilla' or self.mode == 'inverted'
|
||||
#
|
||||
# @property
|
||||
# def fix_skullwoods_exit(self):
|
||||
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
|
||||
#
|
||||
# @property
|
||||
# def fix_palaceofdarkness_exit(self):
|
||||
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
|
||||
#
|
||||
# @property
|
||||
# def fix_trock_exit(self):
|
||||
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
|
||||
|
||||
|
||||
class ALttPLocation(Location):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,30 +27,65 @@ base_info = {
|
||||
"factorio_version": "1.1"
|
||||
}
|
||||
|
||||
def generate_mod(world: MultiWorld, player: int):
|
||||
# TODO: clean this up, probably as a jinja macro; then add logic for the recipes in completion condition
|
||||
rocket_recipes = {
|
||||
Options.MaxSciencePack.option_space_science_pack:
|
||||
'{{"rocket-control-unit", 10}, {"low-density-structure", 10}, {"rocket-fuel", 10}}',
|
||||
Options.MaxSciencePack.option_utility_science_pack:
|
||||
'{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
|
||||
Options.MaxSciencePack.option_production_science_pack:
|
||||
'{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
|
||||
Options.MaxSciencePack.option_chemical_science_pack:
|
||||
'{{"advanced-circuit", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
|
||||
Options.MaxSciencePack.option_military_science_pack:
|
||||
'{{"defender-capsule", 10}, {"stone-wall", 10}, {"coal", 10}}',
|
||||
Options.MaxSciencePack.option_logistic_science_pack:
|
||||
'{{"electronic-circuit", 10}, {"stone-brick", 10}, {"coal", 10}}',
|
||||
Options.MaxSciencePack.option_automation_science_pack:
|
||||
'{{"copper-cable", 10}, {"iron-plate", 10}, {"wood", 10}}'
|
||||
}
|
||||
|
||||
def generate_mod(world: MultiWorld, player: int, seedname: str):
|
||||
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?
|
||||
locations.append((location.name, location.item.name, location.item.player))
|
||||
mod_name = f"archipelago-client-{world.seed}-{player}"
|
||||
locations.append((location.name, location.item.name, location.item.player))
|
||||
mod_name = f"AP-{seedname}-P{player}-{world.player_names[player][0]}"
|
||||
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, "custom_data": world.custom_data[player],
|
||||
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player],
|
||||
"rocket_recipe" : rocket_recipes[world.max_science_pack[player].value],
|
||||
"slot_name": world.player_names[player][0],
|
||||
"starting_items": world.starting_items[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 +93,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)
|
||||
|
||||
|
||||
95
worlds/factorio/Shapes.py
Normal file
95
worlds/factorio/Shapes.py
Normal file
@@ -0,0 +1,95 @@
|
||||
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
|
||||
custom_technologies = world.custom_data[player]["custom_technologies"]
|
||||
if layout == TechTreeLayout.option_small_diamonds:
|
||||
slice_size = 4
|
||||
tech_names: List[str] = list(set(custom_technologies) - 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(custom_technologies[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(custom_technologies) - 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(custom_technologies[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(custom_technologies) - world._static_nodes)
|
||||
tech_names.sort()
|
||||
world.random.shuffle(tech_names)
|
||||
tech_names.sort(key=lambda tech_name: len(custom_technologies[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(custom_technologies) - 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))
|
||||
tech_names.sort()
|
||||
world.random.shuffle(tech_names)
|
||||
tech_names.sort(key=lambda tech_name: len(custom_technologies[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,154 @@
|
||||
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)
|
||||
tech_table = {}
|
||||
with open(recipe_source_file) as f:
|
||||
raw_recipes = json.load(f)
|
||||
tech_table: Dict[str, int] = {}
|
||||
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, factorio_id):
|
||||
self.name = name
|
||||
self.factorio_id = factorio_id
|
||||
self.ingredients = ingredients
|
||||
|
||||
def build_rule(self, player: int):
|
||||
logging.debug(f"Building rules for {self.name}")
|
||||
ingredient_rules = []
|
||||
for ingredient in self.ingredients:
|
||||
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})"
|
||||
|
||||
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
|
||||
return CustomTechnology(self, world, allowed_packs, player)
|
||||
|
||||
|
||||
class CustomTechnology(Technology):
|
||||
"""A particularly configured Technology for a world."""
|
||||
|
||||
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
|
||||
ingredients = origin.ingredients & allowed_packs
|
||||
self.player = player
|
||||
if world.random_tech_ingredients[player]:
|
||||
ingredients = list(ingredients)
|
||||
ingredients.sort() # deterministic sample
|
||||
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
||||
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
|
||||
|
||||
|
||||
|
||||
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, factorio_id)
|
||||
factorio_id += 1
|
||||
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, Set[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 and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging
|
||||
for product_name in recipe.products:
|
||||
all_product_sources.setdefault(product_name, set()).add(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}
|
||||
recipes = all_product_sources.get(ingredient_name)
|
||||
if not recipes:
|
||||
return set()
|
||||
current_technologies = set()
|
||||
for recipe in recipes:
|
||||
current_technologies |= recipe.unlocking_technologies
|
||||
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,23 +1,22 @@
|
||||
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)
|
||||
world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player)
|
||||
set_rules(world, player, custom_technologies)
|
||||
|
||||
|
||||
def factorio_create_regions(world: MultiWorld, player: int):
|
||||
@@ -30,30 +29,33 @@ 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_custom_technologies(world: MultiWorld, player: int):
|
||||
custom_technologies = {}
|
||||
world_custom = getattr(world, "_custom_technologies", {})
|
||||
world_custom[player] = custom_technologies
|
||||
world._custom_technologies = world_custom
|
||||
allowed_packs = world.max_science_pack[player].get_allowed_packs()
|
||||
for technology_name, technology in technology_table.items():
|
||||
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
|
||||
return custom_technologies
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
def set_rules(world: MultiWorld, player: int, custom_technologies):
|
||||
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())
|
||||
# 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))
|
||||
|
||||
for recipe, technology in recipe_sources.items():
|
||||
Rules.set_rule(world.get_location(recipe, player), lambda state, tech=technology: state.has(tech, player))
|
||||
for tech_name, technology in custom_technologies.items():
|
||||
location = world.get_location(tech_name, player)
|
||||
Rules.set_rule(location, technology.build_rule(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))
|
||||
|
||||
|
||||
world.completion_condition[player] = lambda state: all(state.has(ingredient, player)
|
||||
for ingredient in all_ingredients)
|
||||
# get all science pack technologies (but not the ability to craft them)
|
||||
world.completion_condition[player] = lambda state: all(state.has(technology, player)
|
||||
for technology in advancement_technologies)
|
||||
|
||||
@@ -80,7 +80,7 @@ def gen_items(world: MultiWorld, player: int):
|
||||
|
||||
if item_data.type == "Event":
|
||||
event_location = world.get_location(item_name, player)
|
||||
world.push_item(event_location, item)
|
||||
world.push_item(event_location, item, collect=False)
|
||||
event_location.event = True
|
||||
event_location.locked = True
|
||||
if item.name == "King's_Pass":
|
||||
@@ -94,13 +94,12 @@ def gen_items(world: MultiWorld, player: int):
|
||||
world.push_item(event_location, item)
|
||||
event_location.event = True
|
||||
event_location.locked = True
|
||||
world.push_precollected(item)
|
||||
|
||||
elif item_data.type == "Fake":
|
||||
pass
|
||||
elif item_data.type in not_shufflable_types:
|
||||
location = world.get_location(item_name, player)
|
||||
world.push_item(location, item)
|
||||
world.push_item(location, item, collect=False)
|
||||
location.event = item.advancement
|
||||
location.locked = True
|
||||
else:
|
||||
@@ -110,7 +109,7 @@ def gen_items(world: MultiWorld, player: int):
|
||||
pool.append(item)
|
||||
else:
|
||||
location = world.get_location(item_name, player)
|
||||
world.push_item(location, item)
|
||||
world.push_item(location, item, collect=False)
|
||||
location.event = item.advancement
|
||||
location.locked = True
|
||||
logger.debug(f"Placed {item_name} to vanilla for player {player}")
|
||||
|
||||
74
worlds/minecraft/Items.py
Normal file
74
worlds/minecraft/Items.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||
import typing
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: int
|
||||
progression: bool
|
||||
|
||||
class MinecraftItem(Item):
|
||||
game: str = "Minecraft"
|
||||
def __init__(self, name: str, progression: bool, code: int, player: int):
|
||||
super().__init__(name, progression, code if code else None, player)
|
||||
|
||||
item_table = {
|
||||
"Archery": ItemData(45000, True),
|
||||
"Ingot Crafting": ItemData(45001, True),
|
||||
"Resource Blocks": ItemData(45002, True),
|
||||
"Brewing": ItemData(45003, True),
|
||||
"Enchanting": ItemData(45004, True),
|
||||
"Bucket": ItemData(45005, True),
|
||||
"Flint and Steel": ItemData(45006, True),
|
||||
"Bed": ItemData(45007, True),
|
||||
"Bottles": ItemData(45008, True),
|
||||
"Shield": ItemData(45009, True),
|
||||
"Fishing Rod": ItemData(45010, True),
|
||||
"Campfire": ItemData(45011, True),
|
||||
"Progressive Weapons": ItemData(45012, True),
|
||||
"Progressive Tools": ItemData(45013, True),
|
||||
"Progressive Armor": ItemData(45014, True),
|
||||
"8 Netherite Scrap": ItemData(45015, True),
|
||||
"8 Emeralds": ItemData(45016, False),
|
||||
"4 Emeralds": ItemData(45017, False),
|
||||
"Channeling Book": ItemData(45018, True),
|
||||
"Silk Touch Book": ItemData(45019, True),
|
||||
"Sharpness III Book": ItemData(45020, False),
|
||||
"Piercing IV Book": ItemData(45021, True),
|
||||
"Looting III Book": ItemData(45022, False),
|
||||
"Infinity Book": ItemData(45023, False),
|
||||
"4 Diamond Ore": ItemData(45024, False),
|
||||
"16 Iron Ore": ItemData(45025, False),
|
||||
"500 XP": ItemData(45026, False),
|
||||
"100 XP": ItemData(45027, False),
|
||||
"50 XP": ItemData(45028, False),
|
||||
"3 Ender Pearls": ItemData(45029, True),
|
||||
"4 Lapis Lazuli": ItemData(45030, False),
|
||||
"16 Porkchops": ItemData(45031, False),
|
||||
"8 Gold Ore": ItemData(45032, False),
|
||||
"Rotten Flesh": ItemData(45033, False),
|
||||
"Single Arrow": ItemData(45034, False),
|
||||
|
||||
"Victory": ItemData(0, True)
|
||||
}
|
||||
|
||||
# If not listed here then has frequency 1
|
||||
item_frequencies = {
|
||||
"Progressive Weapons": 3,
|
||||
"Progressive Tools": 3,
|
||||
"Progressive Armor": 2,
|
||||
"8 Netherite Scrap": 2,
|
||||
"8 Emeralds": 0,
|
||||
"4 Emeralds": 8,
|
||||
"4 Diamond Ore": 4,
|
||||
"16 Iron Ore": 4,
|
||||
"500 XP": 4, # 2 after exclusions
|
||||
"100 XP": 10, # 4 after exclusions
|
||||
"50 XP": 12, # 4 after exclusions
|
||||
"3 Ender Pearls": 4,
|
||||
"4 Lapis Lazuli": 2,
|
||||
"16 Porkchops": 8,
|
||||
"8 Gold Ore": 4,
|
||||
"Rotten Flesh": 4,
|
||||
"Single Arrow": 0
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||
142
worlds/minecraft/Locations.py
Normal file
142
worlds/minecraft/Locations.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||
import typing
|
||||
|
||||
class AdvData(typing.NamedTuple):
|
||||
id: int
|
||||
region: str
|
||||
|
||||
class MinecraftAdvancement(Location):
|
||||
game: str = "Minecraft"
|
||||
def __init__(self, player: int, name: str, address: int, parent):
|
||||
super().__init__(player, name, address if address else None, parent)
|
||||
self.event = True if address == 0 else False
|
||||
|
||||
advancement_table = {
|
||||
"Who is Cutting Onions?": AdvData(42000, 'Overworld'),
|
||||
"Oh Shiny": AdvData(42001, 'Overworld'),
|
||||
"Suit Up": AdvData(42002, 'Overworld'),
|
||||
"Very Very Frightening": AdvData(42003, 'Village'),
|
||||
"Hot Stuff": AdvData(42004, 'Overworld'),
|
||||
"Free the End": AdvData(42005, 'The End'),
|
||||
"A Furious Cocktail": AdvData(42006, 'Nether Fortress'),
|
||||
"Best Friends Forever": AdvData(42007, 'Overworld'),
|
||||
"Bring Home the Beacon": AdvData(42008, 'Nether Fortress'),
|
||||
"Not Today, Thank You": AdvData(42009, 'Overworld'),
|
||||
"Isn't It Iron Pick": AdvData(42010, 'Overworld'),
|
||||
"Local Brewery": AdvData(42011, 'Nether Fortress'),
|
||||
"The Next Generation": AdvData(42012, 'The End'),
|
||||
"Fishy Business": AdvData(42013, 'Overworld'),
|
||||
"Hot Tourist Destinations": AdvData(42014, 'The Nether'),
|
||||
"This Boat Has Legs": AdvData(42015, 'The Nether'),
|
||||
"Sniper Duel": AdvData(42016, 'Overworld'),
|
||||
"Nether": AdvData(42017, 'The Nether'),
|
||||
"Great View From Up Here": AdvData(42018, 'End City'),
|
||||
"How Did We Get Here?": AdvData(42019, 'Nether Fortress'),
|
||||
"Bullseye": AdvData(42020, 'Overworld'),
|
||||
"Spooky Scary Skeleton": AdvData(42021, 'Nether Fortress'),
|
||||
"Two by Two": AdvData(42022, 'The Nether'),
|
||||
"Stone Age": AdvData(42023, 'Overworld'),
|
||||
"Two Birds, One Arrow": AdvData(42024, 'Overworld'),
|
||||
"We Need to Go Deeper": AdvData(42025, 'The Nether'),
|
||||
"Who's the Pillager Now?": AdvData(42026, 'Pillager Outpost'),
|
||||
"Getting an Upgrade": AdvData(42027, 'Overworld'),
|
||||
"Tactical Fishing": AdvData(42028, 'Overworld'),
|
||||
"Zombie Doctor": AdvData(42029, 'Overworld'),
|
||||
"The City at the End of the Game": AdvData(42030, 'End City'),
|
||||
"Ice Bucket Challenge": AdvData(42031, 'Overworld'),
|
||||
"Remote Getaway": AdvData(42032, 'The End'),
|
||||
"Into Fire": AdvData(42033, 'Nether Fortress'),
|
||||
"War Pigs": AdvData(42034, 'Bastion Remnant'),
|
||||
"Take Aim": AdvData(42035, 'Overworld'),
|
||||
"Total Beelocation": AdvData(42036, 'Overworld'),
|
||||
"Arbalistic": AdvData(42037, 'Overworld'),
|
||||
"The End... Again...": AdvData(42038, 'The End'),
|
||||
"Acquire Hardware": AdvData(42039, 'Overworld'),
|
||||
"Not Quite \"Nine\" Lives": AdvData(42040, 'The Nether'),
|
||||
"Cover Me With Diamonds": AdvData(42041, 'Overworld'),
|
||||
"Sky's the Limit": AdvData(42042, 'End City'),
|
||||
"Hired Help": AdvData(42043, 'Overworld'),
|
||||
"Return to Sender": AdvData(42044, 'The Nether'),
|
||||
"Sweet Dreams": AdvData(42045, 'Overworld'),
|
||||
"You Need a Mint": AdvData(42046, 'The End'),
|
||||
"Adventure": AdvData(42047, 'Overworld'),
|
||||
"Monsters Hunted": AdvData(42048, 'Overworld'),
|
||||
"Enchanter": AdvData(42049, 'Overworld'),
|
||||
"Voluntary Exile": AdvData(42050, 'Pillager Outpost'),
|
||||
"Eye Spy": AdvData(42051, 'Overworld'),
|
||||
"The End": AdvData(42052, 'The End'),
|
||||
"Serious Dedication": AdvData(42053, 'The Nether'),
|
||||
"Postmortal": AdvData(42054, 'Village'),
|
||||
"Monster Hunter": AdvData(42055, 'Overworld'),
|
||||
"Adventuring Time": AdvData(42056, 'Overworld'),
|
||||
"A Seedy Place": AdvData(42057, 'Overworld'),
|
||||
"Those Were the Days": AdvData(42058, 'Bastion Remnant'),
|
||||
"Hero of the Village": AdvData(42059, 'Village'),
|
||||
"Hidden in the Depths": AdvData(42060, 'The Nether'),
|
||||
"Beaconator": AdvData(42061, 'Nether Fortress'),
|
||||
"Withering Heights": AdvData(42062, 'Nether Fortress'),
|
||||
"A Balanced Diet": AdvData(42063, 'Village'),
|
||||
"Subspace Bubble": AdvData(42064, 'The Nether'),
|
||||
"Husbandry": AdvData(42065, 'Overworld'),
|
||||
"Country Lode, Take Me Home": AdvData(42066, 'The Nether'),
|
||||
"Bee Our Guest": AdvData(42067, 'Overworld'),
|
||||
"What a Deal!": AdvData(42068, 'Village'),
|
||||
"Uneasy Alliance": AdvData(42069, 'The Nether'),
|
||||
"Diamonds!": AdvData(42070, 'Overworld'),
|
||||
"A Terrible Fortress": AdvData(42071, 'Nether Fortress'),
|
||||
"A Throwaway Joke": AdvData(42072, 'Overworld'),
|
||||
"Minecraft": AdvData(42073, 'Overworld'),
|
||||
"Sticky Situation": AdvData(42074, 'Overworld'),
|
||||
"Ol' Betsy": AdvData(42075, 'Overworld'),
|
||||
"Cover Me in Debris": AdvData(42076, 'The Nether'),
|
||||
"The End?": AdvData(42077, 'The End'),
|
||||
"The Parrots and the Bats": AdvData(42078, 'Overworld'),
|
||||
"A Complete Catalogue": AdvData(42079, 'Village'),
|
||||
"Getting Wood": AdvData(42080, 'Overworld'),
|
||||
"Time to Mine!": AdvData(42081, 'Overworld'),
|
||||
"Hot Topic": AdvData(42082, 'Overworld'),
|
||||
"Bake Bread": AdvData(42083, 'Overworld'),
|
||||
"The Lie": AdvData(42084, 'Overworld'),
|
||||
"On a Rail": AdvData(42085, 'Overworld'),
|
||||
"Time to Strike!": AdvData(42086, 'Overworld'),
|
||||
"Cow Tipper": AdvData(42087, 'Overworld'),
|
||||
"When Pigs Fly": AdvData(42088, 'Overworld'),
|
||||
"Overkill": AdvData(42089, 'Nether Fortress'),
|
||||
"Librarian": AdvData(42090, 'Overworld'),
|
||||
"Overpowered": AdvData(42091, 'Overworld'),
|
||||
|
||||
"Ender Dragon": AdvData(0, 'The End')
|
||||
}
|
||||
|
||||
exclusion_table = {
|
||||
"hard": {
|
||||
"Very Very Frightening": "50 XP",
|
||||
"Two by Two": "100 XP",
|
||||
"Two Birds, One Arrow": "50 XP",
|
||||
"Arbalistic": "100 XP",
|
||||
"Beaconator": "50 XP",
|
||||
"A Balanced Diet": "100 XP",
|
||||
"Uneasy Alliance": "100 XP",
|
||||
"Cover Me in Debris": "100 XP",
|
||||
"A Complete Catalogue": "50 XP",
|
||||
"Overpowered": "50 XP"
|
||||
},
|
||||
"insane": {
|
||||
"How Did We Get Here?": "500 XP",
|
||||
"Adventuring Time": "500 XP"
|
||||
},
|
||||
"postgame": {
|
||||
"The Next Generation": "50 XP",
|
||||
"The End... Again...": "50 XP",
|
||||
"You Need a Mint": "50 XP",
|
||||
"Monsters Hunted": "100 XP"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
events_table = {
|
||||
"Ender Dragon": "Victory"
|
||||
}
|
||||
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {loc_data.id: loc_name for loc_name, loc_data in advancement_table.items() if loc_data.id}
|
||||
101
worlds/minecraft/Regions.py
Normal file
101
worlds/minecraft/Regions.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from .Locations import MinecraftAdvancement, advancement_table
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||
|
||||
def minecraft_create_regions(world: MultiWorld, player: int):
|
||||
|
||||
def MCRegion(region_name: str, exits=[]):
|
||||
ret = Region(region_name, None, region_name, player)
|
||||
ret.world = world
|
||||
ret.locations = [ MinecraftAdvancement(player, loc_name, loc_data.id, ret)
|
||||
for loc_name, loc_data in advancement_table.items()
|
||||
if loc_data.region == region_name ]
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
return ret
|
||||
|
||||
world.regions += [MCRegion(*r) for r in mc_regions]
|
||||
|
||||
for (exit, region) in mandatory_connections:
|
||||
world.get_entrance(exit, player).connect(world.get_region(region, player))
|
||||
|
||||
def link_minecraft_structures(world: MultiWorld, player: int):
|
||||
|
||||
# Get all unpaired exits and all regions without entrances (except the Menu)
|
||||
# This function is destructive on these lists.
|
||||
exits = [exit.name for r in world.regions if r.player == player for exit in r.exits if exit.connected_region == None]
|
||||
structs = [r.name for r in world.regions if r.player == player and r.entrances == [] and r.name != 'Menu']
|
||||
try:
|
||||
assert len(exits) == len(structs)
|
||||
except AssertionError as e: # this should never happen
|
||||
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player}") from e
|
||||
num_regions = len(exits)
|
||||
pairs = {}
|
||||
|
||||
def check_valid_connection(exit, struct):
|
||||
if (exit in exits) and (struct in structs) and (exit not in pairs):
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_pair(exit, struct):
|
||||
pairs[exit] = struct
|
||||
exits.remove(exit)
|
||||
structs.remove(struct)
|
||||
|
||||
# Plando stuff. Remove any utilized exits/structs from the lists.
|
||||
# Raise error if trying to put Nether Fortress in the End.
|
||||
|
||||
if world.shuffle_structures[player]:
|
||||
# Can't put Nether Fortress in the End
|
||||
if 'The End Structure' in exits and 'Nether Fortress' in structs:
|
||||
try:
|
||||
end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress'])
|
||||
set_pair('The End Structure', end_struct)
|
||||
except IndexError as e:
|
||||
raise Exception(f"Plando forced Nether Fortress in the End for player {player}") from e
|
||||
world.random.shuffle(structs)
|
||||
for exit, struct in zip(exits[:], structs[:]):
|
||||
set_pair(exit, struct)
|
||||
else: # write remaining default connections
|
||||
for (exit, struct) in default_connections:
|
||||
if exit in exits:
|
||||
set_pair(exit, struct)
|
||||
|
||||
# Make sure we actually paired everything; might fail if plando
|
||||
try:
|
||||
assert len(exits) == len(structs) == 0
|
||||
except AssertionError as e:
|
||||
raise Exception(f"Failed to connect all Minecraft structures for player {player}; check plando settings in yaml") from e
|
||||
|
||||
for exit, struct in pairs.items():
|
||||
world.get_entrance(exit, player).connect(world.get_region(struct, player))
|
||||
if world.shuffle_structures[player]:
|
||||
world.spoiler.set_entrance(exit, struct, 'entrance', player)
|
||||
|
||||
# (Region name, list of exits)
|
||||
mc_regions = [
|
||||
('Menu', ['New World']),
|
||||
('Overworld', ['Nether Portal', 'End Portal', 'Overworld Structure 1', 'Overworld Structure 2']),
|
||||
('The Nether', ['Nether Structure 1', 'Nether Structure 2']),
|
||||
('The End', ['The End Structure']),
|
||||
('Village', []),
|
||||
('Pillager Outpost', []),
|
||||
('Nether Fortress', []),
|
||||
('Bastion Remnant', []),
|
||||
('End City', [])
|
||||
]
|
||||
|
||||
# (Entrance, region pointed to)
|
||||
mandatory_connections = [
|
||||
('New World', 'Overworld'),
|
||||
('Nether Portal', 'The Nether'),
|
||||
('End Portal', 'The End')
|
||||
]
|
||||
|
||||
default_connections = {
|
||||
('Overworld Structure 1', 'Village'),
|
||||
('Overworld Structure 2', 'Pillager Outpost'),
|
||||
('Nether Structure 1', 'Nether Fortress'),
|
||||
('Nether Structure 2', 'Bastion Remnant'),
|
||||
('The End Structure', 'End City')
|
||||
}
|
||||
143
worlds/minecraft/Rules.py
Normal file
143
worlds/minecraft/Rules.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from ..generic.Rules import set_rule
|
||||
from .Locations import exclusion_table, events_table
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||
from Options import AdvancementGoal
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
|
||||
def reachable_locations(state):
|
||||
postgame_advancements = set(exclusion_table['postgame'].keys())
|
||||
postgame_advancements.add('Free the End')
|
||||
for event in events_table.keys():
|
||||
postgame_advancements.add(event)
|
||||
return [location for location in world.get_locations() if
|
||||
(player is None or location.player == player) and
|
||||
(location.name not in postgame_advancements) and
|
||||
location.can_reach(state)]
|
||||
|
||||
# 92 total advancements, 16 are typically excluded, 1 is Free the End. Goal is to complete X advancements and then Free the End.
|
||||
goal_map = {
|
||||
'few': 30,
|
||||
'normal': 50,
|
||||
'many': 70
|
||||
}
|
||||
goal = goal_map[getattr(world, 'advancement_goal')[player].get_option_name()]
|
||||
can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', player) and state.can_kill_ender_dragon(player)
|
||||
|
||||
if world.logic[player] != 'nologic':
|
||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
|
||||
set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and
|
||||
(state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and
|
||||
state.has_iron_ingots(player))
|
||||
set_rule(world.get_entrance("End Portal", player), lambda state: state.enter_stronghold(player) and state.has('3 Ender Pearls', player, 4))
|
||||
set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state.can_adventure(player))
|
||||
set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state.can_adventure(player))
|
||||
set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state.can_adventure(player))
|
||||
set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state.can_adventure(player))
|
||||
set_rule(world.get_entrance("The End Structure", player), lambda state: state.can_adventure(player))
|
||||
|
||||
set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state))
|
||||
|
||||
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state.can_piglin_trade(player))
|
||||
set_rule(world.get_location("Oh Shiny", player), lambda state: state.can_piglin_trade(player))
|
||||
set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant(player))
|
||||
set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Free the End", player), lambda state: can_complete(state))
|
||||
set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and state.has("Fishing Rod", player) and state.can_reach('The Nether', 'Region', player))
|
||||
set_rule(world.get_location("Best Friends Forever", player), lambda state: True)
|
||||
set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and
|
||||
state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
|
||||
set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Local Brewery", player), lambda state: state.can_brew_potions(player))
|
||||
set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state))
|
||||
set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player))
|
||||
set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player))
|
||||
set_rule(world.get_location("This Boat Has Legs", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player))
|
||||
set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player))
|
||||
set_rule(world.get_location("Nether", player), lambda state: True)
|
||||
set_rule(world.get_location("Great View From Up Here", player), lambda state: state.basic_combat(player))
|
||||
set_rule(world.get_location("How Did We Get Here?", player), lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player) and # most effects; Absorption
|
||||
state.can_reach('End City', 'Region', player) and state.can_reach('The Nether', 'Region', player) and # Levitation; potion ingredients
|
||||
state.has("Fishing Rod", player) and state.has("Archery", player) and # Pufferfish, Nautilus Shells; spectral arrows
|
||||
state.can_reach("Bring Home the Beacon", "Location", player) and # Haste
|
||||
state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village
|
||||
set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state.basic_combat(player))
|
||||
set_rule(world.get_location("Two by Two", player), lambda state: state.has_iron_ingots(player) and state.can_adventure(player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots
|
||||
set_rule(world.get_location("Stone Age", player), lambda state: True)
|
||||
set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state.craft_crossbow(player) and state.can_enchant(player))
|
||||
set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True)
|
||||
set_rule(world.get_location("Who's the Pillager Now?", player), lambda state: state.craft_crossbow(player))
|
||||
set_rule(world.get_location("Getting an Upgrade", player), lambda state: state.has("Progressive Tools", player))
|
||||
set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Zombie Doctor", player), lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player))
|
||||
set_rule(world.get_location("The City at the End of the Game", player), lambda state: True)
|
||||
set_rule(world.get_location("Ice Bucket Challenge", player), lambda state: state.has_diamond_pickaxe(player))
|
||||
set_rule(world.get_location("Remote Getaway", player), lambda state: True)
|
||||
set_rule(world.get_location("Into Fire", player), lambda state: state.basic_combat(player))
|
||||
set_rule(world.get_location("War Pigs", player), lambda state: state.basic_combat(player))
|
||||
set_rule(world.get_location("Take Aim", player), lambda state: state.has("Archery", player))
|
||||
set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state.can_use_anvil(player) and state.can_enchant(player))
|
||||
set_rule(world.get_location("Arbalistic", player), lambda state: state.craft_crossbow(player) and state.has("Piercing IV Book", player) and
|
||||
state.can_use_anvil(player) and state.can_enchant(player))
|
||||
set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state) and state.has("Ingot Crafting", player) and state.can_reach('The Nether', 'Region', player)) # furnace for glass, nether for ghast tears
|
||||
set_rule(world.get_location("Acquire Hardware", player), lambda state: state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state.can_piglin_trade(player) and state.has("Resource Blocks", player))
|
||||
set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player))
|
||||
set_rule(world.get_location("Sky's the Limit", player), lambda state: state.basic_combat(player))
|
||||
set_rule(world.get_location("Hired Help", player), lambda state: state.has("Resource Blocks", player) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Return to Sender", player), lambda state: True)
|
||||
set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player))
|
||||
set_rule(world.get_location("You Need a Mint", player), lambda state: can_complete(state) and state.has_bottle_mc(player))
|
||||
set_rule(world.get_location("Adventure", player), lambda state: True)
|
||||
set_rule(world.get_location("Monsters Hunted", player), lambda state: can_complete(state) and state.can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing
|
||||
set_rule(world.get_location("Enchanter", player), lambda state: state.can_enchant(player))
|
||||
set_rule(world.get_location("Voluntary Exile", player), lambda state: state.basic_combat(player))
|
||||
set_rule(world.get_location("Eye Spy", player), lambda state: state.enter_stronghold(player))
|
||||
set_rule(world.get_location("The End", player), lambda state: True)
|
||||
set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player))
|
||||
set_rule(world.get_location("Postmortal", player), lambda state: state.complete_raid(player))
|
||||
set_rule(world.get_location("Monster Hunter", player), lambda state: True)
|
||||
set_rule(world.get_location("Adventuring Time", player), lambda state: state.can_adventure(player))
|
||||
set_rule(world.get_location("A Seedy Place", player), lambda state: True)
|
||||
set_rule(world.get_location("Those Were the Days", player), lambda state: True)
|
||||
set_rule(world.get_location("Hero of the Village", player), lambda state: state.complete_raid(player))
|
||||
set_rule(world.get_location("Hidden in the Depths", player), lambda state: state.can_brew_potions(player) and state.has("Bed", player) and state.has_diamond_pickaxe(player)) # bed mining :)
|
||||
set_rule(world.get_location("Beaconator", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and
|
||||
state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
|
||||
set_rule(world.get_location("Withering Heights", player), lambda state: state.can_kill_wither(player))
|
||||
set_rule(world.get_location("A Balanced Diet", player), lambda state: state.has_bottle_mc(player) and state.has_gold_ingots(player) and # honey bottle; gapple
|
||||
state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit
|
||||
set_rule(world.get_location("Subspace Bubble", player), lambda state: state.has_diamond_pickaxe(player))
|
||||
set_rule(world.get_location("Husbandry", player), lambda state: True)
|
||||
set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player))
|
||||
set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player))
|
||||
set_rule(world.get_location("What a Deal!", player), lambda state: True)
|
||||
set_rule(world.get_location("Uneasy Alliance", player), lambda state: state.has_diamond_pickaxe(player))
|
||||
set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything
|
||||
set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned
|
||||
set_rule(world.get_location("Minecraft", player), lambda state: True)
|
||||
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has_bottle_mc(player))
|
||||
set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(player))
|
||||
set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and
|
||||
state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and
|
||||
state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player))
|
||||
set_rule(world.get_location("The End?", player), lambda state: True)
|
||||
set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True)
|
||||
set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw
|
||||
set_rule(world.get_location("Getting Wood", player), lambda state: True)
|
||||
set_rule(world.get_location("Time to Mine!", player), lambda state: True)
|
||||
set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Ingot Crafting", player))
|
||||
set_rule(world.get_location("Bake Bread", player), lambda state: True)
|
||||
set_rule(world.get_location("The Lie", player), lambda state: state.has_iron_ingots(player) and state.has("Bucket", player))
|
||||
set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player))
|
||||
set_rule(world.get_location("Time to Strike!", player), lambda state: True)
|
||||
set_rule(world.get_location("Cow Tipper", player), lambda state: True)
|
||||
set_rule(world.get_location("When Pigs Fly", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player) and state.can_adventure(player)) # saddles in fortress chests
|
||||
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and state.has("Progressive Weapons", player)) # strength 1, stone axe crit
|
||||
set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player))
|
||||
set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player))
|
||||
68
worlds/minecraft/__init__.py
Normal file
68
worlds/minecraft/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from random import Random
|
||||
from .Items import MinecraftItem, item_table, item_frequencies
|
||||
from .Locations import exclusion_table, events_table
|
||||
from .Regions import link_minecraft_structures
|
||||
from .Rules import set_rules
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||
from Options import minecraft_options
|
||||
|
||||
client_version = (0, 3)
|
||||
|
||||
def generate_mc_data(world: MultiWorld, player: int, seedname: str):
|
||||
import base64, json
|
||||
from Utils import output_path
|
||||
|
||||
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", "The End Structure"]
|
||||
data = {
|
||||
'world_seed': Random(world.rom_seeds[player]).getrandbits(32), # consistent and doesn't interfere with other generation
|
||||
'seed_name': seedname,
|
||||
'player_name': world.get_player_names(player),
|
||||
'client_version': client_version,
|
||||
'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits}
|
||||
}
|
||||
|
||||
filename = f"AP_{seedname}_P{player}_{world.get_player_names(player)}.apmc"
|
||||
with open(output_path(filename), 'wb') as f:
|
||||
f.write(base64.b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
def fill_minecraft_slot_data(world: MultiWorld, player: int):
|
||||
slot_data = {}
|
||||
for option_name in minecraft_options:
|
||||
option = getattr(world, option_name)[player]
|
||||
slot_data[option_name] = int(option.value)
|
||||
slot_data['client_version'] = client_version
|
||||
return slot_data
|
||||
|
||||
# Generates the item pool given the table and frequencies in Items.py.
|
||||
def minecraft_gen_item_pool(world: MultiWorld, player: int):
|
||||
|
||||
pool = []
|
||||
for item_name, item_data in item_table.items():
|
||||
for count in range(item_frequencies.get(item_name, 1)):
|
||||
pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, player))
|
||||
|
||||
prefill_pool = {}
|
||||
prefill_pool.update(events_table)
|
||||
exclusion_pools = ['hard', 'insane', 'postgame']
|
||||
for key in exclusion_pools:
|
||||
if not getattr(world, f"include_{key}_advancements")[player]:
|
||||
prefill_pool.update(exclusion_table[key])
|
||||
|
||||
for loc_name, item_name in prefill_pool.items():
|
||||
item_data = item_table[item_name]
|
||||
location = world.get_location(loc_name, player)
|
||||
item = MinecraftItem(item_name, item_data.progression, item_data.code, player)
|
||||
world.push_item(location, item, collect=False)
|
||||
pool.remove(item)
|
||||
location.event = item_data.progression
|
||||
location.locked = True
|
||||
|
||||
world.itempool += pool
|
||||
|
||||
# Generate Minecraft world.
|
||||
def gen_minecraft(world: MultiWorld, player: int):
|
||||
link_minecraft_structures(world, player)
|
||||
minecraft_gen_item_pool(world, player)
|
||||
set_rules(world, player)
|
||||
|
||||
Reference in New Issue
Block a user