mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 00:43:20 -07:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
babd809fa6 | ||
|
|
54177c7064 | ||
|
|
4884184e4a | ||
|
|
4c7ef593be | ||
|
|
2600e9a805 | ||
|
|
6ac74f5686 | ||
|
|
172c1789a8 | ||
|
|
ffc00b7800 | ||
|
|
f44f015cb9 | ||
|
|
a4dcda16c1 | ||
|
|
9db506ef42 | ||
|
|
007f2caecf | ||
|
|
80a5845695 | ||
|
|
1b5525a8c5 | ||
|
|
22d45b9571 | ||
|
|
773602169d | ||
|
|
b650d3d9e6 | ||
|
|
9b2171088e | ||
|
|
e58ae58e24 | ||
|
|
a11e840d36 | ||
|
|
7d5b20ccfc | ||
|
|
2530d28c9d | ||
|
|
c669bc3e7f | ||
|
|
5943c8975a | ||
|
|
d9f97f6aad | ||
|
|
576521229c | ||
|
|
ac919f72a8 | ||
|
|
85ce2aff47 | ||
|
|
8030db03ad | ||
|
|
1e90470862 | ||
|
|
e37ca97bde | ||
|
|
97f45f5d96 | ||
|
|
0a64caf4c5 | ||
|
|
eee6fc0f10 | ||
|
|
60972e026b | ||
|
|
fd9123610b | ||
|
|
6458653812 | ||
|
|
328d448ab2 | ||
|
|
10aca70879 | ||
|
|
92edc68890 | ||
|
|
4d4af9d74e | ||
|
|
92c21de61d | ||
|
|
f918d34098 | ||
|
|
95e0f551e8 | ||
|
|
43e17f82b0 | ||
|
|
c7417623e6 | ||
|
|
50ed657b0e | ||
|
|
8b5d7028f7 | ||
|
|
aa28b3887f | ||
|
|
739b563bc2 | ||
|
|
a3a68de341 | ||
|
|
57c761aa7d | ||
|
|
75891b2d38 | ||
|
|
44943f6bf8 | ||
|
|
5fdcd2d7c7 | ||
|
|
43e3c84635 | ||
|
|
7f8bb10fc5 | ||
|
|
cc85edafc4 | ||
|
|
878ab33039 | ||
|
|
4b495557cd | ||
|
|
d1fd1cd788 | ||
|
|
f870bb3fad | ||
|
|
719f9d7d48 | ||
|
|
fd811bfd1b | ||
|
|
6837cd2917 | ||
|
|
f778a263a7 | ||
|
|
007f66d86e | ||
|
|
0e32393acb | ||
|
|
20729242f9 | ||
|
|
91655a855d | ||
|
|
9f2f343f76 | ||
|
|
16ae77ca1c | ||
|
|
cd0306d513 | ||
|
|
b29d0b8276 | ||
|
|
e49d10ab22 | ||
|
|
059946d59e | ||
|
|
6211760922 | ||
|
|
d7a46f089e | ||
|
|
6e33181f05 |
175
BaseClasses.py
175
BaseClasses.py
@@ -142,38 +142,37 @@ class MultiWorld():
|
|||||||
|
|
||||||
|
|
||||||
def set_options(self, args):
|
def set_options(self, args):
|
||||||
import Options
|
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
for option_set in Options.option_sets:
|
|
||||||
for option in option_set:
|
|
||||||
setattr(self, option, getattr(args, option, {}))
|
|
||||||
for player in self.player_ids:
|
for player in self.player_ids:
|
||||||
self.custom_data[player] = {}
|
self.custom_data[player] = {}
|
||||||
self.worlds[player] = AutoWorld.AutoWorldRegister.world_types[self.game[player]](self, player)
|
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||||
|
for option in world_type.options:
|
||||||
|
setattr(self, option, getattr(args, option, {}))
|
||||||
|
self.worlds[player] = world_type(self, player)
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = secrets.SystemRandom()
|
self.random = secrets.SystemRandom()
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def player_ids(self):
|
def player_ids(self):
|
||||||
yield from range(1, self.players + 1)
|
return tuple(range(1, self.players + 1))
|
||||||
|
|
||||||
@property
|
# Todo: make these automatic, or something like get_players_for_game(game_name)
|
||||||
|
@functools.cached_property
|
||||||
def alttp_player_ids(self):
|
def alttp_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def hk_player_ids(self):
|
def hk_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def factorio_player_ids(self):
|
def factorio_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def minecraft_player_ids(self):
|
def minecraft_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
|
||||||
|
|
||||||
|
|
||||||
def get_name_string_for_object(self, obj) -> str:
|
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)})'
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||||
@@ -238,53 +237,12 @@ class MultiWorld():
|
|||||||
def get_all_state(self, keys=False) -> CollectionState:
|
def get_all_state(self, keys=False) -> CollectionState:
|
||||||
ret = CollectionState(self)
|
ret = CollectionState(self)
|
||||||
|
|
||||||
def soft_collect(item):
|
|
||||||
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
|
|
||||||
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[
|
|
||||||
item.player].progressive_sword_limit >= 4:
|
|
||||||
ret.prog_items['Golden Sword', item.player] += 1
|
|
||||||
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[
|
|
||||||
item.player].progressive_sword_limit >= 3:
|
|
||||||
ret.prog_items['Tempered Sword', item.player] += 1
|
|
||||||
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
|
||||||
ret.prog_items['Master Sword', item.player] += 1
|
|
||||||
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
|
||||||
ret.prog_items['Fighter Sword', item.player] += 1
|
|
||||||
elif 'Glove' in item.name:
|
|
||||||
if ret.has('Titans Mitts', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Power Glove', item.player):
|
|
||||||
ret.prog_items['Titans Mitts', item.player] += 1
|
|
||||||
else:
|
|
||||||
ret.prog_items['Power Glove', item.player] += 1
|
|
||||||
elif 'Shield' in item.name:
|
|
||||||
if ret.has('Mirror Shield', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
|
||||||
ret.prog_items['Mirror Shield', item.player] += 1
|
|
||||||
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
|
||||||
ret.prog_items['Red Shield', item.player] += 1
|
|
||||||
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
|
||||||
ret.prog_items['Blue Shield', item.player] += 1
|
|
||||||
elif 'Bow' in item.name:
|
|
||||||
if ret.has('Silver', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
|
||||||
ret.prog_items['Silver Bow', item.player] += 1
|
|
||||||
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
|
||||||
ret.prog_items['Bow', item.player] += 1
|
|
||||||
elif item.advancement or item.smallkey or item.bigkey:
|
|
||||||
ret.prog_items[item.name, item.player] += 1
|
|
||||||
|
|
||||||
for item in self.itempool:
|
for item in self.itempool:
|
||||||
soft_collect(item)
|
self.worlds[item.player].collect(ret, item)
|
||||||
|
|
||||||
if keys:
|
if keys:
|
||||||
for p in self.alttp_player_ids:
|
for p in self.alttp_player_ids:
|
||||||
|
world = self.worlds[p]
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
for item in ItemFactory(
|
for item in ItemFactory(
|
||||||
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
||||||
@@ -299,7 +257,7 @@ class MultiWorld():
|
|||||||
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
||||||
'Small Key (Ganons Tower)'] * 4,
|
'Small Key (Ganons Tower)'] * 4,
|
||||||
p):
|
p):
|
||||||
soft_collect(item)
|
world.collect(ret, item)
|
||||||
ret.sweep_for_events()
|
ret.sweep_for_events()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -891,72 +849,24 @@ class CollectionState(object):
|
|||||||
return self.fortress_loot(player) and normal_kill
|
return self.fortress_loot(player) and normal_kill
|
||||||
|
|
||||||
def can_kill_ender_dragon(self, player: int):
|
def can_kill_ender_dragon(self, player: int):
|
||||||
|
# Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned.
|
||||||
|
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Ingot Crafting', player)
|
||||||
if self.combat_difficulty(player) == 'easy':
|
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 \
|
return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
|
||||||
self.can_brew_potions(player) and self.can_enchant(player)
|
self.has('Archery', player) and self.can_brew_potions(player) and self.can_enchant(player)
|
||||||
if self.combat_difficulty(player) == 'hard':
|
if self.combat_difficulty(player) == 'hard':
|
||||||
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
return respawn_dragon and ((self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
||||||
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
|
(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)
|
return respawn_dragon and 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:
|
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
||||||
if location:
|
if location:
|
||||||
self.locations_checked.add(location)
|
self.locations_checked.add(location)
|
||||||
changed = False
|
|
||||||
|
|
||||||
# TODO: create a mapping for progressive items in each game and use that
|
changed = self.world.worlds[item.player].collect(self, item)
|
||||||
if item.game == "A Link to the Past":
|
|
||||||
if item.name.startswith('Progressive '):
|
|
||||||
if 'Sword' in item.name:
|
|
||||||
if self.has('Golden Sword', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
|
||||||
item.player].progressive_sword_limit >= 4:
|
|
||||||
self.prog_items['Golden Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
|
|
||||||
self.prog_items['Tempered Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
|
||||||
self.prog_items['Master Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
|
||||||
self.prog_items['Fighter Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif 'Glove' in item.name:
|
|
||||||
if self.has('Titans Mitts', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Power Glove', item.player):
|
|
||||||
self.prog_items['Titans Mitts', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
self.prog_items['Power Glove', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif 'Shield' in item.name:
|
|
||||||
if self.has('Mirror Shield', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
|
||||||
self.prog_items['Mirror Shield', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
|
||||||
self.prog_items['Red Shield', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
|
||||||
self.prog_items['Blue Shield', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif 'Bow' in item.name:
|
|
||||||
if self.has('Silver Bow', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Bow', item.player):
|
|
||||||
self.prog_items['Silver Bow', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
self.prog_items['Bow', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
|
if not changed and event:
|
||||||
if not changed and (event or item.advancement):
|
|
||||||
self.prog_items[item.name, item.player] += 1
|
self.prog_items[item.name, item.player] += 1
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
@@ -1426,8 +1336,6 @@ class Spoiler(object):
|
|||||||
'shuffle': self.world.shuffle,
|
'shuffle': self.world.shuffle,
|
||||||
'item_pool': self.world.difficulty,
|
'item_pool': self.world.difficulty,
|
||||||
'item_functionality': self.world.item_functionality,
|
'item_functionality': self.world.item_functionality,
|
||||||
'gt_crystals': self.world.crystals_needed_for_gt,
|
|
||||||
'ganon_crystals': self.world.crystals_needed_for_ganon,
|
|
||||||
'open_pyramid': self.world.open_pyramid,
|
'open_pyramid': self.world.open_pyramid,
|
||||||
'accessibility': self.world.accessibility,
|
'accessibility': self.world.accessibility,
|
||||||
'hints': self.world.hints,
|
'hints': self.world.hints,
|
||||||
@@ -1451,7 +1359,6 @@ class Spoiler(object):
|
|||||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||||
'shop_shuffle': self.world.shop_shuffle,
|
'shop_shuffle': self.world.shop_shuffle,
|
||||||
'shop_item_slots': self.world.shop_item_slots,
|
|
||||||
'shuffle_prizes': self.world.shuffle_prizes,
|
'shuffle_prizes': self.world.shuffle_prizes,
|
||||||
'sprite_pool': self.world.sprite_pool,
|
'sprite_pool': self.world.sprite_pool,
|
||||||
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
||||||
@@ -1501,22 +1408,13 @@ class Spoiler(object):
|
|||||||
outfile.write('Progression Balanced: %s\n' % (
|
outfile.write('Progression Balanced: %s\n' % (
|
||||||
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
||||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||||
if player in self.world.hk_player_ids:
|
options = self.world.worlds[player].options
|
||||||
for hk_option in Options.hollow_knight_options:
|
if options:
|
||||||
res = getattr(self.world, hk_option)[player]
|
for f_option in options:
|
||||||
outfile.write(f'{hk_option+":":33}{res}\n')
|
|
||||||
|
|
||||||
elif player in self.world.factorio_player_ids:
|
|
||||||
for f_option in Options.factorio_options:
|
|
||||||
res = getattr(self.world, f_option)[player]
|
res = getattr(self.world, f_option)[player]
|
||||||
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
||||||
|
|
||||||
elif player in self.world.minecraft_player_ids:
|
if player in self.world.alttp_player_ids:
|
||||||
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')
|
|
||||||
|
|
||||||
elif player in self.world.alttp_player_ids:
|
|
||||||
for team in range(self.world.teams):
|
for team in range(self.world.teams):
|
||||||
outfile.write('%s%s\n' % (
|
outfile.write('%s%s\n' % (
|
||||||
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
|
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
|
||||||
@@ -1544,8 +1442,6 @@ class Spoiler(object):
|
|||||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||||
if self.metadata['shuffle'][player] != "vanilla":
|
if self.metadata['shuffle'][player] != "vanilla":
|
||||||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||||
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
|
|
||||||
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
|
|
||||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||||
|
|
||||||
@@ -1568,8 +1464,6 @@ class Spoiler(object):
|
|||||||
"f" in self.metadata["shop_shuffle"][player]))
|
"f" in self.metadata["shop_shuffle"][player]))
|
||||||
outfile.write('Custom Potion Shop: %s\n' %
|
outfile.write('Custom Potion Shop: %s\n' %
|
||||||
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
||||||
outfile.write('Shop Item Slots: %s\n' %
|
|
||||||
self.metadata["shop_item_slots"][player])
|
|
||||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||||
outfile.write(
|
outfile.write(
|
||||||
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
||||||
@@ -1598,6 +1492,13 @@ class Spoiler(object):
|
|||||||
for dungeon, medallion in self.medallions.items():
|
for dungeon, medallion in self.medallions.items():
|
||||||
outfile.write(f'\n{dungeon}: {medallion}')
|
outfile.write(f'\n{dungeon}: {medallion}')
|
||||||
|
|
||||||
|
if self.world.factorio_player_ids:
|
||||||
|
outfile.write('\n\nRecipes:\n')
|
||||||
|
for player in self.world.factorio_player_ids:
|
||||||
|
name = self.world.get_player_names(player)
|
||||||
|
for recipe in self.world.worlds[player].custom_recipes.values():
|
||||||
|
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
|
||||||
|
|
||||||
if self.startinventory:
|
if self.startinventory:
|
||||||
outfile.write('\n\nStarting Inventory:\n\n')
|
outfile.write('\n\nStarting Inventory:\n\n')
|
||||||
outfile.write('\n'.join(self.startinventory))
|
outfile.write('\n'.join(self.startinventory))
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import typing
|
|||||||
import asyncio
|
import asyncio
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import prompt_toolkit
|
|
||||||
import websockets
|
import websockets
|
||||||
from prompt_toolkit.patch_stdout import patch_stdout
|
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
@@ -210,8 +208,6 @@ class CommonContext():
|
|||||||
logger.info(args["text"])
|
logger.info(args["text"])
|
||||||
|
|
||||||
def on_print_json(self, args: dict):
|
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"]))
|
logger.info(self.jsontotextparser(args["data"]))
|
||||||
|
|
||||||
|
|
||||||
@@ -331,9 +327,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
logger.error('Invalid password')
|
logger.error('Invalid password')
|
||||||
ctx.password = None
|
ctx.password = None
|
||||||
await ctx.server_auth(True)
|
await ctx.server_auth(True)
|
||||||
else:
|
elif errors:
|
||||||
raise Exception("Unknown connection errors: " + str(errors))
|
raise Exception("Unknown connection errors: " + str(errors))
|
||||||
raise Exception('Connection refused by the multiworld host, no reason provided')
|
else:
|
||||||
|
raise Exception('Connection refused by the multiworld host, no reason provided')
|
||||||
|
|
||||||
elif cmd == 'Connected':
|
elif cmd == 'Connected':
|
||||||
ctx.team = args["team"]
|
ctx.team = args["team"]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import json
|
|||||||
import string
|
import string
|
||||||
import copy
|
import copy
|
||||||
import sys
|
import sys
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
import subprocess
|
||||||
|
import factorio_rcon
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -21,9 +22,9 @@ from worlds.factorio.Technologies import lookup_id_to_name
|
|||||||
|
|
||||||
rcon_port = 24242
|
rcon_port = 24242
|
||||||
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
|
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
|
||||||
save_name = "Archipelago"
|
|
||||||
|
|
||||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
||||||
|
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||||
options = Utils.get_options()
|
options = Utils.get_options()
|
||||||
executable = options["factorio_options"]["executable"]
|
executable = options["factorio_options"]["executable"]
|
||||||
bin_dir = os.path.dirname(executable)
|
bin_dir = os.path.dirname(executable)
|
||||||
@@ -35,9 +36,7 @@ if not os.path.exists(executable):
|
|||||||
else:
|
else:
|
||||||
raise FileNotFoundError(executable)
|
raise FileNotFoundError(executable)
|
||||||
|
|
||||||
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
||||||
|
|
||||||
thread_pool = ThreadPoolExecutor(10)
|
|
||||||
|
|
||||||
|
|
||||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||||
@@ -56,7 +55,10 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_connect(self, address: str = "") -> bool:
|
def _cmd_connect(self, address: str = "") -> bool:
|
||||||
"""Connect to a MultiWorld Server"""
|
"""Connect to a MultiWorld Server"""
|
||||||
if not self.ctx.auth:
|
if not self.ctx.auth:
|
||||||
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
if self.ctx.rcon_client:
|
||||||
|
get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code
|
||||||
|
else:
|
||||||
|
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
||||||
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,54 +101,49 @@ class FactorioContext(CommonContext):
|
|||||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||||
f"{cleaned_text}\")")
|
f"{cleaned_text}\")")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def savegame_name(self) -> str:
|
||||||
|
return f"AP_{self.seed_name}_{self.auth}.zip"
|
||||||
|
|
||||||
async def game_watcher(ctx: FactorioContext, bridge_file: str):
|
|
||||||
|
async def game_watcher(ctx: FactorioContext):
|
||||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||||
from worlds.factorio.Technologies import lookup_id_to_name
|
from worlds.factorio.Technologies import lookup_id_to_name
|
||||||
bridge_counter = 0
|
|
||||||
try:
|
try:
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
if os.path.exists(bridge_file):
|
if ctx.awaiting_bridge and ctx.rcon_client:
|
||||||
bridge_logger.info("Found Factorio Bridge file.")
|
ctx.awaiting_bridge = False
|
||||||
while not ctx.exit_event.is_set():
|
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
|
||||||
if ctx.awaiting_bridge:
|
if data["slot_name"] != ctx.auth:
|
||||||
ctx.awaiting_bridge = False
|
logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
|
||||||
with open(bridge_file) as f:
|
elif data["seed_name"] != ctx.seed_name:
|
||||||
data = json.load(f)
|
logger.warning(f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
||||||
research_data = data["research_done"]
|
else:
|
||||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
data = data["info"]
|
||||||
victory = data["victory"]
|
research_data = data["research_done"]
|
||||||
ctx.auth = data["slot_name"]
|
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||||
ctx.seed_name = data["seed_name"]
|
victory = data["victory"]
|
||||||
|
|
||||||
if not ctx.finished_game and victory:
|
if not ctx.finished_game and victory:
|
||||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
ctx.finished_game = True
|
ctx.finished_game = True
|
||||||
|
|
||||||
|
if ctx.locations_checked != research_data:
|
||||||
|
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)
|
||||||
|
|
||||||
if ctx.locations_checked != research_data:
|
|
||||||
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:
|
|
||||||
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:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
|
|
||||||
|
|
||||||
def stream_factorio_output(pipe, queue):
|
def stream_factorio_output(pipe, queue, process):
|
||||||
def queuer():
|
def queuer():
|
||||||
while 1:
|
while process.poll() is None:
|
||||||
text = pipe.readline().strip()
|
text = pipe.readline().strip()
|
||||||
if text:
|
if text:
|
||||||
queue.put_nowait(text)
|
queue.put_nowait(text)
|
||||||
@@ -155,25 +152,32 @@ def stream_factorio_output(pipe, queue):
|
|||||||
|
|
||||||
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
|
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
async def factorio_server_watcher(ctx: FactorioContext):
|
async def factorio_server_watcher(ctx: FactorioContext):
|
||||||
import subprocess
|
savegame_name = os.path.abspath(ctx.savegame_name)
|
||||||
import factorio_rcon
|
if not os.path.exists(savegame_name):
|
||||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
logger.info(f"Creating savegame {savegame_name}")
|
||||||
factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)),
|
subprocess.run((
|
||||||
|
executable, "--create", savegame_name
|
||||||
|
))
|
||||||
|
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
|
||||||
|
*(str(elem) for elem in server_args)),
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
encoding="utf-8")
|
encoding="utf-8")
|
||||||
factorio_server_logger.info("Started Factorio Server")
|
factorio_server_logger.info("Started Factorio Server")
|
||||||
factorio_queue = Queue()
|
factorio_queue = Queue()
|
||||||
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
||||||
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
||||||
script_folder = None
|
|
||||||
progression_watcher = None
|
|
||||||
try:
|
try:
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
|
if factorio_process.poll():
|
||||||
|
factorio_server_logger.info("Factorio server has exited.")
|
||||||
|
ctx.exit_event.set()
|
||||||
|
|
||||||
while not factorio_queue.empty():
|
while not factorio_queue.empty():
|
||||||
msg = factorio_queue.get()
|
msg = factorio_queue.get()
|
||||||
factorio_server_logger.info(msg)
|
factorio_server_logger.info(msg)
|
||||||
@@ -182,16 +186,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
# trigger lua interface confirmation
|
# 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("/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 ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||||
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}")
|
|
||||||
progression_watcher = asyncio.create_task(
|
|
||||||
game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
|
||||||
if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
|
|
||||||
ctx.awaiting_bridge = True
|
ctx.awaiting_bridge = True
|
||||||
if ctx.rcon_client:
|
if ctx.rcon_client:
|
||||||
while ctx.send_index < len(ctx.items_received):
|
while ctx.send_index < len(ctx.items_received):
|
||||||
@@ -203,47 +198,104 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
else:
|
else:
|
||||||
item_name = lookup_id_to_name[item_id]
|
item_name = lookup_id_to_name[item_id]
|
||||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
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.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
|
||||||
ctx.send_index += 1
|
ctx.send_index += 1
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
|
ctx.rcon_client = None
|
||||||
|
ctx.exit_event.set()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
factorio_process.terminate()
|
factorio_process.terminate()
|
||||||
if progression_watcher:
|
|
||||||
await progression_watcher
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
def get_info(ctx, rcon_client):
|
||||||
|
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
|
||||||
|
ctx.auth = info["slot_name"]
|
||||||
|
ctx.seed_name = info["seed_name"]
|
||||||
|
|
||||||
|
|
||||||
|
async def factorio_spinup_server(ctx: FactorioContext):
|
||||||
|
savegame_name = os.path.abspath("Archipelago.zip")
|
||||||
|
if not os.path.exists(savegame_name):
|
||||||
|
logger.info(f"Creating savegame {savegame_name}")
|
||||||
|
subprocess.run((
|
||||||
|
executable, "--create", savegame_name, "--preset", "archipelago"
|
||||||
|
))
|
||||||
|
factorio_process = subprocess.Popen(
|
||||||
|
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
encoding="utf-8")
|
||||||
|
factorio_server_logger.info("Started Information Exchange Factorio Server")
|
||||||
|
factorio_queue = Queue()
|
||||||
|
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
||||||
|
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
||||||
|
rcon_client = None
|
||||||
|
try:
|
||||||
|
while not ctx.auth:
|
||||||
|
while not factorio_queue.empty():
|
||||||
|
msg = factorio_queue.get()
|
||||||
|
factorio_server_logger.info(msg)
|
||||||
|
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||||
|
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||||
|
get_info(ctx, rcon_client)
|
||||||
|
|
||||||
|
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
|
ctx.exit_event.set()
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info(f"Got World Information from AP Mod for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
factorio_process.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
async def main(ui=None):
|
||||||
ctx = FactorioContext(None, None, True)
|
ctx = FactorioContext(None, None, True)
|
||||||
# testing shortcuts
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
# ctx.server_address = "localhost"
|
if ui:
|
||||||
# ctx.auth = "Nauvis"
|
input_task = None
|
||||||
if ctx.server_task is None:
|
ui_app = ui(ctx)
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
||||||
await asyncio.sleep(3)
|
else:
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
|
ui_task = None
|
||||||
|
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
|
||||||
|
await factorio_server_task
|
||||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||||
|
progression_watcher = asyncio.create_task(
|
||||||
|
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
ctx.server_address = None
|
ctx.server_address = None
|
||||||
ctx.snes_reconnect_address = None
|
|
||||||
|
|
||||||
await asyncio.gather(input_task, factorio_server_task)
|
await progression_watcher
|
||||||
|
await factorio_server_task
|
||||||
|
|
||||||
if ctx.server is not None and not ctx.server.socket.closed:
|
if ctx.server and not ctx.server.socket.closed:
|
||||||
await ctx.server.socket.close()
|
await ctx.server.socket.close()
|
||||||
if ctx.server_task is not None:
|
if ctx.server_task is not None:
|
||||||
await ctx.server_task
|
await ctx.server_task
|
||||||
await factorio_server_task
|
|
||||||
|
|
||||||
while ctx.input_requests > 0:
|
while ctx.input_requests > 0:
|
||||||
ctx.input_queue.put_nowait(None)
|
ctx.input_queue.put_nowait(None)
|
||||||
ctx.input_requests -= 1
|
ctx.input_requests -= 1
|
||||||
|
|
||||||
await input_task
|
if ui_task:
|
||||||
|
await ui_task
|
||||||
|
|
||||||
|
if input_task:
|
||||||
|
input_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
class FactorioJSONtoTextParser(JSONtoTextParser):
|
class FactorioJSONtoTextParser(JSONtoTextParser):
|
||||||
|
|||||||
@@ -13,35 +13,8 @@ os.environ["KIVY_NO_FILELOG"] = "1"
|
|||||||
os.environ["KIVY_NO_ARGS"] = "1"
|
os.environ["KIVY_NO_ARGS"] = "1"
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from CommonClient import server_loop, logger
|
from CommonClient import logger
|
||||||
from FactorioClient import FactorioContext, factorio_server_watcher
|
from FactorioClient import main
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
ctx = FactorioContext(None, None, True)
|
|
||||||
|
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
|
||||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
|
||||||
ui_app = FactorioManager(ctx)
|
|
||||||
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
|
||||||
|
|
||||||
await ctx.exit_event.wait() # wait for signal to exit application
|
|
||||||
ui_app.stop()
|
|
||||||
ctx.server_address = None
|
|
||||||
ctx.snes_reconnect_address = None
|
|
||||||
# allow tasks to quit
|
|
||||||
await ui_task
|
|
||||||
await factorio_server_task
|
|
||||||
await ctx.server_task
|
|
||||||
|
|
||||||
if ctx.server is not None and not ctx.server.socket.closed:
|
|
||||||
await ctx.server.socket.close()
|
|
||||||
if ctx.server_task is not None:
|
|
||||||
await ctx.server_task
|
|
||||||
|
|
||||||
while ctx.input_requests > 0: # clear queue for shutdown
|
|
||||||
ctx.input_queue.put_nowait(None)
|
|
||||||
ctx.input_requests -= 1
|
|
||||||
|
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
@@ -59,7 +32,7 @@ class FactorioManager(App):
|
|||||||
super(FactorioManager, self).__init__()
|
super(FactorioManager, self).__init__()
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.commandprocessor = ctx.command_processor(ctx)
|
self.commandprocessor = ctx.command_processor(ctx)
|
||||||
self.icon = "data/icon.png"
|
self.icon = r"data/icon.png"
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
self.grid = GridLayout()
|
self.grid = GridLayout()
|
||||||
@@ -70,7 +43,7 @@ class FactorioManager(App):
|
|||||||
pairs = [
|
pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
("FactorioServer", "Factorio Server Log"),
|
("FactorioServer", "Factorio Server Log"),
|
||||||
("FactorioWatcher", "Bridge File Log"),
|
("FactorioWatcher", "Bridge Data Log"),
|
||||||
]
|
]
|
||||||
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
|
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
|
||||||
for logger_name, display_name in pairs:
|
for logger_name, display_name in pairs:
|
||||||
@@ -163,5 +136,6 @@ Builder.load_string('''
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(main())
|
ui_app = FactorioManager
|
||||||
|
loop.run_until_complete(main(ui_app))
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class Context(CommonContext):
|
|||||||
self.snes_reconnect_address = None
|
self.snes_reconnect_address = None
|
||||||
self.snes_recv_queue = asyncio.Queue()
|
self.snes_recv_queue = asyncio.Queue()
|
||||||
self.snes_request_lock = asyncio.Lock()
|
self.snes_request_lock = asyncio.Lock()
|
||||||
self.is_sd2snes = False
|
|
||||||
self.snes_write_buffer = []
|
self.snes_write_buffer = []
|
||||||
|
|
||||||
self.awaiting_rom = False
|
self.awaiting_rom = False
|
||||||
@@ -408,26 +407,30 @@ class SNESState(enum.IntEnum):
|
|||||||
SNES_ATTACHED = 3
|
SNES_ATTACHED = 3
|
||||||
|
|
||||||
|
|
||||||
def launch_qusb2snes(ctx: Context):
|
def launch_sni(ctx: Context):
|
||||||
qusb2snes_path = Utils.get_options()["lttp_options"]["qusb2snes"]
|
sni_path = Utils.get_options()["lttp_options"]["sni"]
|
||||||
|
|
||||||
if not os.path.isfile(qusb2snes_path):
|
if not os.path.isdir(sni_path):
|
||||||
qusb2snes_path = Utils.local_path(qusb2snes_path)
|
sni_path = Utils.local_path(sni_path)
|
||||||
|
if os.path.isdir(sni_path):
|
||||||
|
for file in os.listdir(sni_path):
|
||||||
|
if file.startswith("sni.") and not file.endswith(".proto"):
|
||||||
|
sni_path = os.path.join(sni_path, file)
|
||||||
|
|
||||||
if os.path.isfile(qusb2snes_path):
|
if os.path.isfile(sni_path):
|
||||||
logger.info(f"Attempting to start {qusb2snes_path}")
|
logger.info(f"Attempting to start {sni_path}")
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path))
|
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, "
|
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
|
||||||
f"please start it yourself if it is not running")
|
f"please start it yourself if it is not running")
|
||||||
|
|
||||||
|
|
||||||
async def _snes_connect(ctx: Context, address: str):
|
async def _snes_connect(ctx: Context, address: str):
|
||||||
address = f"ws://{address}" if "://" not in address else address
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
|
|
||||||
logger.info("Connecting to QUsb2snes at %s ..." % address)
|
logger.info("Connecting to SNI at %s ..." % address)
|
||||||
seen_problems = set()
|
seen_problems = set()
|
||||||
succesful = False
|
succesful = False
|
||||||
while not succesful:
|
while not succesful:
|
||||||
@@ -439,11 +442,11 @@ async def _snes_connect(ctx: Context, address: str):
|
|||||||
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
||||||
if problem not in seen_problems:
|
if problem not in seen_problems:
|
||||||
seen_problems.add(problem)
|
seen_problems.add(problem)
|
||||||
logger.error(f"Error connecting to QUsb2snes ({problem})")
|
logger.error(f"Error connecting to SNI ({problem})")
|
||||||
|
|
||||||
if len(seen_problems) == 1:
|
if len(seen_problems) == 1:
|
||||||
# this is the first problem. Let's try launching QUsb2snes if it isn't already running
|
# this is the first problem. Let's try launching SNI if it isn't already running
|
||||||
launch_qusb2snes(ctx)
|
launch_sni(ctx)
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
@@ -462,7 +465,7 @@ async def get_snes_devices(ctx: Context):
|
|||||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
logger.info('No SNES device found. Ensure QUsb2Snes is running and connect it to the multibridge.')
|
logger.info('No SNES device found. Please connect a SNES device to SNI.')
|
||||||
while not devices:
|
while not devices:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await socket.send(dumps(DeviceList_Request))
|
await socket.send(dumps(DeviceList_Request))
|
||||||
@@ -510,17 +513,6 @@ async def snes_connect(ctx: Context, address):
|
|||||||
await ctx.snes_socket.send(dumps(Attach_Request))
|
await ctx.snes_socket.send(dumps(Attach_Request))
|
||||||
ctx.snes_state = SNESState.SNES_ATTACHED
|
ctx.snes_state = SNESState.SNES_ATTACHED
|
||||||
ctx.snes_attached_device = (devices.index(device), device)
|
ctx.snes_attached_device = (devices.index(device), device)
|
||||||
|
|
||||||
if 'sd2snes' in device.lower() or 'COM' in device:
|
|
||||||
logger.info("SD2SNES/FXPAK Detected")
|
|
||||||
ctx.is_sd2snes = True
|
|
||||||
await ctx.snes_socket.send(dumps({"Opcode": "Info", "Space": "SNES"}))
|
|
||||||
reply = loads(await ctx.snes_socket.recv())
|
|
||||||
if reply and 'Results' in reply:
|
|
||||||
logger.info(reply['Results'])
|
|
||||||
else:
|
|
||||||
ctx.is_sd2snes = False
|
|
||||||
|
|
||||||
ctx.snes_reconnect_address = address
|
ctx.snes_reconnect_address = address
|
||||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||||
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
|
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
|
||||||
@@ -614,8 +606,7 @@ async def snes_read(ctx: Context, address, size):
|
|||||||
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||||
if len(data):
|
if len(data):
|
||||||
logger.error(str(data))
|
logger.error(str(data))
|
||||||
logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
|
logger.warning('Communication Failure with SNI')
|
||||||
'Try un-selecting and re-selecting the SNES Device.')
|
|
||||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
return None
|
return None
|
||||||
@@ -634,45 +625,16 @@ async def snes_write(ctx: Context, write_list):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||||
|
try:
|
||||||
if ctx.is_sd2snes:
|
|
||||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
|
||||||
|
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||||
logger.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
|
||||||
return False
|
|
||||||
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
|
||||||
cmd += b'\xA9' # LDA
|
|
||||||
cmd += bytes([byte])
|
|
||||||
cmd += b'\x8F' # STA.l
|
|
||||||
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
|
|
||||||
|
|
||||||
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
|
|
||||||
|
|
||||||
PutAddress_Request['Space'] = 'CMD'
|
|
||||||
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd) - 1)[2:], "2C00", "1"]
|
|
||||||
try:
|
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
await ctx.snes_socket.send(cmd)
|
await ctx.snes_socket.send(data)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Could not send data to SNES: {cmd}")
|
logger.warning(f"Could not send data to SNES: {data}")
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
PutAddress_Request['Space'] = 'SNES'
|
|
||||||
try:
|
|
||||||
# will pack those requests as soon as qusb2snes actually supports that for real
|
|
||||||
for address, data in write_list:
|
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
|
||||||
if ctx.snes_socket is not None:
|
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
|
||||||
await ctx.snes_socket.send(data)
|
|
||||||
else:
|
|
||||||
logger.warning(f"Could not send data to SNES: {data}")
|
|
||||||
except websockets.ConnectionClosed:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
@@ -852,10 +814,11 @@ async def game_watcher(ctx: Context):
|
|||||||
|
|
||||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
|
recv_index += 1
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received)))
|
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
|
||||||
recv_index += 1
|
|
||||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
||||||
@@ -886,7 +849,7 @@ async def main():
|
|||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
help='Path to a Archipelago Binary Patch file')
|
help='Path to a Archipelago Binary Patch file')
|
||||||
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
|
||||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||||
@@ -909,14 +872,12 @@ async def main():
|
|||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||||
|
|
||||||
port = None
|
|
||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password, args.founditems)
|
ctx = Context(args.snes, args.connect, args.password, args.founditems)
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
|
|
||||||
if ctx.server_task is None:
|
if ctx.server_task is None:
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
asyncio.create_task(snes_connect(ctx, ctx.snes_address))
|
||||||
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
|
|||||||
36
Main.py
36
Main.py
@@ -21,10 +21,6 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p
|
|||||||
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||||
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
||||||
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
|
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
|
||||||
from worlds.hk import gen_hollow
|
|
||||||
from worlds.hk import create_regions as hk_create_regions
|
|
||||||
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.generic.Rules import locality_rules
|
||||||
from worlds import Games, lookup_any_item_name_to_id, AutoWorld
|
from worlds import Games, lookup_any_item_name_to_id, AutoWorld
|
||||||
import Patch
|
import Patch
|
||||||
@@ -196,14 +192,8 @@ def main(args, seed=None):
|
|||||||
world.non_local_items[player] -= item_name_groups['Pendants']
|
world.non_local_items[player] -= item_name_groups['Pendants']
|
||||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||||
|
|
||||||
for player in world.hk_player_ids:
|
|
||||||
hk_create_regions(world, player)
|
|
||||||
|
|
||||||
AutoWorld.call_all(world, "create_regions")
|
AutoWorld.call_all(world, "create_regions")
|
||||||
|
|
||||||
for player in world.minecraft_player_ids:
|
|
||||||
minecraft_create_regions(world, player)
|
|
||||||
|
|
||||||
for player in world.alttp_player_ids:
|
for player in world.alttp_player_ids:
|
||||||
if world.open_pyramid[player] == 'goal':
|
if world.open_pyramid[player] == 'goal':
|
||||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
|
||||||
@@ -263,14 +253,8 @@ def main(args, seed=None):
|
|||||||
for player in world.alttp_player_ids:
|
for player in world.alttp_player_ids:
|
||||||
set_rules(world, player)
|
set_rules(world, player)
|
||||||
|
|
||||||
for player in world.hk_player_ids:
|
|
||||||
gen_hollow(world, player)
|
|
||||||
|
|
||||||
AutoWorld.call_all(world, "generate_basic")
|
AutoWorld.call_all(world, "generate_basic")
|
||||||
|
|
||||||
for player in world.minecraft_player_ids:
|
|
||||||
gen_minecraft(world, player)
|
|
||||||
|
|
||||||
logger.info("Running Item Plando")
|
logger.info("Running Item Plando")
|
||||||
|
|
||||||
for item in world.itempool:
|
for item in world.itempool:
|
||||||
@@ -497,10 +481,7 @@ def main(args, seed=None):
|
|||||||
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||||
games = {}
|
games = {}
|
||||||
for slot in world.player_ids:
|
for slot in world.player_ids:
|
||||||
if world.game[slot] == "Factorio":
|
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||||
client_versions[slot] = (0, 1, 2)
|
|
||||||
else:
|
|
||||||
client_versions[slot] = (0, 0, 3)
|
|
||||||
games[slot] = world.game[slot]
|
games[slot] = world.game[slot]
|
||||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
||||||
slot, team, rom_name in rom_names}
|
slot, team, rom_name in rom_names}
|
||||||
@@ -519,14 +500,10 @@ def main(args, seed=None):
|
|||||||
if player not in world.alttp_player_ids:
|
if player not in world.alttp_player_ids:
|
||||||
connect_names[name] = (i, player)
|
connect_names[name] = (i, player)
|
||||||
if world.hk_player_ids:
|
if world.hk_player_ids:
|
||||||
import Options
|
|
||||||
for slot in world.hk_player_ids:
|
for slot in world.hk_player_ids:
|
||||||
slots_data = slot_data[slot] = {}
|
slot_data[slot] = AutoWorld.call_single(world, "fill_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:
|
for slot in world.minecraft_player_ids:
|
||||||
slot_data[slot] = fill_minecraft_slot_data(world, slot)
|
slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot)
|
||||||
|
|
||||||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||||
for location in world.get_filled_locations():
|
for location in world.get_filled_locations():
|
||||||
@@ -578,8 +555,6 @@ def main(args, seed=None):
|
|||||||
if multidata_task:
|
if multidata_task:
|
||||||
multidata_task.result() # retrieve exception if one exists
|
multidata_task.result() # retrieve exception if one exists
|
||||||
pool.shutdown() # wait for all queued tasks to complete
|
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)
|
|
||||||
if not args.skip_playthrough:
|
if not args.skip_playthrough:
|
||||||
logger.info('Calculating playthrough.')
|
logger.info('Calculating playthrough.')
|
||||||
create_playthrough(world)
|
create_playthrough(world)
|
||||||
@@ -697,8 +672,9 @@ def create_playthrough(world):
|
|||||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||||
return list(pathpairs)
|
return list(pathpairs)
|
||||||
|
|
||||||
world.spoiler.paths = dict()
|
world.spoiler.paths = {}
|
||||||
for player in range(1, world.players + 1):
|
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
|
||||||
|
for player in topology_worlds:
|
||||||
world.spoiler.paths.update(
|
world.spoiler.paths.update(
|
||||||
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
||||||
sphere if location.player == player})
|
sphere if location.player == player})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import concurrent.futures
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
|
|
||||||
def feedback(text: str):
|
def feedback(text: str):
|
||||||
@@ -76,8 +77,10 @@ if __name__ == "__main__":
|
|||||||
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
|
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
|
||||||
elif os.path.exists("ArchipelagoMystery"):
|
elif os.path.exists("ArchipelagoMystery"):
|
||||||
basemysterycommand = "./ArchipelagoMystery" # compiled linux
|
basemysterycommand = "./ArchipelagoMystery" # compiled linux
|
||||||
|
elif which('py'):
|
||||||
|
basemysterycommand = f"py -{py_version} Mystery.py" # source windows
|
||||||
else:
|
else:
|
||||||
basemysterycommand = f"py -{py_version} Mystery.py" # source
|
basemysterycommand = f"python3 Mystery.py" # source others
|
||||||
|
|
||||||
weights_file_path = os.path.join(player_files_path, weights_file_path)
|
weights_file_path = os.path.join(player_files_path, weights_file_path)
|
||||||
if os.path.exists(weights_file_path):
|
if os.path.exists(weights_file_path):
|
||||||
@@ -210,8 +213,10 @@ if __name__ == "__main__":
|
|||||||
baseservercommand = ["ArchipelagoServer.exe"] # compiled windows
|
baseservercommand = ["ArchipelagoServer.exe"] # compiled windows
|
||||||
elif os.path.exists("ArchipelagoServer"):
|
elif os.path.exists("ArchipelagoServer"):
|
||||||
baseservercommand = ["./ArchipelagoServer"] # compiled linux
|
baseservercommand = ["./ArchipelagoServer"] # compiled linux
|
||||||
|
elif which('py'):
|
||||||
|
baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source windows
|
||||||
else:
|
else:
|
||||||
baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source
|
baseservercommand = ["python3", "MultiServer.py"] # source others
|
||||||
# don't have a mac to test that. If you try to run compiled on mac, good luck.
|
# don't have a mac to test that. If you try to run compiled on mac, good luck.
|
||||||
subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)])
|
subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)])
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -244,7 +244,15 @@ class Context(Node):
|
|||||||
import atexit
|
import atexit
|
||||||
atexit.register(self._save, True) # make sure we save on exit too
|
atexit.register(self._save, True) # make sure we save on exit too
|
||||||
|
|
||||||
|
def recheck_hints(self):
|
||||||
|
for team, slot in self.hints:
|
||||||
|
self.hints[team, slot] = {
|
||||||
|
hint.re_check(self, team) for hint in
|
||||||
|
self.hints[team, slot]
|
||||||
|
}
|
||||||
|
|
||||||
def get_save(self) -> dict:
|
def get_save(self) -> dict:
|
||||||
|
self.recheck_hints()
|
||||||
d = {
|
d = {
|
||||||
"connect_names": self.connect_names,
|
"connect_names": self.connect_names,
|
||||||
"received_items": self.received_items,
|
"received_items": self.received_items,
|
||||||
@@ -561,7 +569,8 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
|||||||
NetUtils.add_json_text(parts, ")")
|
NetUtils.add_json_text(parts, ")")
|
||||||
|
|
||||||
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
|
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
|
||||||
"receiving": receiving_player, "sending": net_item.player}
|
"receiving": receiving_player,
|
||||||
|
"item": net_item}
|
||||||
|
|
||||||
|
|
||||||
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str] = all_console_names) -> typing.Tuple[
|
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str] = all_console_names) -> typing.Tuple[
|
||||||
@@ -1026,7 +1035,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
if errors:
|
if errors:
|
||||||
logging.info(f"A client connection was refused due to: {errors}")
|
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
||||||
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
||||||
else:
|
else:
|
||||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||||
|
|||||||
62
Mystery.py
62
Mystery.py
@@ -9,6 +9,7 @@ from collections import Counter
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
from worlds.alttp import Options as LttPOptions
|
||||||
from worlds.generic import PlandoItem, PlandoConnection
|
from worlds.generic import PlandoItem, PlandoConnection
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
@@ -546,49 +547,38 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
ret.startinventory = startitems
|
ret.startinventory = startitems
|
||||||
ret.start_hints = set(game_weights.get('start_hints', []))
|
ret.start_hints = set(game_weights.get('start_hints', []))
|
||||||
|
|
||||||
if ret.game == "A Link to the Past":
|
if ret.game in AutoWorldRegister.world_types:
|
||||||
roll_alttp_settings(ret, game_weights, plando_options)
|
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
|
||||||
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, game_weights, True)))
|
|
||||||
elif ret.game == "Factorio":
|
|
||||||
for option_name, option in Options.factorio_options.items():
|
|
||||||
if option_name in game_weights:
|
if option_name in game_weights:
|
||||||
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
|
try:
|
||||||
setattr(ret, option_name, option.from_any(game_weights[option_name]))
|
if issubclass(option, Options.OptionDict):
|
||||||
else:
|
setattr(ret, option_name, option.from_any(game_weights[option_name]))
|
||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
|
else:
|
||||||
|
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Error generating option {option_name} in {ret.game}")
|
||||||
else:
|
else:
|
||||||
setattr(ret, option_name, option(option.default))
|
setattr(ret, option_name, option(option.default))
|
||||||
elif ret.game == "Minecraft":
|
if ret.game == "Minecraft":
|
||||||
for option_name, option in Options.minecraft_options.items():
|
# bad hardcoded behavior to make this work for now
|
||||||
if option_name in game_weights:
|
ret.plando_connections = []
|
||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
|
if "connections" in plando_options:
|
||||||
else:
|
options = game_weights.get("plando_connections", [])
|
||||||
setattr(ret, option_name, option(option.default))
|
for placement in options:
|
||||||
# bad hardcoded behavior to make this work for now
|
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||||
ret.plando_connections = []
|
ret.plando_connections.append(PlandoConnection(
|
||||||
if "connections" in plando_options:
|
get_choice("entrance", placement),
|
||||||
options = game_weights.get("plando_connections", [])
|
get_choice("exit", placement),
|
||||||
for placement in options:
|
get_choice("direction", placement, "both")
|
||||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
))
|
||||||
ret.plando_connections.append(PlandoConnection(
|
elif ret.game == "A Link to the Past":
|
||||||
get_choice("entrance", placement),
|
roll_alttp_settings(ret, game_weights, plando_options)
|
||||||
get_choice("exit", placement),
|
|
||||||
get_choice("direction", placement, "both")
|
|
||||||
))
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unsupported game {ret.game}")
|
raise Exception(f"Unsupported game {ret.game}")
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||||
for option_name, option in Options.alttp_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))
|
|
||||||
|
|
||||||
glitches_required = get_choice('glitches_required', weights)
|
glitches_required = get_choice('glitches_required', weights)
|
||||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
||||||
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
||||||
@@ -639,7 +629,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
|
|
||||||
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
|
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
|
||||||
|
|
||||||
ret.triforce_pieces_required = Options.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
||||||
|
|
||||||
# sum a percentage to required
|
# sum a percentage to required
|
||||||
if extra_pieces == 'percentage':
|
if extra_pieces == 'percentage':
|
||||||
@@ -647,7 +637,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||||
# vanilla mode (specify how many pieces are)
|
# vanilla mode (specify how many pieces are)
|
||||||
elif extra_pieces == 'available':
|
elif extra_pieces == 'available':
|
||||||
ret.triforce_pieces_available = Options.TriforcePieces.from_any(
|
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
|
||||||
get_choice('triforce_pieces_available', weights, 30))
|
get_choice('triforce_pieces_available', weights, 30))
|
||||||
# required pieces + fixed extra
|
# required pieces + fixed extra
|
||||||
elif extra_pieces == 'extra':
|
elif extra_pieces == 'extra':
|
||||||
|
|||||||
@@ -307,7 +307,9 @@ class Hint(typing.NamedTuple):
|
|||||||
else:
|
else:
|
||||||
add_json_text(parts, ".")
|
add_json_text(parts, ".")
|
||||||
|
|
||||||
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
|
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
||||||
|
"receiving": self.receiving_player,
|
||||||
|
"item": NetworkItem(self.item, self.location, self.finding_player)}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local(self):
|
def local(self):
|
||||||
|
|||||||
264
Options.py
264
Options.py
@@ -21,38 +21,6 @@ class AssembleOptions(type):
|
|||||||
name.startswith("alias_")})
|
name.startswith("alias_")})
|
||||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
class AssembleCategoryPath(type):
|
|
||||||
def __new__(mcs, name, bases, attrs):
|
|
||||||
path = []
|
|
||||||
for base in bases:
|
|
||||||
if hasattr(base, "segment"):
|
|
||||||
path += base.segment
|
|
||||||
path += attrs["segment"]
|
|
||||||
attrs["path"] = path
|
|
||||||
return super(AssembleCategoryPath, mcs).__new__(mcs, name, bases, attrs)
|
|
||||||
|
|
||||||
|
|
||||||
class RootCategory(metaclass=AssembleCategoryPath):
|
|
||||||
segment = []
|
|
||||||
|
|
||||||
|
|
||||||
class LttPCategory(RootCategory):
|
|
||||||
segment = ["A Link to the Past"]
|
|
||||||
|
|
||||||
|
|
||||||
class LttPRomCategory(LttPCategory):
|
|
||||||
segment = ["rom"]
|
|
||||||
|
|
||||||
|
|
||||||
class FactorioCategory(RootCategory):
|
|
||||||
segment = ["Factorio"]
|
|
||||||
|
|
||||||
|
|
||||||
class MinecraftCategory(RootCategory):
|
|
||||||
segment = ["Minecraft"]
|
|
||||||
|
|
||||||
|
|
||||||
class Option(metaclass=AssembleOptions):
|
class Option(metaclass=AssembleOptions):
|
||||||
value: int
|
value: int
|
||||||
name_lookup: typing.Dict[int, str]
|
name_lookup: typing.Dict[int, str]
|
||||||
@@ -175,6 +143,9 @@ class Range(Option, int):
|
|||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
|
def get_option_name(self):
|
||||||
|
return str(self.value)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
@@ -213,243 +184,18 @@ class OptionDict(Option):
|
|||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
|
|
||||||
class Logic(Choice):
|
|
||||||
option_no_glitches = 0
|
|
||||||
option_minor_glitches = 1
|
|
||||||
option_overworld_glitches = 2
|
|
||||||
option_hybrid_major_glitches = 3
|
|
||||||
option_no_logic = 4
|
|
||||||
alias_owg = 2
|
|
||||||
alias_hmg = 3
|
|
||||||
|
|
||||||
|
|
||||||
class Objective(Choice):
|
|
||||||
option_crystals = 0
|
|
||||||
# option_pendants = 1
|
|
||||||
option_triforce_pieces = 2
|
|
||||||
option_pedestal = 3
|
|
||||||
option_bingo = 4
|
|
||||||
|
|
||||||
|
|
||||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice):
|
|
||||||
option_kill_ganon = 0
|
|
||||||
option_kill_ganon_and_gt_agahnim = 1
|
|
||||||
option_hand_in = 2
|
|
||||||
|
|
||||||
|
|
||||||
class Accessibility(Choice):
|
class Accessibility(Choice):
|
||||||
option_locations = 0
|
option_locations = 0
|
||||||
option_items = 1
|
option_items = 1
|
||||||
option_beatable = 2
|
option_beatable = 2
|
||||||
|
|
||||||
|
|
||||||
class Crystals(Range):
|
|
||||||
range_start = 0
|
|
||||||
range_end = 7
|
|
||||||
|
|
||||||
|
|
||||||
class CrystalsTower(Crystals):
|
|
||||||
default = 7
|
|
||||||
|
|
||||||
|
|
||||||
class CrystalsGanon(Crystals):
|
|
||||||
default = 7
|
|
||||||
|
|
||||||
|
|
||||||
class TriforcePieces(Range):
|
|
||||||
default = 30
|
|
||||||
range_start = 1
|
|
||||||
range_end = 90
|
|
||||||
|
|
||||||
|
|
||||||
class ShopItemSlots(Range):
|
|
||||||
range_start = 0
|
|
||||||
range_end = 30
|
|
||||||
|
|
||||||
|
|
||||||
class WorldState(Choice):
|
|
||||||
option_standard = 1
|
|
||||||
option_open = 0
|
|
||||||
option_inverted = 2
|
|
||||||
|
|
||||||
|
|
||||||
class Bosses(Choice):
|
|
||||||
option_vanilla = 0
|
|
||||||
option_simple = 1
|
|
||||||
option_full = 2
|
|
||||||
option_chaos = 3
|
|
||||||
option_singularity = 4
|
|
||||||
|
|
||||||
|
|
||||||
class Enemies(Choice):
|
|
||||||
option_vanilla = 0
|
|
||||||
option_shuffled = 1
|
|
||||||
option_chaos = 2
|
|
||||||
|
|
||||||
|
|
||||||
alttp_options: typing.Dict[str, type(Option)] = {
|
|
||||||
"crystals_needed_for_gt": CrystalsTower,
|
|
||||||
"crystals_needed_for_ganon": CrystalsGanon,
|
|
||||||
"shop_item_slots": ShopItemSlots,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {
|
|
||||||
"RandomizeDreamers": DefaultOnToggle,
|
|
||||||
"RandomizeSkills": DefaultOnToggle,
|
|
||||||
"RandomizeCharms": DefaultOnToggle,
|
|
||||||
"RandomizeKeys": DefaultOnToggle,
|
|
||||||
"RandomizeGeoChests": Toggle,
|
|
||||||
"RandomizeMaskShards": DefaultOnToggle,
|
|
||||||
"RandomizeVesselFragments": DefaultOnToggle,
|
|
||||||
"RandomizeCharmNotches": Toggle,
|
|
||||||
"RandomizePaleOre": DefaultOnToggle,
|
|
||||||
"RandomizeRancidEggs": Toggle,
|
|
||||||
"RandomizeRelics": DefaultOnToggle,
|
|
||||||
"RandomizeMaps": Toggle,
|
|
||||||
"RandomizeStags": Toggle,
|
|
||||||
"RandomizeGrubs": Toggle,
|
|
||||||
"RandomizeWhisperingRoots": Toggle,
|
|
||||||
"RandomizeRocks": Toggle,
|
|
||||||
"RandomizeSoulTotems": Toggle,
|
|
||||||
"RandomizePalaceTotems": Toggle,
|
|
||||||
"RandomizeLoreTablets": Toggle,
|
|
||||||
"RandomizeLifebloodCocoons": Toggle,
|
|
||||||
"RandomizeFlames": Toggle
|
|
||||||
}
|
|
||||||
|
|
||||||
hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
|
|
||||||
"MILDSKIPS": Toggle,
|
|
||||||
"SPICYSKIPS": Toggle,
|
|
||||||
"FIREBALLSKIPS": Toggle,
|
|
||||||
"ACIDSKIPS": Toggle,
|
|
||||||
"SPIKETUNNELS": Toggle,
|
|
||||||
"DARKROOMS": Toggle,
|
|
||||||
"CURSED": Toggle,
|
|
||||||
"SHADESKIPS": Toggle,
|
|
||||||
}
|
|
||||||
|
|
||||||
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} - \
|
|
||||||
{"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense
|
|
||||||
|
|
||||||
|
|
||||||
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_large_diamonds = 3
|
|
||||||
option_small_pyramids = 4
|
|
||||||
option_medium_pyramids = 5
|
|
||||||
option_large_pyramids = 6
|
|
||||||
option_small_funnels = 7
|
|
||||||
option_medium_funnels = 8
|
|
||||||
option_large_funnels = 9
|
|
||||||
option_funnels = 4
|
|
||||||
alias_pyramid = 6
|
|
||||||
alias_funnel = 9
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
class TechTreeInformation(Choice):
|
|
||||||
option_none = 0
|
|
||||||
option_advancement = 1
|
|
||||||
option_full = 2
|
|
||||||
default = 2
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeTime(Choice):
|
|
||||||
option_vanilla = 0
|
|
||||||
option_fast = 1
|
|
||||||
option_normal = 2
|
|
||||||
option_slow = 4
|
|
||||||
option_chaos = 5
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
"tech_tree_information": TechTreeInformation,
|
|
||||||
"starting_items": FactorioStartItems,
|
|
||||||
"recipe_time": RecipeTime,
|
|
||||||
"imported_blueprints": DefaultOnToggle,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
option_sets = (
|
|
||||||
minecraft_options,
|
|
||||||
factorio_options,
|
|
||||||
alttp_options,
|
|
||||||
hollow_knight_options
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from worlds.alttp.Options import Logic
|
||||||
import argparse
|
import argparse
|
||||||
mapshuffle = Toggle
|
mapshuffle = Toggle
|
||||||
compassshuffle = Toggle
|
compassshuffle = Toggle
|
||||||
|
|||||||
162
Utils.py
162
Utils.py
@@ -12,7 +12,8 @@ class Version(typing.NamedTuple):
|
|||||||
minor: int
|
minor: int
|
||||||
build: int
|
build: int
|
||||||
|
|
||||||
__version__ = "0.1.3"
|
|
||||||
|
__version__ = "0.1.4"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
@@ -22,6 +23,7 @@ import sys
|
|||||||
import pickle
|
import pickle
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
|
import collections
|
||||||
|
|
||||||
from yaml import load, dump, safe_load
|
from yaml import load, dump, safe_load
|
||||||
|
|
||||||
@@ -52,7 +54,6 @@ def snes_to_pc(value):
|
|||||||
def parse_player_names(names, players, teams):
|
def parse_player_names(names, players, teams):
|
||||||
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
||||||
if len(names) != len(set(names)):
|
if len(names) != len(set(names)):
|
||||||
import collections
|
|
||||||
name_counter = collections.Counter(names)
|
name_counter = collections.Counter(names)
|
||||||
raise ValueError(f"Duplicate Player names is not supported, "
|
raise ValueError(f"Duplicate Player names is not supported, "
|
||||||
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
||||||
@@ -68,6 +69,21 @@ def parse_player_names(names, players, teams):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def cache_argsless(function):
|
||||||
|
if function.__code__.co_argcount:
|
||||||
|
raise Exception("Can only cache 0 argument functions with this cache.")
|
||||||
|
|
||||||
|
result = sentinel = object()
|
||||||
|
|
||||||
|
def _wrap():
|
||||||
|
nonlocal result
|
||||||
|
if result is sentinel:
|
||||||
|
result = function()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return _wrap
|
||||||
|
|
||||||
|
|
||||||
def is_bundled() -> bool:
|
def is_bundled() -> bool:
|
||||||
return getattr(sys, 'frozen', False)
|
return getattr(sys, 'frozen', False)
|
||||||
|
|
||||||
@@ -118,20 +134,10 @@ def open_file(filename):
|
|||||||
subprocess.call([open_command, filename])
|
subprocess.call([open_command, filename])
|
||||||
|
|
||||||
|
|
||||||
def close_console():
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
# windows
|
|
||||||
import ctypes.wintypes
|
|
||||||
try:
|
|
||||||
ctypes.windll.kernel32.FreeConsole()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
parse_yaml = safe_load
|
parse_yaml = safe_load
|
||||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_public_ipv4() -> str:
|
def get_public_ipv4() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -147,7 +153,7 @@ def get_public_ipv4() -> str:
|
|||||||
pass # we could be offline, in a local game, so no point in erroring out
|
pass # we could be offline, in a local game, so no point in erroring out
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_public_ipv6() -> str:
|
def get_public_ipv6() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -160,70 +166,68 @@ def get_public_ipv6() -> str:
|
|||||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_default_options() -> dict:
|
def get_default_options() -> dict:
|
||||||
if not hasattr(get_default_options, "options"):
|
# Refer to host.yaml for comments as to what all these options mean.
|
||||||
# Refer to host.yaml for comments as to what all these options mean.
|
options = {
|
||||||
options = {
|
"general_options": {
|
||||||
"general_options": {
|
"output_path": "output",
|
||||||
"output_path": "output",
|
},
|
||||||
},
|
"factorio_options": {
|
||||||
"factorio_options": {
|
"executable": "factorio\\bin\\x64\\factorio",
|
||||||
"executable": "factorio\\bin\\x64\\factorio",
|
},
|
||||||
},
|
"lttp_options": {
|
||||||
"lttp_options": {
|
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
"sni": "SNI",
|
||||||
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
|
"rom_start": True,
|
||||||
"rom_start": True,
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"server_options": {
|
"server_options": {
|
||||||
"host": None,
|
"host": None,
|
||||||
"port": 38281,
|
"port": 38281,
|
||||||
"password": None,
|
"password": None,
|
||||||
"multidata": None,
|
"multidata": None,
|
||||||
"savefile": None,
|
"savefile": None,
|
||||||
"disable_save": False,
|
"disable_save": False,
|
||||||
"loglevel": "info",
|
"loglevel": "info",
|
||||||
"server_password": None,
|
"server_password": None,
|
||||||
"disable_item_cheat": False,
|
"disable_item_cheat": False,
|
||||||
"location_check_points": 1,
|
"location_check_points": 1,
|
||||||
"hint_cost": 10,
|
"hint_cost": 10,
|
||||||
"forfeit_mode": "goal",
|
"forfeit_mode": "goal",
|
||||||
"remaining_mode": "goal",
|
"remaining_mode": "goal",
|
||||||
"auto_shutdown": 0,
|
"auto_shutdown": 0,
|
||||||
"compatibility": 2,
|
"compatibility": 2,
|
||||||
"log_network": 0
|
"log_network": 0
|
||||||
},
|
},
|
||||||
"multi_mystery_options": {
|
"multi_mystery_options": {
|
||||||
"teams": 1,
|
"teams": 1,
|
||||||
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||||
"player_files_path": "Players",
|
"player_files_path": "Players",
|
||||||
"players": 0,
|
"players": 0,
|
||||||
"weights_file_path": "weights.yaml",
|
"weights_file_path": "weights.yaml",
|
||||||
"meta_file_path": "meta.yaml",
|
"meta_file_path": "meta.yaml",
|
||||||
"pre_roll": False,
|
"pre_roll": False,
|
||||||
"create_spoiler": 1,
|
"create_spoiler": 1,
|
||||||
"zip_roms": 0,
|
"zip_roms": 0,
|
||||||
"zip_diffs": 2,
|
"zip_diffs": 2,
|
||||||
"zip_apmcs": 1,
|
"zip_apmcs": 1,
|
||||||
"zip_spoiler": 0,
|
"zip_spoiler": 0,
|
||||||
"zip_multidata": 1,
|
"zip_multidata": 1,
|
||||||
"zip_format": 1,
|
"zip_format": 1,
|
||||||
"glitch_triforce_room": 1,
|
"glitch_triforce_room": 1,
|
||||||
"race": 0,
|
"race": 0,
|
||||||
"cpu_threads": 0,
|
"cpu_threads": 0,
|
||||||
"max_attempts": 0,
|
"max_attempts": 0,
|
||||||
"take_first_working": False,
|
"take_first_working": False,
|
||||||
"keep_all_seeds": False,
|
"keep_all_seeds": False,
|
||||||
"log_output_path": "Output Logs",
|
"log_output_path": "Output Logs",
|
||||||
"log_level": None,
|
"log_level": None,
|
||||||
"plando_options": "bosses",
|
"plando_options": "bosses",
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get_default_options.options = options
|
return options
|
||||||
return get_default_options.options
|
|
||||||
|
|
||||||
|
|
||||||
blacklisted_options = {"multi_mystery_options.cpu_threads",
|
blacklisted_options = {"multi_mystery_options.cpu_threads",
|
||||||
@@ -253,7 +257,7 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
|||||||
dest[key] = update_options(value, dest[key], filename, new_keys)
|
dest[key] = update_options(value, dest[key], filename, new_keys)
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_options() -> dict:
|
def get_options() -> dict:
|
||||||
if not hasattr(get_options, "options"):
|
if not hasattr(get_options, "options"):
|
||||||
locations = ("options.yaml", "host.yaml",
|
locations = ("options.yaml", "host.yaml",
|
||||||
@@ -367,7 +371,7 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
|||||||
return romfile, adjusted
|
return romfile, adjusted
|
||||||
return romfile, False
|
return romfile, False
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_unique_identifier():
|
def get_unique_identifier():
|
||||||
uuid = persistent_load().get("client", {}).get("uuid", None)
|
uuid = persistent_load().get("client", {}).get("uuid", None)
|
||||||
if uuid:
|
if uuid:
|
||||||
@@ -405,3 +409,9 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
def restricted_loads(s):
|
def restricted_loads(s):
|
||||||
"""Helper function analogous to pickle.loads()."""
|
"""Helper function analogous to pickle.loads()."""
|
||||||
return RestrictedUnpickler(io.BytesIO(s)).load()
|
return RestrictedUnpickler(io.BytesIO(s)).load()
|
||||||
|
|
||||||
|
|
||||||
|
class KeyedDefaultDict(collections.defaultdict):
|
||||||
|
def __missing__(self, key):
|
||||||
|
self[key] = value = self.default_factory(key)
|
||||||
|
return value
|
||||||
@@ -6,7 +6,6 @@ import socket
|
|||||||
import jinja2.exceptions
|
import jinja2.exceptions
|
||||||
from pony.flask import Pony
|
from pony.flask import Pony
|
||||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
from flask import Blueprint
|
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import concurrent.futures
|
|||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
from pony.orm import db_session, select, commit
|
from pony.orm import db_session, select, commit
|
||||||
|
|
||||||
@@ -15,10 +16,13 @@ from Utils import restricted_loads
|
|||||||
|
|
||||||
class CommonLocker():
|
class CommonLocker():
|
||||||
"""Uses a file lock to signal that something is already running"""
|
"""Uses a file lock to signal that something is already running"""
|
||||||
|
lock_folder = "file_locks"
|
||||||
def __init__(self, lockname: str):
|
def __init__(self, lockname: str, folder=None):
|
||||||
|
if folder:
|
||||||
|
self.lock_folder = folder
|
||||||
|
os.makedirs(self.lock_folder, exist_ok=True)
|
||||||
self.lockname = lockname
|
self.lockname = lockname
|
||||||
self.lockfile = f"./{self.lockname}.lck"
|
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRunningException(Exception):
|
class AlreadyRunningException(Exception):
|
||||||
@@ -26,9 +30,6 @@ class AlreadyRunningException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
class Locker(CommonLocker):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class WebHostContext(Context):
|
|||||||
def get_random_port():
|
def get_random_port():
|
||||||
return random.randint(49152, 65535)
|
return random.randint(49152, 65535)
|
||||||
|
|
||||||
|
|
||||||
def run_server_process(room_id, ponyconfig: dict):
|
def run_server_process(room_id, ponyconfig: dict):
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
@@ -144,7 +145,9 @@ def run_server_process(room_id, ponyconfig: dict):
|
|||||||
await ctx.shutdown_task
|
await ctx.shutdown_task
|
||||||
logging.info("Shutting down")
|
logging.info("Shutting down")
|
||||||
|
|
||||||
asyncio.run(main())
|
from .autolauncher import Locker
|
||||||
|
with Locker(room_id):
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib import LOGS_FOLDER
|
from WebHostLib import LOGS_FOLDER
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ flask>=2.0.1
|
|||||||
pony>=0.7.14
|
pony>=0.7.14
|
||||||
waitress>=2.0.0
|
waitress>=2.0.0
|
||||||
flask-caching>=1.10.1
|
flask-caching>=1.10.1
|
||||||
Flask-Compress>=1.9.0
|
Flask-Compress>=1.10.1
|
||||||
Flask-Limiter>=1.4
|
Flask-Limiter>=1.4
|
||||||
|
|||||||
@@ -17,6 +17,47 @@ window.addEventListener('load', () => {
|
|||||||
paging: false,
|
paging: false,
|
||||||
info: false,
|
info: false,
|
||||||
dom: "t",
|
dom: "t",
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: 'hours',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
if (type === "sort" || type === 'type') {
|
||||||
|
if (data === "None")
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return parseInt(data);
|
||||||
|
}
|
||||||
|
if (data === "None")
|
||||||
|
return data;
|
||||||
|
|
||||||
|
let hours = Math.floor(data / 3600);
|
||||||
|
let minutes = Math.floor((data - (hours * 3600)) / 60);
|
||||||
|
|
||||||
|
if (minutes < 10) {minutes = "0"+minutes;}
|
||||||
|
return hours+':'+minutes;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 'number',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
if (type === "sort" || type === 'type') {
|
||||||
|
return parseFloat(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 'fraction',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
let splitted = data.split("/", 1);
|
||||||
|
let current = splitted[0]
|
||||||
|
if (type === "sort" || type === 'type') {
|
||||||
|
return parseInt(current);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from
|
// DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from
|
||||||
// the tbody and render two separate tables.
|
// the tbody and render two separate tables.
|
||||||
|
|||||||
@@ -33,19 +33,17 @@ use-system-read-write-data-directories=false
|
|||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
1. Make a fresh world. Using any Factorio, create a savegame with options of your choice and save that world as Archipelago.
|
1. Install the generated Factorio AP Mod (would be in <Factorio Directory>/Mods after step 2 of Setup)
|
||||||
|
|
||||||
2. Take that savegame and put it into your Archipelago folder
|
2. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
||||||
|
|
||||||
3. Install the generated Factorio AP Mod
|
* It should start up, create a world and become ready for Factorio connections.
|
||||||
|
|
||||||
4. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
3. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
|
||||||
|
|
||||||
* It should say it loaded the Archipelago mod and found a bridge file. If not, the most likely case is that the mod is not correctly installed or activated.
|
|
||||||
|
|
||||||
5. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
|
|
||||||
|
|
||||||
* / commands are run on your local client, ! commands are requests for the AP server
|
* / commands are run on your local client, ! commands are requests for the AP server
|
||||||
|
|
||||||
* Players should be able to connect to your Factorio Server and begin playing.
|
* Players should be able to connect to your Factorio Server and begin playing.
|
||||||
|
|
||||||
|
4. You can join yourself by connecting to address `localhost`, other people will need to connect to your IP
|
||||||
|
and you may need to port forward for the Factorio Server for those connections.
|
||||||
@@ -102,7 +102,6 @@ html{
|
|||||||
width: 260px;
|
width: 260px;
|
||||||
height: calc(130px - 35px);
|
height: calc(130px - 35px);
|
||||||
padding-top: 35px;
|
padding-top: 35px;
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-clouds{
|
#landing-clouds{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%) contrast(75%) brightness(75%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table img.acquired{
|
#inventory-table img.acquired{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<a href="/games" id="mid-button">start<br />playing</a>
|
<a href="/games" id="mid-button">start<br />playing</a>
|
||||||
<a id="far-left-button"></a>
|
<a id="far-left-button"></a>
|
||||||
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
||||||
<a id="far-right-button"></a>
|
<a href="/uploads" id="far-right-button">Host Game</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="landing-clouds">
|
<div id="landing-clouds">
|
||||||
|
|||||||
@@ -98,20 +98,20 @@
|
|||||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
<th rowspan="2" class="center-column">Last<br>Activity</th>
|
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for area in ordered_areas %}
|
{% for area in ordered_areas %}
|
||||||
<th class="center-column lower-row">
|
<th class="center-column lower-row fraction">
|
||||||
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
||||||
</th>
|
</th>
|
||||||
{% if area in key_locations %}
|
{% if area in key_locations %}
|
||||||
<th class="center-column lower-row">
|
<th class="center-column lower-row number">
|
||||||
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
|
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if area in big_key_locations %}
|
{% if area in big_key_locations %}
|
||||||
<th class="center-column lower-row">
|
<th class="center-column lower-row number">
|
||||||
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
|
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
|
||||||
</th>
|
</th>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- if activity_timers[(team, player)] -%}
|
{%- if activity_timers[(team, player)] -%}
|
||||||
<td class="center-column">{{ activity_timers[(team, player)] | render_timedelta }}</td>
|
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<td class="center-column">None</td>
|
<td class="center-column">None</td>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
@@ -21,3 +21,15 @@ function get_any_stack_size(name)
|
|||||||
-- failsafe
|
-- failsafe
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- from https://stackoverflow.com/a/40180465
|
||||||
|
-- split("a,b,c", ",") => {"a", "b", "c"}
|
||||||
|
function split(s, sep)
|
||||||
|
local fields = {}
|
||||||
|
|
||||||
|
sep = sep or " "
|
||||||
|
local pattern = string.format("([^%s]+)", sep)
|
||||||
|
string.gsub(s, pattern, function(c) fields[#fields + 1] = c end)
|
||||||
|
|
||||||
|
return fields
|
||||||
|
end
|
||||||
@@ -6,6 +6,7 @@ require "util"
|
|||||||
FREE_SAMPLES = {{ free_samples }}
|
FREE_SAMPLES = {{ free_samples }}
|
||||||
SLOT_NAME = "{{ slot_name }}"
|
SLOT_NAME = "{{ slot_name }}"
|
||||||
SEED_NAME = "{{ seed_name }}"
|
SEED_NAME = "{{ seed_name }}"
|
||||||
|
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
|
||||||
|
|
||||||
{% if not imported_blueprints -%}
|
{% if not imported_blueprints -%}
|
||||||
function set_permissions()
|
function set_permissions()
|
||||||
@@ -140,69 +141,49 @@ script.on_init(function()
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- for testing
|
|
||||||
-- script.on_event(defines.events.on_tick, function(event)
|
|
||||||
-- if event.tick%3600 == 300 then
|
|
||||||
-- dumpInfo(game.forces["player"])
|
|
||||||
-- end
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
-- hook into researches done
|
-- hook into researches done
|
||||||
script.on_event(defines.events.on_research_finished, function(event)
|
script.on_event(defines.events.on_research_finished, function(event)
|
||||||
local technology = event.research
|
local technology = event.research
|
||||||
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
||||||
dumpInfo(technology.force) --is sendable
|
dumpInfo(technology.force) --is sendable
|
||||||
end
|
else
|
||||||
if FREE_SAMPLES == 0 then
|
if FREE_SAMPLES == 0 then
|
||||||
return -- Nothing else to do
|
return -- Nothing else to do
|
||||||
end
|
end
|
||||||
if not technology.effects then
|
if not technology.effects then
|
||||||
return -- No technology effects, so nothing to do.
|
return -- No technology effects, so nothing to do.
|
||||||
end
|
end
|
||||||
for _, effect in pairs(technology.effects) do
|
for _, effect in pairs(technology.effects) do
|
||||||
if effect.type == "unlock-recipe" then
|
if effect.type == "unlock-recipe" then
|
||||||
local recipe = game.recipe_prototypes[effect.recipe]
|
local recipe = game.recipe_prototypes[effect.recipe]
|
||||||
for _, result in pairs(recipe.products) do
|
for _, result in pairs(recipe.products) do
|
||||||
if result.type == "item" and result.amount then
|
if result.type == "item" and result.amount then
|
||||||
local name = result.name
|
local name = result.name
|
||||||
local count
|
if FREE_SAMPLE_BLACKLIST[name] ~= 1 then
|
||||||
if FREE_SAMPLES == 1 then
|
local count
|
||||||
count = result.amount
|
if FREE_SAMPLES == 1 then
|
||||||
else
|
count = result.amount
|
||||||
count = get_any_stack_size(result.name)
|
else
|
||||||
if FREE_SAMPLES == 2 then
|
count = get_any_stack_size(result.name)
|
||||||
count = math.ceil(count / 2)
|
if FREE_SAMPLES == 2 then
|
||||||
|
count = math.ceil(count / 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
add_samples(technology.force, name, count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
add_samples(technology.force, name, count)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
function dumpInfo(force)
|
function dumpInfo(force)
|
||||||
local research_done = {}
|
log("Archipelago Bridge Data available for game tick ".. game.tick .. ".") -- notifies client
|
||||||
local data_collection = {
|
|
||||||
["research_done"] = research_done,
|
|
||||||
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
|
||||||
["slot_name"] = SLOT_NAME,
|
|
||||||
["seed_name"] = SEED_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)
|
|
||||||
log("Archipelago Bridge File written for game tick ".. game.tick .. ".")
|
|
||||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
|
||||||
-- game.print("Sent progress to Archipelago.")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function chain_lookup(table, ...)
|
function chain_lookup(table, ...)
|
||||||
for _, k in ipairs{...} do
|
for _, k in ipairs{...} do
|
||||||
table = table[k]
|
table = table[k]
|
||||||
@@ -213,33 +194,76 @@ function chain_lookup(table, ...)
|
|||||||
return table
|
return table
|
||||||
end
|
end
|
||||||
|
|
||||||
-- add / commands
|
|
||||||
|
|
||||||
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
-- add / commands
|
||||||
|
commands.add_command("ap-sync", "Used by the Archipelago client to get progress information", function(call)
|
||||||
|
local force
|
||||||
if call.player_index == nil then
|
if call.player_index == nil then
|
||||||
dumpInfo(game.forces.player)
|
force = game.forces.player
|
||||||
else
|
else
|
||||||
dumpInfo(game.players[call.player_index].force)
|
force = game.players[call.player_index].force
|
||||||
end
|
end
|
||||||
game.print("Wrote bridge file.")
|
local research_done = {}
|
||||||
|
local data_collection = {
|
||||||
|
["research_done"] = research_done,
|
||||||
|
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection}))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||||
local force = game.forces["player"]
|
if global.index_sync == nil then
|
||||||
chunks = {}
|
global.index_sync = {}
|
||||||
for substring in call.parameter:gmatch("%S+") do -- split on " "
|
|
||||||
table.insert(chunks, substring)
|
|
||||||
end
|
end
|
||||||
|
local tech
|
||||||
|
local force = game.forces["player"]
|
||||||
|
chunks = split(call.parameter, "\t")
|
||||||
local tech_name = chunks[1]
|
local tech_name = chunks[1]
|
||||||
local source = chunks[2] or "Archipelago"
|
local index = chunks[2]
|
||||||
local tech = force.technologies[tech_name]
|
local source = chunks[3] or "Archipelago"
|
||||||
if tech ~= nil then
|
if progressive_technologies[tech_name] ~= nil then
|
||||||
if tech.researched ~= true then
|
if global.index_sync[index] == nil then -- not yet received prog item
|
||||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
global.index_sync[index] = tech_name
|
||||||
game.play_sound({path="utility/research_completed"})
|
local tech_stack = progressive_technologies[tech_name]
|
||||||
tech.researched = true
|
for _, tech_name in ipairs(tech_stack) do
|
||||||
|
tech = force.technologies[tech_name]
|
||||||
|
if tech.researched ~= true then
|
||||||
|
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||||
|
game.play_sound({path="utility/research_completed"})
|
||||||
|
tech.researched = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif force.technologies[tech_name] ~= nil then
|
||||||
|
tech = force.technologies[tech_name]
|
||||||
|
if tech ~= nil then
|
||||||
|
if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then
|
||||||
|
game.print("Warning: Desync Detected. Duplicate/Missing items may occur.")
|
||||||
|
end
|
||||||
|
global.index_sync[index] = tech
|
||||||
|
if tech.researched ~= true then
|
||||||
|
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||||
|
game.play_sound({path="utility/research_completed"})
|
||||||
|
tech.researched = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
game.print("Unknown Technology " .. tech_name)
|
game.print("Unknown Technology " .. tech_name)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call)
|
||||||
|
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME}))
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- data
|
||||||
|
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||||
require('lib')
|
require('lib')
|
||||||
|
|
||||||
data.raw["recipe"]["rocket-part"].ingredients = {{ dict_to_recipe(rocket_recipe) }}
|
{%- for recipe_name, recipe in custom_recipes.items() %}
|
||||||
|
data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ingredients) }}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
local technologies = data.raw["technology"]
|
local technologies = data.raw["technology"]
|
||||||
local original_tech
|
local original_tech
|
||||||
@@ -50,14 +52,25 @@ function copy_factorio_icon(tech, tech_source)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function adjust_energy(recipe_name, factor)
|
function adjust_energy(recipe_name, factor)
|
||||||
local energy = data.raw.recipe[recipe_name].energy_required
|
local recipe = data.raw.recipe[recipe_name]
|
||||||
if (energy == nil) then
|
local energy = recipe.energy_required
|
||||||
energy = 1
|
if (energy ~= nil) then
|
||||||
|
data.raw.recipe[recipe_name].energy_required = energy * factor
|
||||||
|
end
|
||||||
|
if (recipe.normal ~= nil and recipe.normal.energy_required ~= nil) then
|
||||||
|
energy = recipe.normal.energy_required
|
||||||
|
recipe.normal.energy_required = energy * factor
|
||||||
|
end
|
||||||
|
if (recipe.expensive ~= nil and recipe.expensive.energy_required ~= nil) then
|
||||||
|
energy = recipe.expensive.energy_required
|
||||||
|
recipe.expensive.energy_required = energy * factor
|
||||||
end
|
end
|
||||||
data.raw.recipe[recipe_name].energy_required = energy * factor
|
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
|
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||||
|
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||||
|
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||||
|
|
||||||
|
|
||||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||||
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
|
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||||
@@ -69,9 +82,11 @@ prep_copy(new_tree_copy, original_tech)
|
|||||||
{% if tech_cost_scale != 1 %}
|
{% if tech_cost_scale != 1 %}
|
||||||
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- if item_name in tech_table and tech_tree_information == 2 -%}
|
{%- if (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in base_tech_table -%}
|
||||||
{#- copy Factorio Technology Icon -#}
|
{#- copy Factorio Technology Icon -#}
|
||||||
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
|
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
|
||||||
|
{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%}
|
||||||
|
copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}")
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{#- use default AP icon if no Factorio graphics exist -#}
|
{#- use default AP icon if no Factorio graphics exist -#}
|
||||||
{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
|
{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
|
||||||
@@ -86,7 +101,9 @@ table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
|||||||
data:extend{new_tree_copy}
|
data:extend{new_tree_copy}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if recipe_time_scale %}
|
{% if recipe_time_scale %}
|
||||||
{%- for recipe in recipes %}
|
{%- for recipe_name, recipe in recipes.items() %}
|
||||||
adjust_energy("{{ recipe }}", {{ random.triangular(*recipe_time_scale) }})
|
{%- if recipe.category != "mining" %}
|
||||||
|
adjust_energy("{{ recipe_name }}", {{ random.triangular(*recipe_time_scale) }})
|
||||||
|
{%- endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
2
data/factorio/mod_template/data.lua
Normal file
2
data/factorio/mod_template/data.lua
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{% from "macros.lua" import dict_to_lua %}
|
||||||
|
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen}) }}
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
|
[map-gen-preset-name]
|
||||||
|
archipelago=Archipelago
|
||||||
|
|
||||||
|
[map-gen-preset-description]
|
||||||
|
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
|
||||||
|
|
||||||
[technology-name]
|
[technology-name]
|
||||||
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||||
{%- if tech_tree_information == 2 -%}
|
{%- if tech_tree_information == 2 or original_tech_name in static_nodes %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
||||||
{% else %}
|
{%- else %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable
|
ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
[technology-description]
|
[technology-description]
|
||||||
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||||
{%- if tech_tree_information == 2 %}
|
{%- if tech_tree_information == 2 or original_tech_name in static_nodes %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}.
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}.
|
||||||
{%- elif tech_tree_information == 1 and advancement %}
|
{%- elif tech_tree_information == 1 and advancement %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement.
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}".
|
||||||
{%- else %}
|
{%- else %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}".
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -1,10 +1,25 @@
|
|||||||
{% macro dict_to_lua(dict) -%}
|
{% macro dict_to_lua(dict) -%}
|
||||||
{
|
{
|
||||||
{%- for key, value in dict.items() -%}
|
{%- for key, value in dict.items() -%}
|
||||||
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
|
["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %}
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
}
|
}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
{% macro list_to_lua(list) -%}
|
||||||
|
{
|
||||||
|
{%- for key in list -%}
|
||||||
|
{{ variable_to_lua(key) }}{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor -%}
|
||||||
|
}
|
||||||
|
{%- endmacro %}
|
||||||
|
{%- macro variable_to_lua(value) %}
|
||||||
|
{%- if value is mapping -%}{{ dict_to_lua(value) }}
|
||||||
|
{%- elif value is boolean -%}{{ value | string | lower }}
|
||||||
|
{%- elif value is string -%}"{{ value | safe }}"
|
||||||
|
{%- elif value is iterable -%}{{ list_to_lua(value) }}
|
||||||
|
{%- else -%} {{ value | safe }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro -%}
|
||||||
{% macro dict_to_recipe(dict) -%}
|
{% macro dict_to_recipe(dict) -%}
|
||||||
{
|
{
|
||||||
{%- for key, value in dict.items() -%}
|
{%- for key, value in dict.items() -%}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -109,8 +109,8 @@ multi_mystery_options:
|
|||||||
lttp_options:
|
lttp_options:
|
||||||
# File name of the v1.0 J rom
|
# File name of the v1.0 J rom
|
||||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||||
# Set this to your (Q)Usb2Snes location if you want the MultiClient to attempt an auto start, does nothing if not found
|
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||||
qusb2snes: "QUsb2Snes\\QUsb2Snes.exe"
|
sni: "SNI"
|
||||||
# Set this to false to never autostart a rom (such as after patching)
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
# True for operating system default program
|
# True for operating system default program
|
||||||
# Alternatively, a path to a program to open the .sfc file with
|
# Alternatively, a path to a program to open the .sfc file with
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ Factorio:
|
|||||||
normal: 0 # 50 % to 200% of original time
|
normal: 0 # 50 % to 200% of original time
|
||||||
slow: 0 # 100% to 400% of original time
|
slow: 0 # 100% to 400% of original time
|
||||||
chaos: 0 # 25% to 400% of original time
|
chaos: 0 # 25% to 400% of original time
|
||||||
|
recipe_ingredients:
|
||||||
|
rocket: 1 # only randomize rocket part recipe
|
||||||
|
science_pack: 1 # also randomize science pack ingredients
|
||||||
max_science_pack:
|
max_science_pack:
|
||||||
automation_science_pack: 0
|
automation_science_pack: 0
|
||||||
logistic_science_pack: 0
|
logistic_science_pack: 0
|
||||||
@@ -91,6 +94,9 @@ Factorio:
|
|||||||
single_craft: 0
|
single_craft: 0
|
||||||
half_stack: 0
|
half_stack: 0
|
||||||
stack: 0
|
stack: 0
|
||||||
|
progressive:
|
||||||
|
on: 1
|
||||||
|
off: 0
|
||||||
tech_tree_information:
|
tech_tree_information:
|
||||||
none: 0
|
none: 0
|
||||||
advancement: 0 # show which items are a logical advancement
|
advancement: 0 # show which items are a logical advancement
|
||||||
@@ -102,10 +108,7 @@ Factorio:
|
|||||||
burner-mining-drill: 19
|
burner-mining-drill: 19
|
||||||
stone-furnace: 19
|
stone-furnace: 19
|
||||||
Minecraft:
|
Minecraft:
|
||||||
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
|
advancement_goal: 50 # 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.
|
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
|
||||||
easy: 0
|
easy: 0
|
||||||
normal: 1
|
normal: 1
|
||||||
@@ -119,7 +122,7 @@ Minecraft:
|
|||||||
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.
|
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
|
on: 0
|
||||||
off: 1
|
off: 1
|
||||||
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
shuffle_structures: # Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||||
on: 0
|
on: 0
|
||||||
off: 1
|
off: 1
|
||||||
A Link to the Past:
|
A Link to the Past:
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ colorama>=0.4.4
|
|||||||
websockets>=9.1
|
websockets>=9.1
|
||||||
PyYAML>=5.4.1
|
PyYAML>=5.4.1
|
||||||
fuzzywuzzy>=0.18.0
|
fuzzywuzzy>=0.18.0
|
||||||
prompt_toolkit>=3.0.18
|
prompt_toolkit>=3.0.19
|
||||||
appdirs>=1.4.4
|
appdirs>=1.4.4
|
||||||
jinja2>=3.0.1
|
jinja2>=3.0.1
|
||||||
28
setup.py
28
setup.py
@@ -88,7 +88,7 @@ cx_Freeze.setup(
|
|||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas"],
|
"pandas"],
|
||||||
"zip_include_packages": ["*"],
|
"zip_include_packages": ["*"],
|
||||||
"zip_exclude_packages": [],
|
"zip_exclude_packages": ["worlds"],
|
||||||
"include_files": [],
|
"include_files": [],
|
||||||
"include_msvcr": True,
|
"include_msvcr": True,
|
||||||
"replace_paths": [("*", "")],
|
"replace_paths": [("*", "")],
|
||||||
@@ -113,7 +113,7 @@ def installfile(path, keep_content=False):
|
|||||||
print('Warning,', path, 'not found')
|
print('Warning,', path, 'not found')
|
||||||
|
|
||||||
|
|
||||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "QUsb2Snes", "meta.yaml"]
|
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
|
||||||
|
|
||||||
for data in extra_data:
|
for data in extra_data:
|
||||||
installfile(Path(data))
|
installfile(Path(data))
|
||||||
@@ -131,30 +131,12 @@ else:
|
|||||||
file = z3pr.__file__
|
file = z3pr.__file__
|
||||||
installfile(Path(os.path.dirname(file)) / "data", keep_content=True)
|
installfile(Path(os.path.dirname(file)) / "data", keep_content=True)
|
||||||
|
|
||||||
qusb2sneslog = buildfolder / "QUsb2Snes" / "log.txt"
|
|
||||||
if os.path.exists(qusb2sneslog):
|
|
||||||
os.remove(qusb2sneslog)
|
|
||||||
|
|
||||||
qusb2snesconfig = buildfolder / "QUsb2Snes" / "config.ini"
|
|
||||||
# turns on all bridges, disables auto update
|
|
||||||
with open(qusb2snesconfig, "w") as f:
|
|
||||||
f.write("""[General]
|
|
||||||
SendToSet=true
|
|
||||||
checkUpdateCounter=20
|
|
||||||
luabridge=true
|
|
||||||
LuaBridgeRNGSeed=79120361805329566567327599
|
|
||||||
FirstTime=true
|
|
||||||
sd2snessupport=true
|
|
||||||
retroarchdevice=true
|
|
||||||
snesclassic=true""")
|
|
||||||
|
|
||||||
|
|
||||||
if signtool:
|
if signtool:
|
||||||
for exe in exes:
|
for exe in exes:
|
||||||
print(f"Signing {exe.target_name}")
|
print(f"Signing {exe.target_name}")
|
||||||
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
||||||
print(f"Signing QUsb2Snes")
|
print(f"Signing SNI")
|
||||||
os.system(signtool + os.path.join(buildfolder, "Qusb2Snes", "QUsb2Snes.exe"))
|
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
|
||||||
|
|
||||||
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
|
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
|
||||||
for file in os.listdir(alttpr_sprites_folder):
|
for file in os.listdir(alttpr_sprites_folder):
|
||||||
@@ -204,7 +186,7 @@ cx_Freeze.setup(
|
|||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas"],
|
"pandas"],
|
||||||
"zip_include_packages": ["*"],
|
"zip_include_packages": ["*"],
|
||||||
"zip_exclude_packages": ["kivy"],
|
"zip_exclude_packages": ["kivy", "worlds"],
|
||||||
"include_files": [],
|
"include_files": [],
|
||||||
"include_msvcr": True,
|
"include_msvcr": True,
|
||||||
"replace_paths": [("*", "")],
|
"replace_paths": [("*", "")],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState
|
from BaseClasses import MultiWorld, CollectionState
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
@@ -8,14 +9,16 @@ from worlds.alttp.Items import ItemFactory
|
|||||||
from worlds.alttp.Regions import create_regions
|
from worlds.alttp.Regions import create_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from Options import alttp_options
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
class TestDungeon(unittest.TestCase):
|
class TestDungeon(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.starting_regions = [] # Where to start exploring
|
self.starting_regions = [] # Where to start exploring
|
||||||
self.remove_exits = [] # Block dungeon exits
|
self.remove_exits = [] # Block dungeon exits
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from worlds.hk import HKWorld
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.hk.Regions import create_regions
|
from worlds import AutoWorld
|
||||||
from worlds.hk import gen_hollow
|
from worlds.hk.Options import hollow_knight_randomize_options, hollow_knight_skip_options
|
||||||
|
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
|
|
||||||
@@ -9,10 +10,11 @@ class TestVanilla(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
self.world.game[1] = "Hollow Knight"
|
self.world.game[1] = "Hollow Knight"
|
||||||
import Options
|
self.world.worlds[1] = HKWorld(self.world, 1)
|
||||||
for hk_option in Options.hollow_knight_randomize_options:
|
for hk_option in hollow_knight_randomize_options:
|
||||||
setattr(self.world, hk_option, {1: True})
|
setattr(self.world, hk_option, {1: True})
|
||||||
for hk_option, option in Options.hollow_knight_skip_options.items():
|
for hk_option, option in hollow_knight_skip_options.items():
|
||||||
setattr(self.world, hk_option, {1: option.default})
|
setattr(self.world, hk_option, {1: option.default})
|
||||||
create_regions(self.world, 1)
|
AutoWorld.call_single(self.world, "create_regions", 1)
|
||||||
gen_hollow(self.world, 1)
|
AutoWorld.call_single(self.world, "generate_basic", 1)
|
||||||
|
AutoWorld.call_single(self.world, "set_rules", 1)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||||
@@ -8,13 +10,16 @@ from worlds.alttp.Regions import mark_light_world_regions
|
|||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from Options import alttp_options
|
|
||||||
|
from worlds import AutoWorld
|
||||||
|
|
||||||
class TestInverted(TestBase):
|
class TestInverted(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
self.world.mode[1] = "inverted"
|
self.world.mode[1] = "inverted"
|
||||||
create_inverted_regions(self.world, 1)
|
create_inverted_regions(self.world, 1)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||||
@@ -8,13 +10,16 @@ from worlds.alttp.Regions import mark_light_world_regions
|
|||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from Options import alttp_options
|
|
||||||
|
from worlds import AutoWorld
|
||||||
|
|
||||||
class TestInvertedMinor(TestBase):
|
class TestInvertedMinor(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.world.mode[1] = "inverted"
|
self.world.mode[1] = "inverted"
|
||||||
self.world.logic[1] = "minorglitches"
|
self.world.logic[1] = "minorglitches"
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||||
@@ -8,14 +10,17 @@ from worlds.alttp.Regions import mark_light_world_regions
|
|||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from Options import alttp_options
|
|
||||||
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedOWG(TestBase):
|
class TestInvertedOWG(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.world.logic[1] = "owglitches"
|
self.world.logic[1] = "owglitches"
|
||||||
self.world.mode[1] = "inverted"
|
self.world.mode[1] = "inverted"
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import worlds.minecraft.Options
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.minecraft import minecraft_gen_item_pool
|
from worlds import AutoWorld
|
||||||
from worlds.minecraft.Regions import minecraft_create_regions, link_minecraft_structures
|
from worlds.minecraft import MinecraftWorld
|
||||||
from worlds.minecraft.Rules import set_rules
|
|
||||||
from worlds.minecraft.Items import MinecraftItem, item_table
|
from worlds.minecraft.Items import MinecraftItem, item_table
|
||||||
import Options
|
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty
|
||||||
|
from Options import Toggle
|
||||||
|
|
||||||
# Converts the name of an item into an item object
|
# Converts the name of an item into an item object
|
||||||
def MCItemFactory(items, player: int):
|
def MCItemFactory(items, player: int):
|
||||||
@@ -28,16 +29,17 @@ class TestMinecraft(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
self.world.game[1] = "Minecraft"
|
self.world.game[1] = "Minecraft"
|
||||||
|
self.world.worlds[1] = MinecraftWorld(self.world, 1)
|
||||||
exclusion_pools = ['hard', 'insane', 'postgame']
|
exclusion_pools = ['hard', 'insane', 'postgame']
|
||||||
for pool in exclusion_pools:
|
for pool in exclusion_pools:
|
||||||
setattr(self.world, f"include_{pool}_advancements", [False, False])
|
setattr(self.world, f"include_{pool}_advancements", [False, False])
|
||||||
setattr(self.world, "advancement_goal", [0, Options.AdvancementGoal(value=0)])
|
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
|
||||||
setattr(self.world, "shuffle_structures", [False, False])
|
setattr(self.world, "shuffle_structures", {1: Toggle(False)})
|
||||||
setattr(self.world, "combat_difficulty", [0, Options.CombatDifficulty(value=1)])
|
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
|
||||||
minecraft_create_regions(self.world, 1)
|
setattr(self.world, "bee_traps", {1: Toggle(False)})
|
||||||
link_minecraft_structures(self.world, 1)
|
AutoWorld.call_single(self.world, "create_regions", 1)
|
||||||
minecraft_gen_item_pool(self.world, 1)
|
AutoWorld.call_single(self.world, "generate_basic", 1)
|
||||||
set_rules(self.world, 1)
|
AutoWorld.call_single(self.world, "set_rules", 1)
|
||||||
|
|
||||||
def _get_items(self, item_pool, all_except):
|
def _get_items(self, item_pool, all_except):
|
||||||
if all_except and len(all_except) > 0:
|
if all_except and len(all_except) > 0:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_entrances
|
from worlds.alttp.EntranceShuffle import link_entrances
|
||||||
@@ -8,13 +10,16 @@ from worlds.alttp.Regions import create_regions
|
|||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from Options import alttp_options
|
|
||||||
|
from worlds import AutoWorld
|
||||||
|
|
||||||
class TestMinor(TestBase):
|
class TestMinor(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.world.logic[1] = "minorglitches"
|
self.world.logic[1] = "minorglitches"
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
create_regions(self.world, 1)
|
create_regions(self.world, 1)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_entrances
|
from worlds.alttp.EntranceShuffle import link_entrances
|
||||||
@@ -8,14 +10,17 @@ from worlds.alttp.Regions import create_regions
|
|||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from Options import alttp_options
|
|
||||||
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
class TestVanillaOWG(TestBase):
|
class TestVanillaOWG(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
self.world.logic[1] = "owglitches"
|
self.world.logic[1] = "owglitches"
|
||||||
create_regions(self.world, 1)
|
create_regions(self.world, 1)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_entrances
|
from worlds.alttp.EntranceShuffle import link_entrances
|
||||||
@@ -8,13 +10,15 @@ from worlds.alttp.Regions import create_regions
|
|||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
from worlds.alttp.Rules import set_rules
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
from Options import alttp_options
|
from worlds import AutoWorld
|
||||||
|
|
||||||
class TestVanilla(TestBase):
|
class TestVanilla(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
for option_name, option in alttp_options.items():
|
args = Namespace()
|
||||||
setattr(self.world, option_name, {1: option.default})
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
self.world.set_options(args)
|
||||||
self.world.logic[1] = "noglitches"
|
self.world.logic[1] = "noglitches"
|
||||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||||
create_regions(self.world, 1)
|
create_regions(self.world, 1)
|
||||||
|
|||||||
@@ -27,20 +27,37 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
|
|
||||||
world: MultiWorld
|
world: MultiWorld
|
||||||
player: int
|
player: int
|
||||||
|
options: dict = {}
|
||||||
|
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, world: MultiWorld, player: int):
|
||||||
self.world = world
|
self.world = world
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
# overwritable methods that get called by Main.py
|
# overwritable methods that get called by Main.py, sorted by execution order
|
||||||
def generate_basic(self):
|
def create_regions(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create_regions(self):
|
def generate_basic(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def generate_output(self):
|
def generate_output(self):
|
||||||
|
"""This method gets called from a threadpool, do not use world.random here.
|
||||||
|
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_required_client_version(self) -> tuple:
|
||||||
|
return 0, 0, 3
|
||||||
|
|
||||||
|
# end of Main.py calls
|
||||||
|
|
||||||
|
def collect(self, state, item) -> bool:
|
||||||
|
"""Collect an item into state. For speed reasons items that aren't logically useful get skipped."""
|
||||||
|
if item.advancement:
|
||||||
|
state.prog_items[item.name, item.player] += 1
|
||||||
|
return True # indicate that a logical state change has occured
|
||||||
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import enum
|
import enum
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
|
||||||
__all__ = {"lookup_any_item_id_to_name",
|
__all__ = {"lookup_any_item_id_to_name",
|
||||||
"lookup_any_location_id_to_name",
|
"lookup_any_location_id_to_name",
|
||||||
"network_data_package",
|
"network_data_package",
|
||||||
"Games"}
|
"Games"}
|
||||||
|
|
||||||
|
# all of the below should be moved to AutoWorld functionality
|
||||||
from .alttp.Items import lookup_id_to_name as alttp
|
from .alttp.Items import lookup_id_to_name as alttp
|
||||||
from .hk.Items import lookup_id_to_name as hk
|
from .hk.Items import lookup_id_to_name as hk
|
||||||
from .factorio import Technologies
|
from .factorio import Technologies
|
||||||
@@ -29,7 +32,7 @@ assert len(lookup_any_location_name_to_id) == len(lookup_any_location_id_to_name
|
|||||||
|
|
||||||
network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name,
|
network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name,
|
||||||
"lookup_any_item_id_to_name": lookup_any_item_id_to_name,
|
"lookup_any_item_id_to_name": lookup_any_item_id_to_name,
|
||||||
"version": 6}
|
"version": 9}
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
@@ -38,3 +41,11 @@ class Games(str, enum.Enum):
|
|||||||
LTTP = "A Link to the Past"
|
LTTP = "A Link to the Past"
|
||||||
Factorio = "Factorio"
|
Factorio = "Factorio"
|
||||||
Minecraft = "Minecraft"
|
Minecraft = "Minecraft"
|
||||||
|
|
||||||
|
|
||||||
|
# end of TODO block
|
||||||
|
|
||||||
|
# import all submodules to trigger AutoWorldRegister
|
||||||
|
for file in os.scandir(os.path.dirname(__file__)):
|
||||||
|
if file.is_dir():
|
||||||
|
importlib.import_module(f".{file.name}", "worlds")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool
|
|||||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
from worlds.alttp.EntranceShuffle import connect_entrance
|
||||||
from Fill import FillError, fill_restrictive
|
from Fill import FillError, fill_restrictive
|
||||||
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
||||||
from worlds.generic.Rules import forbid_items_for_player
|
|
||||||
|
|
||||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||||
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||||
@@ -349,9 +348,7 @@ def generate_itempool(world, player: int):
|
|||||||
world.escape_assist[player].append('bombs')
|
world.escape_assist[player].append('bombs')
|
||||||
|
|
||||||
for (location, item) in placed_items.items():
|
for (location, item) in placed_items.items():
|
||||||
world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
|
world.get_location(location, player).place_locked_item(ItemFactory(item, player))
|
||||||
world.get_location(location, player).event = True
|
|
||||||
world.get_location(location, player).locked = True
|
|
||||||
|
|
||||||
items = ItemFactory(pool, player)
|
items = ItemFactory(pool, player)
|
||||||
|
|
||||||
|
|||||||
77
worlds/alttp/Options.py
Normal file
77
worlds/alttp/Options.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from Options import Choice, Range, Option
|
||||||
|
|
||||||
|
|
||||||
|
class Logic(Choice):
|
||||||
|
option_no_glitches = 0
|
||||||
|
option_minor_glitches = 1
|
||||||
|
option_overworld_glitches = 2
|
||||||
|
option_hybrid_major_glitches = 3
|
||||||
|
option_no_logic = 4
|
||||||
|
alias_owg = 2
|
||||||
|
alias_hmg = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Objective(Choice):
|
||||||
|
option_crystals = 0
|
||||||
|
# option_pendants = 1
|
||||||
|
option_triforce_pieces = 2
|
||||||
|
option_pedestal = 3
|
||||||
|
option_bingo = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(Choice):
|
||||||
|
option_kill_ganon = 0
|
||||||
|
option_kill_ganon_and_gt_agahnim = 1
|
||||||
|
option_hand_in = 2
|
||||||
|
|
||||||
|
|
||||||
|
class Crystals(Range):
|
||||||
|
range_start = 0
|
||||||
|
range_end = 7
|
||||||
|
|
||||||
|
|
||||||
|
class CrystalsTower(Crystals):
|
||||||
|
default = 7
|
||||||
|
|
||||||
|
|
||||||
|
class CrystalsGanon(Crystals):
|
||||||
|
default = 7
|
||||||
|
|
||||||
|
|
||||||
|
class TriforcePieces(Range):
|
||||||
|
default = 30
|
||||||
|
range_start = 1
|
||||||
|
range_end = 90
|
||||||
|
|
||||||
|
|
||||||
|
class ShopItemSlots(Range):
|
||||||
|
range_start = 0
|
||||||
|
range_end = 30
|
||||||
|
|
||||||
|
|
||||||
|
class WorldState(Choice):
|
||||||
|
option_standard = 1
|
||||||
|
option_open = 0
|
||||||
|
option_inverted = 2
|
||||||
|
|
||||||
|
|
||||||
|
class Bosses(Choice):
|
||||||
|
option_vanilla = 0
|
||||||
|
option_simple = 1
|
||||||
|
option_full = 2
|
||||||
|
option_chaos = 3
|
||||||
|
option_singularity = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Enemies(Choice):
|
||||||
|
option_vanilla = 0
|
||||||
|
option_shuffled = 1
|
||||||
|
option_chaos = 2
|
||||||
|
|
||||||
|
alttp_options: typing.Dict[str, type(Option)] = {
|
||||||
|
"crystals_needed_for_gt": CrystalsTower,
|
||||||
|
"crystals_needed_for_ganon": CrystalsGanon,
|
||||||
|
"shop_item_slots": ShopItemSlots,
|
||||||
|
}
|
||||||
@@ -751,18 +751,11 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028,
|
|||||||
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3,
|
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3,
|
||||||
0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
|
0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
|
||||||
|
|
||||||
def get_nonnative_item_sprite(game: str) -> int:
|
|
||||||
|
def get_nonnative_item_sprite(item: str) -> int:
|
||||||
return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at
|
return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at
|
||||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||||
|
|
||||||
# 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):
|
def patch_rom(world, rom, player, team, enemized):
|
||||||
local_random = world.slot_seeds[player]
|
local_random = world.slot_seeds[player]
|
||||||
@@ -1724,20 +1717,7 @@ def write_custom_shops(rom, world, player):
|
|||||||
if item is None:
|
if item is None:
|
||||||
break
|
break
|
||||||
if not item['item'] in item_table: # item not native to ALTTP
|
if not item['item'] in item_table: # item not native to ALTTP
|
||||||
# This is a terrible way to do this, please fix later
|
item_code = get_nonnative_item_sprite(item['item'])
|
||||||
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:
|
else:
|
||||||
item_code = ItemFactory(item['item'], player).code
|
item_code = ItemFactory(item['item'], player).code
|
||||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ Blind_texts = [
|
|||||||
"Do I like\ndrills? Just\na bit.",
|
"Do I like\ndrills? Just\na bit.",
|
||||||
"I'd shell out\ngood rupees\nfor a conch.",
|
"I'd shell out\ngood rupees\nfor a conch.",
|
||||||
"Current\naffairs are\nshocking!",
|
"Current\naffairs are\nshocking!",
|
||||||
"Agriculture\nis a growing\nfield."
|
"Agriculture\nis a growing\nfield.",
|
||||||
|
"Did you hear\nabout the guy\nwhose whole\nleft side was\ncut off?\nHe's all right\nnow.",
|
||||||
|
"What do you\ncall a bee\nthat lives in\nAmerica?\nA USB.",
|
||||||
|
"Leather is\ngreat for\nsneaking\naround because\nit's made of\nhide.",
|
||||||
]
|
]
|
||||||
Ganon1_texts = [
|
Ganon1_texts = [
|
||||||
"Start your day\nsmiling with a\ndelicious\nwhole grain\nbreakfast\ncreated for\nyour\nincredible\ninsides.",
|
"Start your day\nsmiling with a\ndelicious\nwhole grain\nbreakfast\ncreated for\nyour\nincredible\ninsides.",
|
||||||
|
|||||||
@@ -1,10 +1,70 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from BaseClasses import Location, Item
|
from BaseClasses import Location, Item, CollectionState
|
||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
|
from .Options import alttp_options
|
||||||
|
|
||||||
class ALTTPWorld(World):
|
class ALTTPWorld(World):
|
||||||
game: str = "A Link to the Past"
|
game: str = "A Link to the Past"
|
||||||
|
options = alttp_options
|
||||||
|
topology_present = True
|
||||||
|
|
||||||
|
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||||
|
if item.name.startswith('Progressive '):
|
||||||
|
if 'Sword' in item.name:
|
||||||
|
if state.has('Golden Sword', item.player):
|
||||||
|
pass
|
||||||
|
elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
||||||
|
item.player].progressive_sword_limit >= 4:
|
||||||
|
state.prog_items['Golden Sword', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[
|
||||||
|
item.player].progressive_sword_limit >= 3:
|
||||||
|
state.prog_items['Tempered Sword', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
||||||
|
state.prog_items['Master Sword', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
||||||
|
state.prog_items['Fighter Sword', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif 'Glove' in item.name:
|
||||||
|
if state.has('Titans Mitts', item.player):
|
||||||
|
pass
|
||||||
|
elif state.has('Power Glove', item.player):
|
||||||
|
state.prog_items['Titans Mitts', item.player] += 1
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
state.prog_items['Power Glove', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif 'Shield' in item.name:
|
||||||
|
if state.has('Mirror Shield', item.player):
|
||||||
|
pass
|
||||||
|
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
||||||
|
state.prog_items['Mirror Shield', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||||
|
state.prog_items['Red Shield', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||||
|
state.prog_items['Blue Shield', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif 'Bow' in item.name:
|
||||||
|
if state.has('Silver', item.player):
|
||||||
|
pass
|
||||||
|
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
||||||
|
state.prog_items['Silver Bow', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||||
|
state.prog_items['Bow', item.player] += 1
|
||||||
|
return True
|
||||||
|
elif item.advancement or item.smallkey or item.bigkey:
|
||||||
|
state.prog_items[item.name, item.player] += 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_required_client_version(self) -> tuple:
|
||||||
|
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
|
||||||
|
|
||||||
|
|
||||||
class ALttPLocation(Location):
|
class ALttPLocation(Location):
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import json
|
|||||||
import jinja2
|
import jinja2
|
||||||
import Utils
|
import Utils
|
||||||
import shutil
|
import shutil
|
||||||
import Options
|
from . import Options
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from .Technologies import tech_table, rocket_recipes, recipes
|
from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist, progressive_technology_table, \
|
||||||
|
base_tech_table, tech_to_progressive_lookup, progressive_tech_table
|
||||||
|
|
||||||
template_env: Optional[jinja2.Environment] = None
|
template_env: Optional[jinja2.Environment] = None
|
||||||
|
|
||||||
template: Optional[jinja2.Template] = None
|
data_template: Optional[jinja2.Template] = None
|
||||||
|
data_final_template: Optional[jinja2.Template] = None
|
||||||
locale_template: Optional[jinja2.Template] = None
|
locale_template: Optional[jinja2.Template] = None
|
||||||
control_template: Optional[jinja2.Template] = None
|
control_template: Optional[jinja2.Template] = None
|
||||||
|
|
||||||
@@ -41,57 +43,65 @@ recipe_time_scales = {
|
|||||||
Options.RecipeTime.option_vanilla: None
|
Options.RecipeTime.option_vanilla: None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def generate_mod(world):
|
||||||
def generate_mod(world: MultiWorld, player: int):
|
player = world.player
|
||||||
global template, locale_template, control_template
|
multiworld = world.world
|
||||||
|
global data_final_template, locale_template, control_template, data_template
|
||||||
with template_load_lock:
|
with template_load_lock:
|
||||||
if not template:
|
if not data_final_template:
|
||||||
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
|
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
|
||||||
template_env: Optional[jinja2.Environment] = \
|
template_env: Optional[jinja2.Environment] = \
|
||||||
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
|
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
|
||||||
|
data_template = template_env.get_template("data.lua")
|
||||||
template = template_env.get_template("data-final-fixes.lua")
|
data_final_template = template_env.get_template("data-final-fixes.lua")
|
||||||
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
||||||
control_template = template_env.get_template("control.lua")
|
control_template = template_env.get_template("control.lua")
|
||||||
# get data for templates
|
# get data for templates
|
||||||
player_names = {x: world.player_names[x][0] for x in world.player_ids}
|
player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids}
|
||||||
locations = []
|
locations = []
|
||||||
for location in world.get_filled_locations(player):
|
for location in multiworld.get_filled_locations(player):
|
||||||
if location.address:
|
if location.address:
|
||||||
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
|
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
|
||||||
mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}"
|
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}"
|
||||||
tech_cost_scale = {0: 0.1,
|
tech_cost_scale = {0: 0.1,
|
||||||
1: 0.25,
|
1: 0.25,
|
||||||
2: 0.5,
|
2: 0.5,
|
||||||
3: 1,
|
3: 1,
|
||||||
4: 2,
|
4: 2,
|
||||||
5: 5,
|
5: 5,
|
||||||
6: 10}[world.tech_cost[player].value]
|
6: 10}[multiworld.tech_cost[player].value]
|
||||||
|
|
||||||
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
|
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
|
||||||
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
|
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
|
||||||
"tech_cost_scale": tech_cost_scale, "custom_technologies": world.worlds[player].custom_technologies,
|
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
||||||
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player],
|
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
|
||||||
"rocket_recipe": rocket_recipes[world.max_science_pack[player].value],
|
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
|
||||||
"slot_name": world.player_names[player][0], "seed_name": world.seed_name,
|
"slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name,
|
||||||
"starting_items": world.starting_items[player], "recipes": recipes,
|
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
||||||
"random": world.slot_seeds[player],
|
"random": multiworld.slot_seeds[player], "static_nodes": multiworld.worlds[player].static_nodes,
|
||||||
"recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]}
|
"recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value],
|
||||||
|
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
|
||||||
|
"progressive_technology_table": {tech.name : tech.progressive for tech in
|
||||||
|
progressive_technology_table.values()},
|
||||||
|
"custom_recipes": world.custom_recipes}
|
||||||
|
|
||||||
for factorio_option in Options.factorio_options:
|
for factorio_option in Options.factorio_options:
|
||||||
template_data[factorio_option] = getattr(world, factorio_option)[player].value
|
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
||||||
|
|
||||||
control_code = control_template.render(**template_data)
|
control_code = control_template.render(**template_data)
|
||||||
data_final_fixes_code = template.render(**template_data)
|
data_template_code = data_template.render(**template_data)
|
||||||
|
data_final_fixes_code = data_final_template.render(**template_data)
|
||||||
|
|
||||||
mod_dir = Utils.output_path(mod_name) + "_" + Utils.__version__
|
mod_dir = Utils.output_path(mod_name) + "_" + Utils.__version__
|
||||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||||
os.makedirs(en_locale_dir, exist_ok=True)
|
os.makedirs(en_locale_dir, exist_ok=True)
|
||||||
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
|
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
|
||||||
|
with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
|
||||||
|
f.write(data_template_code)
|
||||||
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
||||||
f.write(data_final_fixes_code)
|
f.write(data_final_fixes_code)
|
||||||
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
|
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
|
||||||
f.write(control_code)
|
f.write(control_code)
|
||||||
locale_content = locale_template.render(**template_data)
|
locale_content = locale_template.render(**template_data)
|
||||||
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
||||||
f.write(locale_content)
|
f.write(locale_content)
|
||||||
|
|||||||
123
worlds/factorio/Options.py
Normal file
123
worlds/factorio/Options.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from Options import Choice, OptionDict, Option, DefaultOnToggle
|
||||||
|
|
||||||
|
|
||||||
|
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} - \
|
||||||
|
{"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ordered_science_packs(cls):
|
||||||
|
return [option.replace("_", "-") for option, value in sorted(cls.options.items(), key=lambda pair: pair[1])]
|
||||||
|
|
||||||
|
def get_max_pack(self):
|
||||||
|
return self.get_ordered_science_packs()[self.value].replace("_", "-")
|
||||||
|
|
||||||
|
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_large_diamonds = 3
|
||||||
|
option_small_pyramids = 4
|
||||||
|
option_medium_pyramids = 5
|
||||||
|
option_large_pyramids = 6
|
||||||
|
option_small_funnels = 7
|
||||||
|
option_medium_funnels = 8
|
||||||
|
option_large_funnels = 9
|
||||||
|
option_funnels = 4
|
||||||
|
alias_pyramid = 6
|
||||||
|
alias_funnel = 9
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TechTreeInformation(Choice):
|
||||||
|
option_none = 0
|
||||||
|
option_advancement = 1
|
||||||
|
option_full = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeTime(Choice):
|
||||||
|
option_vanilla = 0
|
||||||
|
option_fast = 1
|
||||||
|
option_normal = 2
|
||||||
|
option_slow = 4
|
||||||
|
option_chaos = 5
|
||||||
|
|
||||||
|
# TODO: implement random
|
||||||
|
class Progressive(Choice):
|
||||||
|
option_off = 0
|
||||||
|
option_random = 1
|
||||||
|
option_on = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
def want_progressives(self, random):
|
||||||
|
return random.choice([True, False]) if self.value == self.option_random else int(self.value)
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredients(Choice):
|
||||||
|
option_rocket = 0
|
||||||
|
option_science_pack = 1
|
||||||
|
|
||||||
|
|
||||||
|
class FactorioStartItems(OptionDict):
|
||||||
|
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||||
|
|
||||||
|
|
||||||
|
class FactorioWorldGen(OptionDict):
|
||||||
|
default = {"terrain_segmentation": 0.5, "water": 1.5,
|
||||||
|
"autoplace_controls": {"coal": {"frequency": 1, "size": 3, "richness": 6},
|
||||||
|
"copper-ore": {"frequency": 1, "size": 3, "richness": 6},
|
||||||
|
"crude-oil": {"frequency": 1, "size": 3, "richness": 6},
|
||||||
|
"enemy-base": {"frequency": 1, "size": 1, "richness": 1},
|
||||||
|
"iron-ore": {"frequency": 1, "size": 3, "richness": 6},
|
||||||
|
"stone": {"frequency": 1, "size": 3, "richness": 6},
|
||||||
|
"trees": {"frequency": 1, "size": 1, "richness": 1},
|
||||||
|
"uranium-ore": {"frequency": 1, "size": 3, "richness": 6}},
|
||||||
|
"starting_area": 1, "peaceful_mode": False,
|
||||||
|
"cliff_settings": {"name": "cliff", "cliff_elevation_0": 10, "cliff_elevation_interval": 40,
|
||||||
|
"richness": 1}}
|
||||||
|
|
||||||
|
factorio_options: typing.Dict[str, type(Option)] = {
|
||||||
|
"max_science_pack": MaxSciencePack,
|
||||||
|
"tech_tree_layout": TechTreeLayout,
|
||||||
|
"tech_cost": TechCost,
|
||||||
|
"free_samples": FreeSamples,
|
||||||
|
"tech_tree_information": TechTreeInformation,
|
||||||
|
"starting_items": FactorioStartItems,
|
||||||
|
"recipe_time": RecipeTime,
|
||||||
|
"recipe_ingredients": RecipeIngredients,
|
||||||
|
"imported_blueprints": DefaultOnToggle,
|
||||||
|
"world_gen": FactorioWorldGen,
|
||||||
|
"progressive": DefaultOnToggle
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from worlds.factorio.Options import TechTreeLayout
|
||||||
from Options import TechTreeLayout
|
|
||||||
|
|
||||||
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
|
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
|
||||||
TechTreeLayout.option_medium_funnels: 4,
|
TechTreeLayout.option_medium_funnels: 4,
|
||||||
@@ -11,6 +10,7 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6,
|
|||||||
TechTreeLayout.option_medium_funnels: 10,
|
TechTreeLayout.option_medium_funnels: 10,
|
||||||
TechTreeLayout.option_large_funnels: 15}
|
TechTreeLayout.option_large_funnels: 15}
|
||||||
|
|
||||||
|
|
||||||
def get_shapes(factorio_world) -> Dict[str, List[str]]:
|
def get_shapes(factorio_world) -> Dict[str, List[str]]:
|
||||||
world = factorio_world.world
|
world = factorio_world.world
|
||||||
player = factorio_world.player
|
player = factorio_world.player
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# Factorio technologies are imported from a .json document in /data
|
# Factorio technologies are imported from a .json document in /data
|
||||||
from typing import Dict, Set, FrozenSet
|
from typing import Dict, Set, FrozenSet, Tuple
|
||||||
|
from collections import Counter, defaultdict
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import string
|
||||||
|
|
||||||
import Options
|
|
||||||
import Utils
|
import Utils
|
||||||
import logging
|
import logging
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
|
from . import Options
|
||||||
|
|
||||||
factorio_id = 2 ** 17
|
factorio_id = 2 ** 17
|
||||||
source_folder = Utils.local_path("data", "factorio")
|
source_folder = Utils.local_path("data", "factorio")
|
||||||
|
|
||||||
@@ -35,10 +38,11 @@ class FactorioElement():
|
|||||||
|
|
||||||
|
|
||||||
class Technology(FactorioElement): # maybe make subclass of Location?
|
class Technology(FactorioElement): # maybe make subclass of Location?
|
||||||
def __init__(self, name, ingredients, factorio_id):
|
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = ()):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.factorio_id = factorio_id
|
self.factorio_id = factorio_id
|
||||||
self.ingredients = ingredients
|
self.ingredients = ingredients
|
||||||
|
self.progressive = progressive
|
||||||
|
|
||||||
def build_rule(self, player: int):
|
def build_rule(self, player: int):
|
||||||
logging.debug(f"Building rules for {self.name}")
|
logging.debug(f"Building rules for {self.name}")
|
||||||
@@ -74,7 +78,12 @@ class CustomTechnology(Technology):
|
|||||||
|
|
||||||
|
|
||||||
class Recipe(FactorioElement):
|
class Recipe(FactorioElement):
|
||||||
def __init__(self, name, category, ingredients, products):
|
name: str
|
||||||
|
category: str
|
||||||
|
ingredients: Dict[str, int]
|
||||||
|
products: Dict[str, int]
|
||||||
|
|
||||||
|
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int]):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.category = category
|
self.category = category
|
||||||
self.ingredients = ingredients
|
self.ingredients = ingredients
|
||||||
@@ -84,25 +93,47 @@ class Recipe(FactorioElement):
|
|||||||
return f"{self.__class__.__name__}({self.name})"
|
return f"{self.__class__.__name__}({self.name})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crafting_machines(self) -> Set[Machine]:
|
def crafting_machine(self) -> str:
|
||||||
"""crafting machines able to run this recipe"""
|
"""cheapest crafting machine name able to run this recipe"""
|
||||||
return machines_per_category[self.category]
|
return machine_per_category[self.category]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unlocking_technologies(self) -> Set[Technology]:
|
def unlocking_technologies(self) -> Set[Technology]:
|
||||||
"""Unlocked by any of the returned technologies. Empty set indicates a starting recipe."""
|
"""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, ())}
|
return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recursive_unlocking_technologies(self) -> Set[Technology]:
|
||||||
|
base = {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
|
||||||
|
for ingredient in self.ingredients:
|
||||||
|
base |= required_technologies[ingredient]
|
||||||
|
return base
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rel_cost(self) -> float:
|
||||||
|
ingredients = sum(self.ingredients.values())
|
||||||
|
return min(ingredients/amount for product, amount in self.products.items())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_cost(self) -> Dict[str, int]:
|
||||||
|
ingredients = Counter()
|
||||||
|
for ingredient, cost in self.ingredients.items():
|
||||||
|
if ingredient in all_product_sources:
|
||||||
|
for recipe in all_product_sources[ingredient]:
|
||||||
|
ingredients.update({name: amount*cost/recipe.products[ingredient] for name, amount in recipe.base_cost.items()})
|
||||||
|
else:
|
||||||
|
ingredients[ingredient] += cost
|
||||||
|
return ingredients
|
||||||
|
|
||||||
class Machine(FactorioElement):
|
class Machine(FactorioElement):
|
||||||
def __init__(self, name, categories):
|
def __init__(self, name, categories):
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.categories: set = categories
|
self.categories: set = categories
|
||||||
|
|
||||||
|
|
||||||
# recipes and technologies can share names in Factorio
|
# recipes and technologies can share names in Factorio
|
||||||
for technology_name in sorted(raw):
|
for technology_name in sorted(raw):
|
||||||
data = raw[technology_name]
|
data = raw[technology_name]
|
||||||
factorio_id += 1
|
|
||||||
current_ingredients = set(data["ingredients"])
|
current_ingredients = set(data["ingredients"])
|
||||||
technology = Technology(technology_name, current_ingredients, factorio_id)
|
technology = Technology(technology_name, current_ingredients, factorio_id)
|
||||||
factorio_id += 1
|
factorio_id += 1
|
||||||
@@ -116,16 +147,22 @@ for technology, data in raw.items():
|
|||||||
recipe_sources.setdefault(recipe_name, set()).add(technology)
|
recipe_sources.setdefault(recipe_name, set()).add(technology)
|
||||||
|
|
||||||
del (raw)
|
del (raw)
|
||||||
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
|
||||||
recipes = {}
|
recipes = {}
|
||||||
all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
|
all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
|
||||||
|
# add uranium mining to logic graph. TODO: add to automatic extractor for mod support
|
||||||
|
raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining"}
|
||||||
|
|
||||||
for recipe_name, recipe_data in raw_recipes.items():
|
for recipe_name, recipe_data in raw_recipes.items():
|
||||||
# example:
|
# example:
|
||||||
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
|
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
|
||||||
|
|
||||||
recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))
|
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"], recipe_data["products"])
|
||||||
recipes[recipe_name] = Recipe
|
recipes[recipe_name] = recipe
|
||||||
if recipe.products.isdisjoint(recipe.ingredients) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging
|
if set(recipe.products).isdisjoint(
|
||||||
|
# prevents loop recipes like uranium centrifuging
|
||||||
|
set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \
|
||||||
|
not recipe_name.endswith("-reprocessing"):
|
||||||
for product_name in recipe.products:
|
for product_name in recipe.products:
|
||||||
all_product_sources.setdefault(product_name, set()).add(recipe)
|
all_product_sources.setdefault(product_name, set()).add(recipe)
|
||||||
|
|
||||||
@@ -137,6 +174,10 @@ for name, categories in raw_machines.items():
|
|||||||
machine = Machine(name, set(categories))
|
machine = Machine(name, set(categories))
|
||||||
machines[name] = machine
|
machines[name] = machine
|
||||||
|
|
||||||
|
# add electric mining drill as a crafting machine to resolve uranium-ore
|
||||||
|
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"})
|
||||||
|
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
|
||||||
|
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
|
||||||
del (raw_machines)
|
del (raw_machines)
|
||||||
|
|
||||||
# build requirements graph for all technology ingredients
|
# build requirements graph for all technology ingredients
|
||||||
@@ -147,22 +188,24 @@ for technology in technology_table.values():
|
|||||||
|
|
||||||
|
|
||||||
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
|
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
|
||||||
current_technologies = set()
|
current_technologies = recipe.unlocking_technologies
|
||||||
current_technologies |= recipe.unlocking_technologies
|
|
||||||
for ingredient_name in recipe.ingredients:
|
for ingredient_name in recipe.ingredients:
|
||||||
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
|
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done,
|
||||||
|
unlock_func=unlock_just_tech)
|
||||||
return current_technologies
|
return current_technologies
|
||||||
|
|
||||||
|
|
||||||
def unlock(recipe: Recipe, _done) -> Set[Technology]:
|
def unlock(recipe: Recipe, _done) -> Set[Technology]:
|
||||||
current_technologies = set()
|
current_technologies = recipe.unlocking_technologies
|
||||||
current_technologies |= recipe.unlocking_technologies
|
|
||||||
for ingredient_name in recipe.ingredients:
|
for ingredient_name in recipe.ingredients:
|
||||||
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
|
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, unlock_func=unlock)
|
||||||
current_technologies |= required_category_technologies[recipe.category]
|
current_technologies |= required_category_technologies[recipe.category]
|
||||||
|
|
||||||
return current_technologies
|
return current_technologies
|
||||||
|
|
||||||
def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[Technology]:
|
|
||||||
|
def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[
|
||||||
|
Technology]:
|
||||||
if _done:
|
if _done:
|
||||||
if ingredient_name in _done:
|
if ingredient_name in _done:
|
||||||
return set()
|
return set()
|
||||||
@@ -180,58 +223,52 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f
|
|||||||
return current_technologies
|
return current_technologies
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
required_machine_technologies: Dict[str, FrozenSet[Technology]] = {}
|
required_machine_technologies: Dict[str, FrozenSet[Technology]] = {}
|
||||||
for ingredient_name in machines:
|
for ingredient_name in machines:
|
||||||
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
|
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
|
||||||
|
|
||||||
logical_machines = {}
|
logical_machines = {}
|
||||||
|
machine_tech_cost = {}
|
||||||
for machine in machines.values():
|
for machine in machines.values():
|
||||||
logically_useful = True
|
|
||||||
for pot_source_machine in machines.values():
|
|
||||||
if machine != pot_source_machine \
|
|
||||||
and machine.categories.issuperset(pot_source_machine.categories) \
|
|
||||||
and required_machine_technologies[machine.name].issuperset(
|
|
||||||
required_machine_technologies[pot_source_machine.name]):
|
|
||||||
logically_useful = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if logically_useful:
|
|
||||||
logical_machines[machine.name] = machine
|
|
||||||
|
|
||||||
del(required_machine_technologies)
|
|
||||||
|
|
||||||
machines_per_category: Dict[str: Set[Machine]] = {}
|
|
||||||
for machine in logical_machines.values():
|
|
||||||
for category in machine.categories:
|
for category in machine.categories:
|
||||||
machines_per_category.setdefault(category, set()).add(machine)
|
current_cost, current_machine = machine_tech_cost.get(category, (10000, "character"))
|
||||||
|
machine_cost = len(required_machine_technologies[machine.name])
|
||||||
|
if machine_cost < current_cost:
|
||||||
|
machine_tech_cost[category] = machine_cost, machine.name
|
||||||
|
|
||||||
|
machine_per_category: Dict[str: str] = {}
|
||||||
|
for category, (cost, machine_name) in machine_tech_cost.items():
|
||||||
|
machine_per_category[category] = machine_name
|
||||||
|
|
||||||
|
del (machine_tech_cost)
|
||||||
|
|
||||||
# required technologies to be able to craft recipes from a certain category
|
# required technologies to be able to craft recipes from a certain category
|
||||||
required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {}
|
required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {}
|
||||||
for category_name, cat_machines in machines_per_category.items():
|
for category_name, machine_name in machine_per_category.items():
|
||||||
techs = set()
|
techs = set()
|
||||||
for machine in cat_machines:
|
techs |= recursively_get_unlocking_technologies(machine_name)
|
||||||
techs |= recursively_get_unlocking_technologies(machine.name)
|
|
||||||
required_category_technologies[category_name] = frozenset(techs)
|
required_category_technologies[category_name] = frozenset(techs)
|
||||||
|
|
||||||
required_technologies: Dict[str, FrozenSet[Technology]] = {}
|
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name : frozenset(
|
||||||
for ingredient_name in all_ingredient_names:
|
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
|
||||||
required_technologies[ingredient_name] = frozenset(
|
|
||||||
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))
|
|
||||||
|
|
||||||
|
|
||||||
advancement_technologies: Set[str] = set()
|
advancement_technologies: Set[str] = set()
|
||||||
for technologies in required_technologies.values():
|
for ingredient_name in all_ingredient_names:
|
||||||
|
technologies = required_technologies[ingredient_name]
|
||||||
advancement_technologies |= {technology.name for technology in technologies}
|
advancement_technologies |= {technology.name for technology in technologies}
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(10)
|
@functools.lru_cache(10)
|
||||||
def get_rocket_requirements(ingredients: Set[str]) -> Set[str]:
|
def get_rocket_requirements(recipe: Recipe) -> Set[str]:
|
||||||
techs = recursively_get_unlocking_technologies("rocket-silo")
|
techs = recursively_get_unlocking_technologies("rocket-silo")
|
||||||
for ingredient in ingredients:
|
for ingredient in recipe.ingredients:
|
||||||
techs |= recursively_get_unlocking_technologies(ingredient)
|
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||||
return {tech.name for tech in techs}
|
return {tech.name for tech in techs}
|
||||||
|
|
||||||
|
|
||||||
|
free_sample_blacklist = all_ingredient_names | {"rocket-part"}
|
||||||
|
|
||||||
rocket_recipes = {
|
rocket_recipes = {
|
||||||
Options.MaxSciencePack.option_space_science_pack:
|
Options.MaxSciencePack.option_space_science_pack:
|
||||||
{"rocket-control-unit": 10, "low-density-structure": 10, "rocket-fuel": 10},
|
{"rocket-control-unit": 10, "low-density-structure": 10, "rocket-fuel": 10},
|
||||||
@@ -248,3 +285,135 @@ rocket_recipes = {
|
|||||||
Options.MaxSciencePack.option_automation_science_pack:
|
Options.MaxSciencePack.option_automation_science_pack:
|
||||||
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
|
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]}
|
||||||
|
|
||||||
|
# progressive technologies
|
||||||
|
# auto-progressive
|
||||||
|
progressive_rows = {}
|
||||||
|
progressive_incs = set()
|
||||||
|
for tech_name in tech_table:
|
||||||
|
if tech_name.endswith("-1"):
|
||||||
|
progressive_rows[tech_name] = []
|
||||||
|
elif tech_name[-2] == "-" and tech_name[-1] in string.digits:
|
||||||
|
progressive_incs.add(tech_name)
|
||||||
|
|
||||||
|
for root, progressive in progressive_rows.items():
|
||||||
|
seeking = root[:-1]+str(int(root[-1])+1)
|
||||||
|
while seeking in progressive_incs:
|
||||||
|
progressive.append(seeking)
|
||||||
|
progressive_incs.remove(seeking)
|
||||||
|
seeking = seeking[:-1]+str(int(seeking[-1])+1)
|
||||||
|
|
||||||
|
# make root entry the progressive name
|
||||||
|
for old_name in set(progressive_rows):
|
||||||
|
prog_name = "progressive-" + old_name.rsplit("-", 1)[0]
|
||||||
|
progressive_rows[prog_name] = tuple([old_name] + progressive_rows[old_name])
|
||||||
|
del(progressive_rows[old_name])
|
||||||
|
|
||||||
|
# no -1 start
|
||||||
|
base_starts = set()
|
||||||
|
for remnant in progressive_incs:
|
||||||
|
if remnant[-1] == "2":
|
||||||
|
base_starts.add(remnant[:-2])
|
||||||
|
|
||||||
|
for root in base_starts:
|
||||||
|
seeking = root+"-2"
|
||||||
|
progressive = [root]
|
||||||
|
while seeking in progressive_incs:
|
||||||
|
progressive.append(seeking)
|
||||||
|
seeking = seeking[:-1]+str(int(seeking[-1])+1)
|
||||||
|
progressive_rows["progressive-"+root] = tuple(progressive)
|
||||||
|
|
||||||
|
# science packs
|
||||||
|
progressive_rows["progressive-science-pack"] = tuple(Options.MaxSciencePack.get_ordered_science_packs())[1:]
|
||||||
|
|
||||||
|
|
||||||
|
# manual progressive
|
||||||
|
progressive_rows["progressive-processing"] = (
|
||||||
|
"steel-processing",
|
||||||
|
"oil-processing", "sulfur-processing", "advanced-oil-processing", "coal-liquefaction",
|
||||||
|
"uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing")
|
||||||
|
progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb")
|
||||||
|
progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron")
|
||||||
|
progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", "automated-rail-transportation", "rail-signals")
|
||||||
|
progressive_rows["progressive-engine"] = ("engine", "electric-engine")
|
||||||
|
progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2")
|
||||||
|
progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment")
|
||||||
|
progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "energy-shield-mk2-equipment")
|
||||||
|
progressive_rows["progressive-wall"] = ("stone-wall", "gate")
|
||||||
|
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
|
||||||
|
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter")
|
||||||
|
|
||||||
|
base_tech_table = tech_table.copy() # without progressive techs
|
||||||
|
base_technology_table = technology_table.copy()
|
||||||
|
|
||||||
|
progressive_tech_table: Dict[str, int] = {}
|
||||||
|
progressive_technology_table: Dict[str, Technology] = {}
|
||||||
|
|
||||||
|
for root in sorted(progressive_rows):
|
||||||
|
progressive = progressive_rows[root]
|
||||||
|
assert all(tech in tech_table for tech in progressive)
|
||||||
|
factorio_id += 1
|
||||||
|
progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id,
|
||||||
|
progressive)
|
||||||
|
progressive_tech_table[root] = progressive_technology.factorio_id
|
||||||
|
progressive_technology_table[root] = progressive_technology
|
||||||
|
if any(tech in advancement_technologies for tech in progressive):
|
||||||
|
advancement_technologies.add(root)
|
||||||
|
|
||||||
|
tech_to_progressive_lookup: Dict[str, str] = {}
|
||||||
|
for technology in progressive_technology_table.values():
|
||||||
|
for progressive in technology.progressive:
|
||||||
|
tech_to_progressive_lookup[progressive] = technology.name
|
||||||
|
|
||||||
|
tech_table.update(progressive_tech_table)
|
||||||
|
technology_table.update(progressive_technology_table)
|
||||||
|
|
||||||
|
# techs that are never progressive
|
||||||
|
common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items()
|
||||||
|
if tech_name not in progressive_tech_table}
|
||||||
|
|
||||||
|
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
||||||
|
|
||||||
|
rel_cost = {
|
||||||
|
"wood" : 10000,
|
||||||
|
"iron-ore": 1,
|
||||||
|
"copper-ore": 1,
|
||||||
|
"stone": 1,
|
||||||
|
"crude-oil": 0.5,
|
||||||
|
"water": 0.001,
|
||||||
|
"coal": 1,
|
||||||
|
"raw-fish": 1000,
|
||||||
|
"steam": 0.01,
|
||||||
|
"used-up-uranium-fuel-cell": 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
# forbid liquids for now, TODO: allow a single liquid per assembler
|
||||||
|
blacklist = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas", "light-oil",
|
||||||
|
"heavy-oil", "lubricant", "steam"}
|
||||||
|
|
||||||
|
@Utils.cache_argsless
|
||||||
|
def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||||
|
def get_estimated_difficulty(recipe: Recipe):
|
||||||
|
base_ingredients = recipe.base_cost
|
||||||
|
cost = 0
|
||||||
|
|
||||||
|
for ingredient_name, amount in base_ingredients.items():
|
||||||
|
cost += rel_cost.get(ingredient_name, 1) * amount
|
||||||
|
return cost
|
||||||
|
|
||||||
|
|
||||||
|
science_pack_pools = {}
|
||||||
|
already_taken = blacklist.copy()
|
||||||
|
current_difficulty = 5
|
||||||
|
for science_pack in Options.MaxSciencePack.get_ordered_science_packs():
|
||||||
|
current = science_pack_pools[science_pack] = set()
|
||||||
|
for name, recipe in recipes.items():
|
||||||
|
if (science_pack != "automation-science-pack" or not recipe.recursive_unlocking_technologies) \
|
||||||
|
and get_estimated_difficulty(recipe) < current_difficulty:
|
||||||
|
current |= set(recipe.products)
|
||||||
|
current -= already_taken
|
||||||
|
already_taken |= current
|
||||||
|
current_difficulty *= 2
|
||||||
|
return science_pack_pools
|
||||||
@@ -1,31 +1,41 @@
|
|||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
from BaseClasses import Region, Entrance, Location, Item
|
||||||
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \
|
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
|
||||||
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes
|
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \
|
||||||
|
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||||
|
get_science_pack_pools, Recipe, recipes, technology_table
|
||||||
from .Shapes import get_shapes
|
from .Shapes import get_shapes
|
||||||
from .Mod import generate_mod
|
from .Mod import generate_mod
|
||||||
|
from .Options import factorio_options
|
||||||
|
|
||||||
class Factorio(World):
|
class Factorio(World):
|
||||||
game: str = "Factorio"
|
game: str = "Factorio"
|
||||||
static_nodes = {"automation", "logistics", "rocket-silo"}
|
static_nodes = {"automation", "logistics", "rocket-silo"}
|
||||||
|
custom_recipes = {}
|
||||||
|
additional_advancement_technologies = set()
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
victory_tech_names = get_rocket_requirements(
|
for tech_name, tech_id in base_tech_table.items():
|
||||||
frozenset(rocket_recipes[self.world.max_science_pack[self.player].value]))
|
if self.world.progressive and tech_name in tech_to_progressive_lookup:
|
||||||
|
item_name = tech_to_progressive_lookup[tech_name]
|
||||||
|
tech_id = progressive_tech_table[item_name]
|
||||||
|
else:
|
||||||
|
item_name = tech_name
|
||||||
|
|
||||||
for tech_name, tech_id in tech_table.items():
|
tech_item = Item(item_name, item_name in advancement_technologies or
|
||||||
tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names,
|
item_name in self.additional_advancement_technologies,
|
||||||
tech_id, self.player)
|
tech_id, self.player)
|
||||||
tech_item.game = "Factorio"
|
tech_item.game = "Factorio"
|
||||||
if tech_name in self.static_nodes:
|
if tech_name in self.static_nodes:
|
||||||
self.world.get_location(tech_name, self.player).place_locked_item(tech_item)
|
self.world.get_location(tech_name, self.player).place_locked_item(tech_item)
|
||||||
else:
|
else:
|
||||||
self.world.itempool.append(tech_item)
|
self.world.itempool.append(tech_item)
|
||||||
|
world_gen = self.world.world_gen[self.player].value
|
||||||
|
if world_gen.get("seed", None) is None: # allow seed 0
|
||||||
|
world_gen["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint
|
||||||
|
|
||||||
def generate_output(self):
|
generate_output = generate_mod
|
||||||
generate_mod(self.world, self.player)
|
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
@@ -35,17 +45,20 @@ class Factorio(World):
|
|||||||
nauvis = Region("Nauvis", None, "Nauvis", player)
|
nauvis = Region("Nauvis", None, "Nauvis", player)
|
||||||
nauvis.world = menu.world = self.world
|
nauvis.world = menu.world = self.world
|
||||||
|
|
||||||
for tech_name, tech_id in tech_table.items():
|
for tech_name, tech_id in base_tech_table.items():
|
||||||
tech = Location(player, tech_name, tech_id, nauvis)
|
tech = Location(player, tech_name, tech_id, nauvis)
|
||||||
nauvis.locations.append(tech)
|
nauvis.locations.append(tech)
|
||||||
tech.game = "Factorio"
|
tech.game = "Factorio"
|
||||||
location = Location(player, "Rocket Launch", None, nauvis)
|
location = Location(player, "Rocket Launch", None, nauvis)
|
||||||
nauvis.locations.append(location)
|
nauvis.locations.append(location)
|
||||||
|
location.game = "Factorio"
|
||||||
event = Item("Victory", True, None, player)
|
event = Item("Victory", True, None, player)
|
||||||
|
event.game = "Factorio"
|
||||||
self.world.push_item(location, event, False)
|
self.world.push_item(location, event, False)
|
||||||
location.event = location.locked = True
|
location.event = location.locked = True
|
||||||
for ingredient in all_ingredient_names:
|
for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
|
||||||
location = Location(player, f"Automate {ingredient}", None, nauvis)
|
location = Location(player, f"Automate {ingredient}", None, nauvis)
|
||||||
|
location.game = "Factorio"
|
||||||
nauvis.locations.append(location)
|
nauvis.locations.append(location)
|
||||||
event = Item(f"Automated {ingredient}", True, None, player)
|
event = Item(f"Automated {ingredient}", True, None, player)
|
||||||
self.world.push_item(location, event, False)
|
self.world.push_item(location, event, False)
|
||||||
@@ -56,14 +69,25 @@ class Factorio(World):
|
|||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
world = self.world
|
world = self.world
|
||||||
player = self.player
|
player = self.player
|
||||||
self.custom_technologies = set_custom_technologies(self.world, self.player)
|
self.custom_technologies = self.set_custom_technologies()
|
||||||
|
self.set_custom_recipes()
|
||||||
shapes = get_shapes(self)
|
shapes = get_shapes(self)
|
||||||
if world.logic[player] != 'nologic':
|
if world.logic[player] != 'nologic':
|
||||||
from worlds.generic import Rules
|
from worlds.generic import Rules
|
||||||
for ingredient in all_ingredient_names:
|
for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
|
||||||
location = world.get_location(f"Automate {ingredient}", player)
|
location = world.get_location(f"Automate {ingredient}", player)
|
||||||
location.access_rule = lambda state, ingredient=ingredient: \
|
|
||||||
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
|
if self.world.recipe_ingredients[self.player]:
|
||||||
|
custom_recipe = self.custom_recipes[ingredient]
|
||||||
|
|
||||||
|
location.access_rule = lambda state, ingredient=ingredient, custom_recipe = custom_recipe: \
|
||||||
|
(ingredient not in technology_table or state.has(ingredient, player)) and \
|
||||||
|
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
|
||||||
|
for technology in required_technologies[sub_ingredient])
|
||||||
|
else:
|
||||||
|
location.access_rule = lambda state, ingredient=ingredient: \
|
||||||
|
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
|
||||||
|
|
||||||
for tech_name, technology in self.custom_technologies.items():
|
for tech_name, technology in self.custom_technologies.items():
|
||||||
location = world.get_location(tech_name, player)
|
location = world.get_location(tech_name, player)
|
||||||
Rules.set_rule(location, technology.build_rule(player))
|
Rules.set_rule(location, technology.build_rule(player))
|
||||||
@@ -72,17 +96,64 @@ class Factorio(World):
|
|||||||
locations = {world.get_location(requisite, player) for requisite in prequisites}
|
locations = {world.get_location(requisite, player) for requisite in prequisites}
|
||||||
Rules.add_rule(location, lambda state,
|
Rules.add_rule(location, lambda state,
|
||||||
locations=locations: all(state.can_reach(loc) for loc in locations))
|
locations=locations: all(state.can_reach(loc) for loc in locations))
|
||||||
# get all science pack technologies (but not the ability to craft them)
|
|
||||||
victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value]))
|
victory_tech_names = get_rocket_requirements(self.custom_recipes["rocket-part"])
|
||||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||||
for technology in
|
for technology in
|
||||||
victory_tech_names)
|
victory_tech_names)
|
||||||
|
|
||||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||||
|
|
||||||
def set_custom_technologies(world: MultiWorld, player: int):
|
def collect(self, state, item) -> bool:
|
||||||
custom_technologies = {}
|
if item.advancement and item.name in progressive_technology_table:
|
||||||
allowed_packs = world.max_science_pack[player].get_allowed_packs()
|
prog_table = progressive_technology_table[item.name].progressive
|
||||||
for technology_name, technology in technology_table.items():
|
for item_name in prog_table:
|
||||||
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
|
if not state.has(item_name, item.player):
|
||||||
return custom_technologies
|
state.prog_items[item_name, item.player] += 1
|
||||||
|
return True
|
||||||
|
return super(Factorio, self).collect(state, item)
|
||||||
|
|
||||||
|
def get_required_client_version(self) -> tuple:
|
||||||
|
return max((0, 1, 4), super(Factorio, self).get_required_client_version())
|
||||||
|
|
||||||
|
options = factorio_options
|
||||||
|
|
||||||
|
def set_custom_technologies(self):
|
||||||
|
custom_technologies = {}
|
||||||
|
allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs()
|
||||||
|
for technology_name, technology in base_technology_table.items():
|
||||||
|
custom_technologies[technology_name] = technology.get_custom(self.world, allowed_packs, self.player)
|
||||||
|
return custom_technologies
|
||||||
|
|
||||||
|
def set_custom_recipes(self):
|
||||||
|
original_rocket_part = recipes["rocket-part"]
|
||||||
|
science_pack_pools = get_science_pack_pools()
|
||||||
|
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
|
||||||
|
self.world.random.shuffle(valid_pool)
|
||||||
|
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||||
|
{valid_pool[x] : 10 for x in range(3)},
|
||||||
|
original_rocket_part.products)}
|
||||||
|
self.additional_advancement_technologies = {tech.name for tech in
|
||||||
|
self.custom_recipes["rocket-part"].recursive_unlocking_technologies}
|
||||||
|
|
||||||
|
if self.world.recipe_ingredients[self.player]:
|
||||||
|
valid_pool = []
|
||||||
|
for pack in self.world.max_science_pack[self.player].get_ordered_science_packs():
|
||||||
|
valid_pool += sorted(science_pack_pools[pack])
|
||||||
|
self.world.random.shuffle(valid_pool)
|
||||||
|
if pack in recipes: # skips over space science pack
|
||||||
|
original = recipes[pack]
|
||||||
|
new_ingredients = {}
|
||||||
|
for _ in original.ingredients:
|
||||||
|
new_ingredients[valid_pool.pop()] = 1
|
||||||
|
new_recipe = Recipe(pack, original.category, new_ingredients, original.products)
|
||||||
|
self.additional_advancement_technologies |= {tech.name for tech in
|
||||||
|
new_recipe.recursive_unlocking_technologies}
|
||||||
|
self.custom_recipes[pack] = new_recipe
|
||||||
|
|
||||||
|
# handle marking progressive techs as advancement
|
||||||
|
prog_add = set()
|
||||||
|
for tech in self.additional_advancement_technologies:
|
||||||
|
if tech in tech_to_progressive_lookup:
|
||||||
|
prog_add.add(tech_to_progressive_lookup[tech])
|
||||||
|
self.additional_advancement_technologies |= prog_add
|
||||||
|
|||||||
39
worlds/hk/Options.py
Normal file
39
worlds/hk/Options.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from Options import Option, DefaultOnToggle, Toggle
|
||||||
|
|
||||||
|
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {
|
||||||
|
"RandomizeDreamers": DefaultOnToggle,
|
||||||
|
"RandomizeSkills": DefaultOnToggle,
|
||||||
|
"RandomizeCharms": DefaultOnToggle,
|
||||||
|
"RandomizeKeys": DefaultOnToggle,
|
||||||
|
"RandomizeGeoChests": Toggle,
|
||||||
|
"RandomizeMaskShards": DefaultOnToggle,
|
||||||
|
"RandomizeVesselFragments": DefaultOnToggle,
|
||||||
|
"RandomizeCharmNotches": Toggle,
|
||||||
|
"RandomizePaleOre": DefaultOnToggle,
|
||||||
|
"RandomizeRancidEggs": Toggle,
|
||||||
|
"RandomizeRelics": DefaultOnToggle,
|
||||||
|
"RandomizeMaps": Toggle,
|
||||||
|
"RandomizeStags": Toggle,
|
||||||
|
"RandomizeGrubs": Toggle,
|
||||||
|
"RandomizeWhisperingRoots": Toggle,
|
||||||
|
"RandomizeRocks": Toggle,
|
||||||
|
"RandomizeSoulTotems": Toggle,
|
||||||
|
"RandomizePalaceTotems": Toggle,
|
||||||
|
"RandomizeLoreTablets": Toggle,
|
||||||
|
"RandomizeLifebloodCocoons": Toggle,
|
||||||
|
"RandomizeFlames": Toggle
|
||||||
|
}
|
||||||
|
hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
|
||||||
|
"MILDSKIPS": Toggle,
|
||||||
|
"SPICYSKIPS": Toggle,
|
||||||
|
"FIREBALLSKIPS": Toggle,
|
||||||
|
"ACIDSKIPS": Toggle,
|
||||||
|
"SPIKETUNNELS": Toggle,
|
||||||
|
"DARKROOMS": Toggle,
|
||||||
|
"CURSED": Toggle,
|
||||||
|
"SHADESKIPS": Toggle,
|
||||||
|
}
|
||||||
|
hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options,
|
||||||
|
**hollow_knight_skip_options}
|
||||||
@@ -6,12 +6,83 @@ from .Locations import lookup_name_to_id
|
|||||||
from .Items import item_table
|
from .Items import item_table
|
||||||
from .Regions import create_regions
|
from .Regions import create_regions
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
|
from .Options import hollow_knight_options
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
|
|
||||||
class HKWorld(World):
|
class HKWorld(World):
|
||||||
game: str = "Hollow Knight"
|
game: str = "Hollow Knight"
|
||||||
|
options = hollow_knight_options
|
||||||
|
|
||||||
|
def generate_basic(self):
|
||||||
|
# Link regions
|
||||||
|
self.world.get_entrance('Hollow Nest S&Q', self.player).connect(self.world.get_region('Hollow Nest', self.player))
|
||||||
|
|
||||||
|
# Generate item pool
|
||||||
|
pool = []
|
||||||
|
for item_name, item_data in item_table.items():
|
||||||
|
|
||||||
|
item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=self.player)
|
||||||
|
|
||||||
|
if item_data.type == "Event":
|
||||||
|
event_location = self.world.get_location(item_name, self.player)
|
||||||
|
self.world.push_item(event_location, item, collect=False)
|
||||||
|
event_location.event = True
|
||||||
|
event_location.locked = True
|
||||||
|
if item.name == "King's_Pass":
|
||||||
|
self.world.push_precollected(item)
|
||||||
|
elif item_data.type == "Cursed":
|
||||||
|
if self.world.CURSED[self.player]:
|
||||||
|
pool.append(item)
|
||||||
|
else:
|
||||||
|
# fill Focus Location with Focus and add it to start inventory as well.
|
||||||
|
event_location = self.world.get_location(item_name, self.player)
|
||||||
|
self.world.push_item(event_location, item)
|
||||||
|
event_location.event = True
|
||||||
|
event_location.locked = True
|
||||||
|
|
||||||
|
elif item_data.type == "Fake":
|
||||||
|
pass
|
||||||
|
elif item_data.type in not_shufflable_types:
|
||||||
|
location = self.world.get_location(item_name, self.player)
|
||||||
|
self.world.push_item(location, item, collect=False)
|
||||||
|
location.event = item.advancement
|
||||||
|
location.locked = True
|
||||||
|
else:
|
||||||
|
target = option_to_type_lookup[item.type]
|
||||||
|
shuffle_it = getattr(self.world, target)
|
||||||
|
if shuffle_it[self.player]:
|
||||||
|
pool.append(item)
|
||||||
|
else:
|
||||||
|
location = self.world.get_location(item_name, self.player)
|
||||||
|
self.world.push_item(location, item, collect=False)
|
||||||
|
location.event = item.advancement
|
||||||
|
location.locked = True
|
||||||
|
logger.debug(f"Placed {item_name} to vanilla for player {self.player}")
|
||||||
|
|
||||||
|
self.world.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
|
def set_rules(self):
|
||||||
|
set_rules(self.world, self.player)
|
||||||
|
|
||||||
|
|
||||||
|
def create_regions(self):
|
||||||
|
create_regions(self.world, self.player)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_output(self):
|
||||||
|
pass # Hollow Knight needs no output files
|
||||||
|
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
slot_data = {}
|
||||||
|
for option_name in self.options:
|
||||||
|
option = getattr(self.world, option_name)[self.player]
|
||||||
|
slot_data[option_name] = int(option.value)
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
|
||||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||||
ret = Region(name, None, name, player)
|
ret = Region(name, None, name, player)
|
||||||
@@ -43,16 +114,6 @@ class HKItem(Item):
|
|||||||
self.type = type
|
self.type = type
|
||||||
|
|
||||||
|
|
||||||
def gen_hollow(world: MultiWorld, player: int):
|
|
||||||
link_regions(world, player)
|
|
||||||
gen_items(world, player)
|
|
||||||
set_rules(world, player)
|
|
||||||
|
|
||||||
|
|
||||||
def link_regions(world: MultiWorld, player: int):
|
|
||||||
world.get_entrance('Hollow Nest S&Q', player).connect(world.get_region('Hollow Nest', player))
|
|
||||||
|
|
||||||
|
|
||||||
not_shufflable_types = {"Essence_Boss"}
|
not_shufflable_types = {"Essence_Boss"}
|
||||||
|
|
||||||
option_to_type_lookup = {
|
option_to_type_lookup = {
|
||||||
@@ -75,49 +136,6 @@ option_to_type_lookup = {
|
|||||||
"Vessel": "RandomizeVesselFragments",
|
"Vessel": "RandomizeVesselFragments",
|
||||||
}
|
}
|
||||||
|
|
||||||
def gen_items(world: MultiWorld, player: int):
|
|
||||||
pool = []
|
|
||||||
for item_name, item_data in item_table.items():
|
|
||||||
|
|
||||||
item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=player)
|
|
||||||
|
|
||||||
if item_data.type == "Event":
|
|
||||||
event_location = world.get_location(item_name, player)
|
|
||||||
world.push_item(event_location, item, collect=False)
|
|
||||||
event_location.event = True
|
|
||||||
event_location.locked = True
|
|
||||||
if item.name == "King's_Pass":
|
|
||||||
world.push_precollected(item)
|
|
||||||
elif item_data.type == "Cursed":
|
|
||||||
if world.CURSED[player]:
|
|
||||||
pool.append(item)
|
|
||||||
else:
|
|
||||||
# fill Focus Location with Focus and add it to start inventory as well.
|
|
||||||
event_location = world.get_location(item_name, player)
|
|
||||||
world.push_item(event_location, item)
|
|
||||||
event_location.event = True
|
|
||||||
event_location.locked = True
|
|
||||||
|
|
||||||
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, collect=False)
|
|
||||||
location.event = item.advancement
|
|
||||||
location.locked = True
|
|
||||||
else:
|
|
||||||
target = option_to_type_lookup[item.type]
|
|
||||||
shuffle_it = getattr(world, target)
|
|
||||||
if shuffle_it[player]:
|
|
||||||
pool.append(item)
|
|
||||||
else:
|
|
||||||
location = world.get_location(item_name, player)
|
|
||||||
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}")
|
|
||||||
|
|
||||||
world.itempool += pool
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
from BaseClasses import Item
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
class ItemData(typing.NamedTuple):
|
class ItemData(typing.NamedTuple):
|
||||||
@@ -46,6 +46,7 @@ item_table = {
|
|||||||
"8 Gold Ore": ItemData(45032, False),
|
"8 Gold Ore": ItemData(45032, False),
|
||||||
"Rotten Flesh": ItemData(45033, False),
|
"Rotten Flesh": ItemData(45033, False),
|
||||||
"Single Arrow": ItemData(45034, False),
|
"Single Arrow": ItemData(45034, False),
|
||||||
|
"Bee Trap (Minecraft)": ItemData(45100, False),
|
||||||
|
|
||||||
"Victory": ItemData(0, True)
|
"Victory": ItemData(0, True)
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,8 @@ item_frequencies = {
|
|||||||
"16 Porkchops": 8,
|
"16 Porkchops": 8,
|
||||||
"8 Gold Ore": 4,
|
"8 Gold Ore": 4,
|
||||||
"Rotten Flesh": 4,
|
"Rotten Flesh": 4,
|
||||||
"Single Arrow": 0
|
"Single Arrow": 0,
|
||||||
|
"Bee Trap (Minecraft)": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
from BaseClasses import Location
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
class AdvData(typing.NamedTuple):
|
class AdvData(typing.NamedTuple):
|
||||||
@@ -114,6 +114,7 @@ exclusion_table = {
|
|||||||
"Two by Two": "100 XP",
|
"Two by Two": "100 XP",
|
||||||
"Two Birds, One Arrow": "50 XP",
|
"Two Birds, One Arrow": "50 XP",
|
||||||
"Arbalistic": "100 XP",
|
"Arbalistic": "100 XP",
|
||||||
|
"Monsters Hunted": "100 XP",
|
||||||
"Beaconator": "50 XP",
|
"Beaconator": "50 XP",
|
||||||
"A Balanced Diet": "100 XP",
|
"A Balanced Diet": "100 XP",
|
||||||
"Uneasy Alliance": "100 XP",
|
"Uneasy Alliance": "100 XP",
|
||||||
|
|||||||
26
worlds/minecraft/Options.py
Normal file
26
worlds/minecraft/Options.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import typing
|
||||||
|
from Options import Choice, Option, Toggle, Range
|
||||||
|
|
||||||
|
|
||||||
|
class AdvancementGoal(Range):
|
||||||
|
range_start = 0
|
||||||
|
range_end = 87
|
||||||
|
default = 50
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
"bee_traps": Toggle
|
||||||
|
}
|
||||||
@@ -1,73 +1,44 @@
|
|||||||
from .Locations import MinecraftAdvancement, advancement_table
|
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
def link_minecraft_structures(world, player):
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
# Link mandatory connections first
|
||||||
for (exit, region) in mandatory_connections:
|
for (exit, region) in mandatory_connections:
|
||||||
world.get_entrance(exit, player).connect(world.get_region(region, player))
|
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)
|
# Get all unpaired exits and all regions without entrances (except the Menu)
|
||||||
# This function is destructive on these lists.
|
# 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]
|
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']
|
structs = [r.name for r in world.regions if r.player == player and r.entrances == [] and r.name != 'Menu']
|
||||||
|
exits_spoiler = exits[:] # copy the original order for the spoiler log
|
||||||
try:
|
try:
|
||||||
assert len(exits) == len(structs)
|
assert len(exits) == len(structs)
|
||||||
except AssertionError as e: # this should never happen
|
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
|
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_names[player]})")
|
||||||
num_regions = len(exits)
|
num_regions = len(exits)
|
||||||
pairs = {}
|
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):
|
def set_pair(exit, struct):
|
||||||
try:
|
if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])):
|
||||||
assert exit in exits
|
pairs[exit] = struct
|
||||||
assert struct in structs
|
exits.remove(exit)
|
||||||
except AssertionError as e:
|
structs.remove(struct)
|
||||||
raise Exception(f"Invalid connection: {exit} => {struct} for player {player}")
|
else:
|
||||||
pairs[exit] = struct
|
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_names[player]})")
|
||||||
exits.remove(exit)
|
|
||||||
structs.remove(struct)
|
|
||||||
|
|
||||||
# Plando stuff. Remove any utilized exits/structs from the lists.
|
# Connect plando structures first
|
||||||
# Raise error if trying to put Nether Fortress in the End.
|
|
||||||
if world.plando_connections[player]:
|
if world.plando_connections[player]:
|
||||||
for connection in world.plando_connections[player]:
|
for conn in world.plando_connections[player]:
|
||||||
try:
|
set_pair(conn.entrance, conn.exit)
|
||||||
if connection.entrance == 'The End Structure' and connection.exit == 'Nether Fortress':
|
|
||||||
raise Exception(f"Cannot place Nether Fortress in the End for player {player}")
|
|
||||||
set_pair(connection.entrance, connection.exit)
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"Could not connect using {connection}") from e
|
|
||||||
|
|
||||||
|
# The algorithm tries to place the most restrictive structures first. This algorithm always works on the
|
||||||
|
# relatively small set of restrictions here, but does not work on all possible inputs with valid configurations.
|
||||||
if world.shuffle_structures[player]:
|
if world.shuffle_structures[player]:
|
||||||
# Can't put Nether Fortress in the End
|
structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, [])))
|
||||||
if 'The End Structure' in exits and 'Nether Fortress' in structs:
|
for struct in structs[:]:
|
||||||
try:
|
try:
|
||||||
end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress'])
|
exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
|
||||||
set_pair('The End Structure', end_struct)
|
except IndexError:
|
||||||
except IndexError as e: # should only happen if structs is emptied by plando
|
raise Exception(f"No valid structure placements remaining for player {player} ({world.player_names[player]})")
|
||||||
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)
|
set_pair(exit, struct)
|
||||||
else: # write remaining default connections
|
else: # write remaining default connections
|
||||||
for (exit, struct) in default_connections:
|
for (exit, struct) in default_connections:
|
||||||
@@ -77,13 +48,15 @@ def link_minecraft_structures(world: MultiWorld, player: int):
|
|||||||
# Make sure we actually paired everything; might fail if plando
|
# Make sure we actually paired everything; might fail if plando
|
||||||
try:
|
try:
|
||||||
assert len(exits) == len(structs) == 0
|
assert len(exits) == len(structs) == 0
|
||||||
except AssertionError as e:
|
except AssertionError:
|
||||||
raise Exception(f"Failed to connect all Minecraft structures for player {player}; check plando settings in yaml") from e
|
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_names[player]})")
|
||||||
|
|
||||||
|
for exit in exits_spoiler:
|
||||||
|
world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player))
|
||||||
|
if world.shuffle_structures[player] or world.plando_connections[player]:
|
||||||
|
world.spoiler.set_entrance(exit, pairs[exit], 'entrance', player)
|
||||||
|
|
||||||
|
|
||||||
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)
|
# (Region name, list of exits)
|
||||||
mc_regions = [
|
mc_regions = [
|
||||||
@@ -112,3 +85,9 @@ default_connections = {
|
|||||||
('Nether Structure 2', 'Bastion Remnant'),
|
('Nether Structure 2', 'Bastion Remnant'),
|
||||||
('The End Structure', 'End City')
|
('The End Structure', 'End City')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Structure: illegal locations
|
||||||
|
illegal_connections = {
|
||||||
|
'Nether Fortress': ['The End Structure']
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from ..generic.Rules import set_rule
|
from ..generic.Rules import set_rule
|
||||||
from .Locations import exclusion_table, events_table
|
from .Locations import exclusion_table, events_table
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
from BaseClasses import MultiWorld
|
||||||
from Options import AdvancementGoal
|
|
||||||
|
|
||||||
def set_rules(world: MultiWorld, player: int):
|
def set_rules(world: MultiWorld, player: int):
|
||||||
|
|
||||||
def reachable_locations(state):
|
def reachable_locations(state):
|
||||||
postgame_advancements = set(exclusion_table['postgame'].keys())
|
postgame_advancements = set(exclusion_table['postgame'].keys())
|
||||||
postgame_advancements.add('Free the End')
|
postgame_advancements.add('Free the End')
|
||||||
@@ -15,21 +14,18 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
(location.name not in postgame_advancements) and
|
(location.name not in postgame_advancements) and
|
||||||
location.can_reach(state)]
|
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.
|
# 92 total advancements. Goal is to complete X advancements and then Free the End.
|
||||||
goal_map = {
|
# There are 5 advancements which cannot be included for dragon spawning (4 postgame, Free the End)
|
||||||
'few': 30,
|
# Hence the true maximum is (92 - 5) = 87
|
||||||
'normal': 50,
|
goal = int(world.advancement_goal[player].value)
|
||||||
'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)
|
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':
|
if world.logic[player] != 'nologic':
|
||||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
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
|
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('Bucket', player) or state.has('Progressive Tools', player, 3)) and
|
||||||
state.has_iron_ingots(player))
|
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("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 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("Overworld Structure 2", player), lambda state: state.can_adventure(player))
|
||||||
@@ -45,15 +41,15 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
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) and \
|
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) and \
|
||||||
((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike
|
((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike
|
||||||
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("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) and state.has('Ingot Crafting', player) and state.can_reach('The Nether', 'Region', 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
|
set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and
|
||||||
state.has("Fishing Rod", player) and # Water Breathing
|
state.has("Fishing Rod", player) and # Water Breathing
|
||||||
state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets
|
state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets
|
||||||
state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility
|
state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility
|
||||||
state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance
|
state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance
|
||||||
set_rule(world.get_location("Best Friends Forever", player), lambda state: True)
|
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
|
set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state.can_kill_wither(player) and
|
||||||
state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
|
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("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("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("Local Brewery", player), lambda state: state.can_brew_potions(player))
|
||||||
@@ -64,14 +60,16 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", 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("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("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
|
set_rule(world.get_location("How Did We Get Here?", player), lambda state: state.can_brew_potions(player) and
|
||||||
state.can_reach('End City', 'Region', player) and state.can_reach('The Nether', 'Region', player) and # Levitation; potion ingredients
|
state.has_gold_ingots(player) and # Absorption
|
||||||
state.has("Fishing Rod", player) and state.has("Archery", player) and # Pufferfish, Nautilus Shells; spectral arrows
|
state.can_reach('End City', 'Region', player) and # Levitation
|
||||||
state.can_reach("Bring Home the Beacon", "Location", player) and # Haste
|
state.can_reach('The Nether', 'Region', player) and # potion ingredients
|
||||||
state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village
|
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("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("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("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("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("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("We Need to Go Deeper", player), lambda state: True)
|
||||||
@@ -88,7 +86,7 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
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("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
|
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))
|
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("The End... Again...", player), lambda state: can_complete(state))
|
||||||
set_rule(world.get_location("Acquire Hardware", player), lambda state: state.has_iron_ingots(player))
|
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("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("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player))
|
||||||
@@ -98,7 +96,7 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
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("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("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("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("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("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("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("Eye Spy", player), lambda state: state.enter_stronghold(player))
|
||||||
@@ -110,12 +108,12 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
set_rule(world.get_location("A Seedy Place", player), lambda state: True)
|
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("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("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("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
|
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))
|
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("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
|
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
|
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("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("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("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player))
|
||||||
@@ -123,26 +121,28 @@ def set_rules(world: MultiWorld, player: int):
|
|||||||
set_rule(world.get_location("What a Deal!", player), lambda state: True)
|
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) and state.has('Fishing Rod', player))
|
set_rule(world.get_location("Uneasy Alliance", player), lambda state: state.has_diamond_pickaxe(player) and state.has('Fishing Rod', 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("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 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("A Throwaway Joke", player), lambda state: True) # kill drowned
|
||||||
set_rule(world.get_location("Minecraft", player), lambda state: True)
|
set_rule(world.get_location("Minecraft", player), lambda state: True)
|
||||||
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player))
|
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player))
|
||||||
set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(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
|
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.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))
|
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 End?", player), lambda state: True)
|
||||||
set_rule(world.get_location("The Parrots and the Bats", 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("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("Getting Wood", player), lambda state: True)
|
||||||
set_rule(world.get_location("Time to Mine!", 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("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("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("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) and state.has('Progressive Tools', player, 2)) # powered rails
|
set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails
|
||||||
set_rule(world.get_location("Time to Strike!", player), lambda state: True)
|
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("Cow Tipper", player), lambda state: True)
|
||||||
set_rule(world.get_location("When Pigs Fly", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", player) and state.can_adventure(player))
|
set_rule(world.get_location("When Pigs Fly", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and
|
||||||
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit
|
state.has("Fishing Rod", player) and state.can_adventure(player))
|
||||||
|
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and
|
||||||
|
(state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit
|
||||||
set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player))
|
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))
|
set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player))
|
||||||
|
|||||||
@@ -1,81 +1,95 @@
|
|||||||
from random import Random
|
|
||||||
from .Items import MinecraftItem, item_table, item_frequencies
|
from .Items import MinecraftItem, item_table, item_frequencies
|
||||||
from .Locations import exclusion_table, events_table
|
from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table
|
||||||
from .Regions import link_minecraft_structures
|
from .Regions import mc_regions, link_minecraft_structures
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import Region, Entrance
|
||||||
from Options import minecraft_options
|
from .Options import minecraft_options
|
||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
|
|
||||||
|
client_version = (0, 4)
|
||||||
|
|
||||||
class MinecraftWorld(World):
|
class MinecraftWorld(World):
|
||||||
game: str = "Minecraft"
|
game: str = "Minecraft"
|
||||||
|
options = minecraft_options
|
||||||
|
topology_present = True
|
||||||
|
|
||||||
|
def _get_mc_data(self):
|
||||||
|
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2",
|
||||||
|
"The End Structure"]
|
||||||
|
return {
|
||||||
|
'world_seed': self.world.slot_seeds[self.player].getrandbits(32),
|
||||||
|
# consistent and doesn't interfere with other generation
|
||||||
|
'seed_name': self.world.seed_name,
|
||||||
|
'player_name': self.world.get_player_names(self.player),
|
||||||
|
'player_id': self.player,
|
||||||
|
'client_version': client_version,
|
||||||
|
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
client_version = (0, 3)
|
def generate_basic(self):
|
||||||
|
link_minecraft_structures(self.world, self.player)
|
||||||
|
|
||||||
|
pool = []
|
||||||
|
pool_counts = item_frequencies.copy()
|
||||||
|
if getattr(self.world, "bee_traps")[self.player]:
|
||||||
|
pool_counts.update({"Rotten Flesh": 0, "Bee Trap (Minecraft)": 4})
|
||||||
|
for item_name, item_data in item_table.items():
|
||||||
|
for count in range(pool_counts.get(item_name, 1)):
|
||||||
|
pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, self.player))
|
||||||
|
|
||||||
|
prefill_pool = {}
|
||||||
|
prefill_pool.update(events_table)
|
||||||
|
exclusion_pools = ['hard', 'insane', 'postgame']
|
||||||
|
for key in exclusion_pools:
|
||||||
|
if not getattr(self.world, f"include_{key}_advancements")[self.player]:
|
||||||
|
prefill_pool.update(exclusion_table[key])
|
||||||
|
|
||||||
|
for loc_name, item_name in prefill_pool.items():
|
||||||
|
item_data = item_table[item_name]
|
||||||
|
location = self.world.get_location(loc_name, self.player)
|
||||||
|
item = MinecraftItem(item_name, item_data.progression, item_data.code, self.player)
|
||||||
|
self.world.push_item(location, item, collect=False)
|
||||||
|
pool.remove(item)
|
||||||
|
location.event = item_data.progression
|
||||||
|
location.locked = True
|
||||||
|
|
||||||
|
self.world.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
def get_mc_data(world: MultiWorld, player: int):
|
def set_rules(self):
|
||||||
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2",
|
set_rules(self.world, self.player)
|
||||||
"The End Structure"]
|
|
||||||
return {
|
|
||||||
'world_seed': world.slot_seeds[player].getrandbits(32),
|
|
||||||
# consistent and doesn't interfere with other generation
|
|
||||||
'seed_name': world.seed_name,
|
|
||||||
'player_name': world.get_player_names(player),
|
|
||||||
'player_id': player,
|
|
||||||
'client_version': client_version,
|
|
||||||
'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_mc_data(world: MultiWorld, player: int):
|
def create_regions(self):
|
||||||
import base64, json
|
def MCRegion(region_name: str, exits=[]):
|
||||||
from Utils import output_path
|
ret = Region(region_name, None, region_name, self.player)
|
||||||
|
ret.world = self.world
|
||||||
|
ret.locations = [ MinecraftAdvancement(self.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(self.player, exit, ret))
|
||||||
|
return ret
|
||||||
|
|
||||||
data = get_mc_data(world, player)
|
self.world.regions += [MCRegion(*r) for r in mc_regions]
|
||||||
filename = f"AP_{world.seed_name}_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):
|
def generate_output(self):
|
||||||
slot_data = get_mc_data(world, player)
|
import json
|
||||||
for option_name in minecraft_options:
|
from base64 import b64encode
|
||||||
option = getattr(world, option_name)[player]
|
from Utils import output_path
|
||||||
slot_data[option_name] = int(option.value)
|
|
||||||
return slot_data
|
data = self._get_mc_data()
|
||||||
|
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_names(self.player)}.apmc"
|
||||||
|
with open(output_path(filename), 'wb') as f:
|
||||||
|
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||||
|
|
||||||
|
|
||||||
# Generates the item pool given the table and frequencies in Items.py.
|
def fill_slot_data(self):
|
||||||
def minecraft_gen_item_pool(world: MultiWorld, player: int):
|
slot_data = self._get_mc_data()
|
||||||
pool = []
|
for option_name in minecraft_options:
|
||||||
for item_name, item_data in item_table.items():
|
option = getattr(self.world, option_name)[self.player]
|
||||||
for count in range(item_frequencies.get(item_name, 1)):
|
slot_data[option_name] = int(option.value)
|
||||||
pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, player))
|
return slot_data
|
||||||
|
|
||||||
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