Compare commits
210 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 | ||
|
|
6c1d164330 | ||
|
|
937fee9019 | ||
|
|
023a798ac1 | ||
|
|
07d61f6d47 | ||
|
|
304f63aedf | ||
|
|
30190f373a | ||
|
|
b51b094cc1 | ||
|
|
f4a2f344a7 | ||
|
|
1e7214a86b | ||
|
|
f8fd8b3585 | ||
|
|
644d62c915 | ||
|
|
741ec36ee1 | ||
|
|
a08d7bb1b2 | ||
|
|
16ae77ca1c | ||
|
|
a5bf3a8407 | ||
|
|
cd0306d513 | ||
|
|
b29d0b8276 | ||
|
|
3ee88fd8fe | ||
|
|
bc9c93b180 | ||
|
|
e49d10ab22 | ||
|
|
059946d59e | ||
|
|
6211760922 | ||
|
|
167958c002 | ||
|
|
8b16ffb629 | ||
|
|
b5193162bf | ||
|
|
bc34c237b6 | ||
|
|
d9824d26d2 | ||
|
|
8d08b55e69 | ||
|
|
503c844971 | ||
|
|
deff356910 | ||
|
|
883ebbf267 | ||
|
|
cd45116dce | ||
|
|
d80362c4b8 | ||
|
|
384e06d6fe | ||
|
|
e6f44a70d0 | ||
|
|
0ca90ee7e8 | ||
|
|
59a56c803a | ||
|
|
1e0b44bdc5 | ||
|
|
2f3296bada | ||
|
|
434d8e0977 | ||
|
|
0a89eaaf62 | ||
|
|
cea2f81b86 | ||
|
|
86b612f3b5 | ||
|
|
d425e5eb6a | ||
|
|
183fd33f3f | ||
|
|
8c82d3e747 | ||
|
|
7b495f3d81 | ||
|
|
3ea7f1cb03 | ||
|
|
2a13fe05c6 | ||
|
|
2c4c899179 | ||
|
|
760fb32016 | ||
|
|
278f40471b | ||
|
|
20ca09c730 | ||
|
|
568a71cdbe | ||
|
|
753a5f7cb2 | ||
|
|
96e13786cd | ||
|
|
5d6592f296 | ||
|
|
534dd331b9 | ||
|
|
b3b56fcafd | ||
|
|
671fd50cfb | ||
|
|
eaf19643a9 | ||
|
|
a582a3781b | ||
|
|
e0d90e0b21 | ||
|
|
a73189338c | ||
|
|
1e414dd370 | ||
|
|
5ea03c71c0 | ||
|
|
d7a46f089e | ||
|
|
6e33181f05 | ||
|
|
622f8f8158 | ||
|
|
821b0f0f92 | ||
|
|
471b217e99 | ||
|
|
adda0eff4a | ||
|
|
2001ca6566 | ||
|
|
b9a783d7d7 | ||
|
|
eb9ee9f41e | ||
|
|
fae14ad283 | ||
|
|
4b5ac3f926 | ||
|
|
72e5acfb86 | ||
|
|
16c6e17a49 | ||
|
|
ac31671914 | ||
|
|
4b283242fe | ||
|
|
353ea0fbbe | ||
|
|
fc941f55ef | ||
|
|
12600a8cbd | ||
|
|
33fa9542e0 | ||
|
|
d872ea32af | ||
|
|
46bb2d1367 | ||
|
|
403ddd603f | ||
|
|
7907838c24 | ||
|
|
15bd79186a | ||
|
|
4555b77204 | ||
|
|
dd3c612dec | ||
|
|
09b6698de8 | ||
|
|
27ee156706 | ||
|
|
48c3d1fa4a | ||
|
|
286254c5cd | ||
|
|
82cd51f5f4 | ||
|
|
08bf993146 | ||
|
|
a55bcae3ec | ||
|
|
607a14e921 | ||
|
|
c71387ad00 | ||
|
|
c095c28618 | ||
|
|
cae1188ff8 | ||
|
|
7e599c51f8 | ||
|
|
6ccb9d2dc2 | ||
|
|
1d00ed463e | ||
|
|
c99054e479 | ||
|
|
85a9e0d0bc | ||
|
|
8b4ea3c80c | ||
|
|
30dec34b72 | ||
|
|
a3d2df7c45 | ||
|
|
034f338f45 | ||
|
|
1d84346705 | ||
|
|
6e916ebd45 | ||
|
|
a993bed8dc | ||
|
|
aa6f65ee1f | ||
|
|
573931930c | ||
|
|
252bb69808 | ||
|
|
0175c8ab8a | ||
|
|
f78bb2078d | ||
|
|
bc028a63cd | ||
|
|
4b04f2b918 | ||
|
|
887a3b0922 | ||
|
|
3df78fa387 | ||
|
|
c36ac5baba | ||
|
|
d8e33fe596 | ||
|
|
80b7e2e188 | ||
|
|
14b430a168 | ||
|
|
22aa4cbb9f | ||
|
|
71bb5b850e | ||
|
|
066c830a43 | ||
|
|
760107becf | ||
|
|
8dad49e385 | ||
|
|
518e5db55b | ||
|
|
31a3c1cf33 | ||
|
|
e1b4975a11 | ||
|
|
f8a5e8bfc7 | ||
|
|
a656ad5cd2 | ||
|
|
109eb5b9dc |
5
.gitignore
vendored
@@ -16,6 +16,7 @@
|
||||
*.apsave
|
||||
|
||||
build
|
||||
/build_factorio/
|
||||
bundle/components.wxs
|
||||
dist
|
||||
README.html
|
||||
@@ -142,4 +143,6 @@ dmypy.json
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
cython_debug/
|
||||
|
||||
Archipelago.zip
|
||||
|
||||
248
BaseClasses.py
@@ -23,6 +23,7 @@ class MultiWorld():
|
||||
plando_items: List[PlandoItem]
|
||||
plando_connections: List[PlandoConnection]
|
||||
er_seeds: Dict[int, str]
|
||||
worlds: Dict[int, "AutoWorld.World"]
|
||||
|
||||
class AttributeProxy():
|
||||
def __init__(self, rule):
|
||||
@@ -32,8 +33,6 @@ class MultiWorld():
|
||||
return self.rule(player)
|
||||
|
||||
def __init__(self, players: int):
|
||||
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
|
||||
|
||||
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
||||
self.players = players
|
||||
self.teams = 1
|
||||
@@ -113,8 +112,6 @@ class MultiWorld():
|
||||
set_player_attr('bush_shuffle', False)
|
||||
set_player_attr('beemizer', 0)
|
||||
set_player_attr('escape_assist', [])
|
||||
set_player_attr('crystals_needed_for_ganon', 7)
|
||||
set_player_attr('crystals_needed_for_gt', 7)
|
||||
set_player_attr('open_pyramid', False)
|
||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||
set_player_attr('treasure_hunt_count', 0)
|
||||
@@ -131,7 +128,6 @@ class MultiWorld():
|
||||
set_player_attr('triforce_pieces_available', 30)
|
||||
set_player_attr('triforce_pieces_required', 20)
|
||||
set_player_attr('shop_shuffle', 'off')
|
||||
set_player_attr('shop_shuffle_slots', 0)
|
||||
set_player_attr('shuffle_prizes', "g")
|
||||
set_player_attr('sprite_pool', [])
|
||||
set_player_attr('dark_room_logic', "lamp")
|
||||
@@ -141,39 +137,42 @@ class MultiWorld():
|
||||
set_player_attr('plando_connections', [])
|
||||
set_player_attr('game', "A Link to the Past")
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
set_player_attr(hk_option, False)
|
||||
self.custom_data = {}
|
||||
for player in range(1, players+1):
|
||||
self.worlds = {}
|
||||
|
||||
|
||||
def set_options(self, args):
|
||||
from worlds import AutoWorld
|
||||
for player in self.player_ids:
|
||||
self.custom_data[player] = {}
|
||||
# self.worlds = []
|
||||
# for i in range(players):
|
||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
||||
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):
|
||||
self.random = secrets.SystemRandom()
|
||||
|
||||
@property
|
||||
@functools.cached_property
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
soft_collect(item)
|
||||
self.worlds[item.player].collect(ret, item)
|
||||
|
||||
if keys:
|
||||
for p in self.alttp_player_ids:
|
||||
world = self.worlds[p]
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
for item in ItemFactory(
|
||||
['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 (Ganons Tower)'] * 4,
|
||||
p):
|
||||
soft_collect(item)
|
||||
world.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
return ret
|
||||
|
||||
@@ -813,6 +771,9 @@ class CollectionState(object):
|
||||
rules.append(self.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
|
||||
def can_bomb_clip(self, region: Region, player: int) -> bool:
|
||||
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
|
||||
|
||||
# Minecraft logic functions
|
||||
def has_iron_ingots(self, player: int):
|
||||
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
|
||||
@@ -888,72 +849,24 @@ class CollectionState(object):
|
||||
return self.fortress_loot(player) and normal_kill
|
||||
|
||||
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':
|
||||
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.has('Archery', player) and \
|
||||
self.can_brew_potions(player) and self.can_enchant(player)
|
||||
return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
|
||||
self.has('Archery', player) and self.can_brew_potions(player) and self.can_enchant(player)
|
||||
if self.combat_difficulty(player) == 'hard':
|
||||
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
||||
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
|
||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
|
||||
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)))
|
||||
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:
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
changed = False
|
||||
|
||||
# TODO: create a mapping for progressive items in each game and use that
|
||||
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
|
||||
changed = self.world.worlds[item.player].collect(self, item)
|
||||
|
||||
|
||||
if not changed and (event or item.advancement):
|
||||
if not changed and event:
|
||||
self.prog_items[item.name, item.player] += 1
|
||||
changed = True
|
||||
|
||||
@@ -1193,6 +1106,14 @@ class Location():
|
||||
return True
|
||||
return False
|
||||
|
||||
def place_locked_item(self, item: Item):
|
||||
if self.item:
|
||||
raise Exception(f"Location {self} already filled.")
|
||||
self.item = item
|
||||
self.event = item.advancement
|
||||
self.item.world = self.parent_region.world
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -1221,7 +1142,7 @@ class Item():
|
||||
zora_credit_text = None
|
||||
fluteboy_credit_text = None
|
||||
|
||||
def __init__(self, name: str, advancement: bool, code: int, player: int):
|
||||
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
|
||||
self.name = name
|
||||
self.advancement = advancement
|
||||
self.player = player
|
||||
@@ -1229,11 +1150,11 @@ class Item():
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return getattr(self, "_hint_text", self.name)
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
|
||||
@property
|
||||
def pedestal_hint_text(self):
|
||||
return getattr(self, "_pedestal_hint_text", self.name)
|
||||
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.player == other.player
|
||||
@@ -1415,8 +1336,6 @@ class Spoiler(object):
|
||||
'shuffle': self.world.shuffle,
|
||||
'item_pool': self.world.difficulty,
|
||||
'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,
|
||||
'accessibility': self.world.accessibility,
|
||||
'hints': self.world.hints,
|
||||
@@ -1440,7 +1359,6 @@ class Spoiler(object):
|
||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||
'shop_shuffle': self.world.shop_shuffle,
|
||||
'shop_shuffle_slots': self.world.shop_shuffle_slots,
|
||||
'shuffle_prizes': self.world.shuffle_prizes,
|
||||
'sprite_pool': self.world.sprite_pool,
|
||||
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
||||
@@ -1467,6 +1385,7 @@ class Spoiler(object):
|
||||
return json.dumps(out)
|
||||
|
||||
def to_file(self, filename):
|
||||
import Options
|
||||
self.parse_data()
|
||||
|
||||
def bool_to_text(variable: Union[bool, str]) -> str:
|
||||
@@ -1489,16 +1408,12 @@ class Spoiler(object):
|
||||
outfile.write('Progression Balanced: %s\n' % (
|
||||
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||
if player in self.world.hk_player_ids:
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
res = getattr(self.world, hk_option)[player]
|
||||
outfile.write(f'{hk_option+":":33}{res}\n')
|
||||
if player in self.world.minecraft_player_ids:
|
||||
import Options
|
||||
for mc_option in Options.minecraft_options:
|
||||
res = getattr(self.world, mc_option)[player]
|
||||
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
||||
options = self.world.worlds[player].options
|
||||
if options:
|
||||
for f_option in options:
|
||||
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')
|
||||
|
||||
if player in self.world.alttp_player_ids:
|
||||
for team in range(self.world.teams):
|
||||
outfile.write('%s%s\n' % (
|
||||
@@ -1527,8 +1442,6 @@ class Spoiler(object):
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||
if self.metadata['shuffle'][player] != "vanilla":
|
||||
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' % (
|
||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
|
||||
@@ -1551,8 +1464,6 @@ class Spoiler(object):
|
||||
"f" in self.metadata["shop_shuffle"][player]))
|
||||
outfile.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
||||
outfile.write('Shop Slots: %s\n' %
|
||||
self.metadata["shop_shuffle_slots"][player])
|
||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||
outfile.write(
|
||||
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
||||
@@ -1575,17 +1486,31 @@ class Spoiler(object):
|
||||
'<=>' if entry['direction'] == 'both' else
|
||||
'<=' if entry['direction'] == 'exit' else '=>',
|
||||
entry['exit']) for entry in self.entrances.values()]))
|
||||
outfile.write('\n\nMedallions:\n')
|
||||
for dungeon, medallion in self.medallions.items():
|
||||
outfile.write(f'\n{dungeon}: {medallion}')
|
||||
|
||||
if self.medallions:
|
||||
outfile.write('\n\nMedallions:\n')
|
||||
for dungeon, medallion in self.medallions.items():
|
||||
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:
|
||||
outfile.write('\n\nStarting Inventory:\n\n')
|
||||
outfile.write('\n'.join(self.startinventory))
|
||||
|
||||
outfile.write('\n\nLocations:\n\n')
|
||||
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
||||
outfile.write('\n\nShops:\n\n')
|
||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
|
||||
for player in range(1, self.world.players + 1):
|
||||
|
||||
if self.shops:
|
||||
outfile.write('\n\nShops:\n\n')
|
||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
|
||||
|
||||
for player in self.world.alttp_player_ids:
|
||||
if self.world.boss_shuffle[player] != 'none':
|
||||
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
||||
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n')
|
||||
@@ -1595,19 +1520,20 @@ class Spoiler(object):
|
||||
if self.unreachables:
|
||||
outfile.write('\n\nUnreachable Items:\n\n')
|
||||
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||
outfile.write('\n\nPaths:\n\n')
|
||||
|
||||
path_listings = []
|
||||
for location, path in sorted(self.paths.items()):
|
||||
path_lines = []
|
||||
for region, exit in path:
|
||||
if exit is not None:
|
||||
path_lines.append("{} -> {}".format(region, exit))
|
||||
else:
|
||||
path_lines.append(region)
|
||||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
||||
if self.paths:
|
||||
outfile.write('\n\nPaths:\n\n')
|
||||
path_listings = []
|
||||
for location, path in sorted(self.paths.items()):
|
||||
path_lines = []
|
||||
for region, exit in path:
|
||||
if exit is not None:
|
||||
path_lines.append("{} -> {}".format(region, exit))
|
||||
else:
|
||||
path_lines.append(region)
|
||||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
||||
|
||||
outfile.write('\n'.join(path_listings))
|
||||
outfile.write('\n'.join(path_listings))
|
||||
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.generic import PlandoItem, PlandoConnection
|
||||
@@ -4,9 +4,7 @@ import typing
|
||||
import asyncio
|
||||
import urllib.parse
|
||||
|
||||
import prompt_toolkit
|
||||
import websockets
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
|
||||
import Utils
|
||||
from MultiServer import CommandProcessor
|
||||
@@ -47,12 +45,9 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_received(self) -> bool:
|
||||
"""List all received items"""
|
||||
logger.info('Received items:')
|
||||
logger.info(f'{len(self.ctx.items_received)} received items:')
|
||||
for index, item in enumerate(self.ctx.items_received, 1):
|
||||
logging.info('%s from %s (%s) (%d/%d in list)' % (
|
||||
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
|
||||
color(self.ctx.player_names[item.player], 'yellow'),
|
||||
self.ctx.location_name_getter(item.location), index, len(self.ctx.items_received)))
|
||||
self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}")
|
||||
return True
|
||||
|
||||
def _cmd_missing(self) -> bool:
|
||||
@@ -213,8 +208,6 @@ class CommonContext():
|
||||
logger.info(args["text"])
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
|
||||
|
||||
@@ -334,9 +327,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
logger.error('Invalid password')
|
||||
ctx.password = None
|
||||
await ctx.server_auth(True)
|
||||
else:
|
||||
elif 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':
|
||||
ctx.team = args["team"]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
import string
|
||||
import copy
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import sys
|
||||
import subprocess
|
||||
import factorio_rcon
|
||||
|
||||
import colorama
|
||||
import asyncio
|
||||
@@ -13,16 +16,15 @@ from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
import random
|
||||
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus
|
||||
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
||||
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
|
||||
rcon_port = 24242
|
||||
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)
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
options = Utils.get_options()
|
||||
executable = options["factorio_options"]["executable"]
|
||||
bin_dir = os.path.dirname(executable)
|
||||
@@ -34,13 +36,12 @@ if not os.path.exists(executable):
|
||||
else:
|
||||
raise FileNotFoundError(executable)
|
||||
|
||||
import sys
|
||||
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
||||
|
||||
threadpool = ThreadPoolExecutor(10)
|
||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
ctx: FactorioContext
|
||||
|
||||
@mark_raw
|
||||
def _cmd_factorio(self, text: str) -> bool:
|
||||
"""Send the following command to the bound Factorio Server."""
|
||||
@@ -54,7 +55,10 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_connect(self, address: str = "") -> bool:
|
||||
"""Connect to a MultiWorld Server"""
|
||||
if not self.ctx.auth:
|
||||
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
||||
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)
|
||||
|
||||
|
||||
@@ -65,14 +69,16 @@ class FactorioContext(CommonContext):
|
||||
super(FactorioContext, self).__init__(*args, **kwargs)
|
||||
self.send_index = 0
|
||||
self.rcon_client = None
|
||||
self.awaiting_bridge = False
|
||||
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils._version_tuple,
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
'tags': ['AP'],
|
||||
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
||||
}])
|
||||
@@ -81,61 +87,63 @@ class FactorioContext(CommonContext):
|
||||
logger.info(args["text"])
|
||||
if self.rcon_client:
|
||||
cleaned_text = args['text'].replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{cleaned_text}\")")
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
if self.rcon_client:
|
||||
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
||||
text = self.factorio_json_text_parser(args["data"])
|
||||
cleaned_text = text.replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{cleaned_text}\")")
|
||||
|
||||
async def game_watcher(ctx: FactorioContext, bridge_file: str):
|
||||
@property
|
||||
def savegame_name(self) -> str:
|
||||
return f"AP_{self.seed_name}_{self.auth}.zip"
|
||||
|
||||
|
||||
async def game_watcher(ctx: FactorioContext):
|
||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
bridge_counter = 0
|
||||
try:
|
||||
while 1:
|
||||
if os.path.exists(bridge_file):
|
||||
bridge_logger.info("Found Factorio Bridge file.")
|
||||
while 1:
|
||||
with open(bridge_file) as f:
|
||||
data = json.load(f)
|
||||
research_data = data["research_done"]
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
victory = data["victory"]
|
||||
ctx.auth = data["slot_name"]
|
||||
ctx.seed_name = data["seed_name"]
|
||||
while not ctx.exit_event.is_set():
|
||||
if ctx.awaiting_bridge and ctx.rcon_client:
|
||||
ctx.awaiting_bridge = False
|
||||
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
|
||||
if data["slot_name"] != ctx.auth:
|
||||
logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
|
||||
elif data["seed_name"] != ctx.seed_name:
|
||||
logger.warning(f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
||||
else:
|
||||
data = data["info"]
|
||||
research_data = data["research_done"]
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
victory = data["victory"]
|
||||
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if ctx.locations_checked != research_data:
|
||||
bridge_logger.info(f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
bridge_logger.info(
|
||||
f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
ctx.locations_checked = research_data
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
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)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
|
||||
|
||||
def stream_factorio_output(pipe, queue):
|
||||
def stream_factorio_output(pipe, queue, process):
|
||||
def queuer():
|
||||
while 1:
|
||||
while process.poll() is None:
|
||||
text = pipe.readline().strip()
|
||||
if text:
|
||||
queue.put_nowait(text)
|
||||
@@ -144,40 +152,42 @@ def stream_factorio_output(pipe, queue):
|
||||
|
||||
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
async def factorio_server_watcher(ctx: FactorioContext):
|
||||
import subprocess
|
||||
import factorio_rcon
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)),
|
||||
savegame_name = os.path.abspath(ctx.savegame_name)
|
||||
if not os.path.exists(savegame_name):
|
||||
logger.info(f"Creating savegame {savegame_name}")
|
||||
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,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
encoding="utf-8")
|
||||
factorio_server_logger.info("Started Factorio Server")
|
||||
factorio_queue = Queue()
|
||||
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
||||
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
||||
script_folder = None
|
||||
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
||||
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
||||
try:
|
||||
while 1:
|
||||
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():
|
||||
msg = factorio_queue.get()
|
||||
factorio_server_logger.info(msg)
|
||||
if not ctx.rcon_client and "Hosting game at IP ADDR:" in msg:
|
||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
# trigger lua interface confirmation
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/ap-sync")
|
||||
if not script_folder and "Write data path:" in msg:
|
||||
script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
|
||||
bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json")
|
||||
if os.path.exists(bridge_file):
|
||||
os.remove(bridge_file)
|
||||
logging.info(f"Bridge File Path: {bridge_file}")
|
||||
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
||||
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||
ctx.awaiting_bridge = True
|
||||
if ctx.rcon_client:
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
@@ -188,42 +198,118 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
else:
|
||||
item_name = lookup_id_to_name[item_id]
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
|
||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
|
||||
ctx.send_index += 1
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
ctx.rcon_client = None
|
||||
ctx.exit_event.set()
|
||||
|
||||
finally:
|
||||
factorio_process.terminate()
|
||||
|
||||
|
||||
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)
|
||||
# testing shortcuts
|
||||
# ctx.server_address = "localhost"
|
||||
# ctx.auth = "Nauvis"
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
await asyncio.sleep(3)
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if ui:
|
||||
input_task = None
|
||||
ui_app = ui(ctx)
|
||||
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
||||
else:
|
||||
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")
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
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()
|
||||
if ctx.server_task is not None:
|
||||
await ctx.server_task
|
||||
await factorio_server_task
|
||||
|
||||
while ctx.input_requests > 0:
|
||||
ctx.input_queue.put_nowait(None)
|
||||
ctx.input_requests -= 1
|
||||
|
||||
await input_task
|
||||
if ui_task:
|
||||
await ui_task
|
||||
|
||||
if input_task:
|
||||
input_task.cancel()
|
||||
|
||||
|
||||
class FactorioJSONtoTextParser(JSONtoTextParser):
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
colors = node["color"].split(";")
|
||||
for color in colors:
|
||||
if color in {"red", "green", "blue", "orange", "yellow", "pink", "purple", "white", "black", "gray",
|
||||
"brown", "cyan", "acid"}:
|
||||
node["text"] = f"[color={color}]{node['text']}[/color]"
|
||||
return self._handle_text(node)
|
||||
elif color == "magenta":
|
||||
node["text"] = f"[color=pink]{node['text']}[/color]"
|
||||
return self._handle_text(node)
|
||||
return self._handle_text(node)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
141
FactorioClientGUI.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
if getattr(sys, "frozen", False):
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w")
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
os.environ["KIVY_NO_ARGS"] = "1"
|
||||
|
||||
import asyncio
|
||||
from CommonClient import logger
|
||||
from FactorioClient import main
|
||||
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.label import Label
|
||||
from kivy.base import ExceptionHandler, ExceptionManager, Config
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
class FactorioManager(App):
|
||||
def __init__(self, ctx):
|
||||
super(FactorioManager, self).__init__()
|
||||
self.ctx = ctx
|
||||
self.commandprocessor = ctx.command_processor(ctx)
|
||||
self.icon = r"data/icon.png"
|
||||
|
||||
def build(self):
|
||||
self.grid = GridLayout()
|
||||
self.grid.cols = 1
|
||||
self.tabs = TabbedPanel()
|
||||
self.tabs.default_tab_text = "All"
|
||||
self.title = "Archipelago Factorio Client"
|
||||
pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("FactorioServer", "Factorio Server Log"),
|
||||
("FactorioWatcher", "Bridge Data Log"),
|
||||
]
|
||||
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
|
||||
for logger_name, display_name in pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
panel = TabbedPanelItem(text=display_name)
|
||||
panel.content = UILog(bridge_logger)
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
self.grid.add_widget(self.tabs)
|
||||
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
|
||||
textinput.bind(on_text_validate=self.on_message)
|
||||
self.grid.add_widget(textinput)
|
||||
self.commandprocessor("/help")
|
||||
return self.grid
|
||||
|
||||
def on_stop(self):
|
||||
self.ctx.exit_event.set()
|
||||
|
||||
def on_message(self, textinput: TextInput):
|
||||
try:
|
||||
input_text = textinput.text.strip()
|
||||
textinput.text = ""
|
||||
|
||||
if self.ctx.input_requests > 0:
|
||||
self.ctx.input_requests -= 1
|
||||
self.ctx.input_queue.put_nowait(input_text)
|
||||
elif input_text:
|
||||
self.commandprocessor(input_text)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
||||
self.on_log = on_log
|
||||
|
||||
def handle(self, record: logging.LogRecord) -> None:
|
||||
self.on_log(record)
|
||||
|
||||
|
||||
class UILog(RecycleView):
|
||||
cols = 1
|
||||
|
||||
def __init__(self, *loggers_to_handle, **kwargs):
|
||||
super(UILog, self).__init__(**kwargs)
|
||||
self.data = []
|
||||
for logger in loggers_to_handle:
|
||||
logger.addHandler(LogtoUI(self.on_log))
|
||||
|
||||
def on_log(self, record: logging.LogRecord) -> None:
|
||||
self.data.append({"text": record.getMessage()})
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
def handle_exception(self, inst):
|
||||
logger.exception(inst)
|
||||
return ExceptionManager.RAISE
|
||||
|
||||
ExceptionManager.add_handler(E())
|
||||
|
||||
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Builder.load_string('''
|
||||
<TabbedPanel>
|
||||
tab_width: 200
|
||||
<Row@Label>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.2, 0.2, 0.2, 1
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
text_size: self.width, None
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
font_size: dp(20)
|
||||
<UILog>:
|
||||
viewclass: 'Row'
|
||||
scroll_y: 0
|
||||
effect_cls: "ScrollEffect"
|
||||
RecycleBoxLayout:
|
||||
default_size: None, dp(20)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: dp(3)
|
||||
''')
|
||||
|
||||
if __name__ == '__main__':
|
||||
loop = asyncio.get_event_loop()
|
||||
ui_app = FactorioManager
|
||||
loop.run_until_complete(main(ui_app))
|
||||
loop.close()
|
||||
143
Fill.py
@@ -3,7 +3,7 @@ import typing
|
||||
import collections
|
||||
import itertools
|
||||
|
||||
from BaseClasses import CollectionState, PlandoItem, Location
|
||||
from BaseClasses import CollectionState, PlandoItem, Location, MultiWorld
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import key_drop_data
|
||||
|
||||
@@ -12,7 +12,7 @@ class FillError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False,
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool, single_player_placement=False,
|
||||
lock=False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
@@ -68,7 +68,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
||||
def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_locations=None):
|
||||
# If not passed in, then get a shuffled list of locations to fill in
|
||||
if not fill_locations:
|
||||
fill_locations = world.get_unfilled_locations()
|
||||
@@ -93,7 +93,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
# fill in gtower locations with trash first
|
||||
for player in world.alttp_player_ids:
|
||||
if not gftower_trash or not world.ganonstower_vanilla[player] or \
|
||||
world.logic[player] in {'owglitches', "nologic"}:
|
||||
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
||||
gtower_trash_count = 0
|
||||
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
|
||||
gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2,
|
||||
@@ -167,14 +167,14 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
|
||||
|
||||
def fast_fill(world, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
|
||||
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
world.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def flood_items(world):
|
||||
def flood_items(world: MultiWorld):
|
||||
# get items to distribute
|
||||
world.random.shuffle(world.itempool)
|
||||
itempool = world.itempool
|
||||
@@ -234,7 +234,7 @@ def flood_items(world):
|
||||
break
|
||||
|
||||
|
||||
def balance_multiworld_progression(world):
|
||||
def balance_multiworld_progression(world: MultiWorld):
|
||||
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
|
||||
if not balanceable_players:
|
||||
logging.info('Skipping multiworld progression balancing.')
|
||||
@@ -363,73 +363,76 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
|
||||
location_1.event, location_2.event = location_2.event, location_1.event
|
||||
|
||||
|
||||
def distribute_planned(world):
|
||||
def distribute_planned(world: MultiWorld):
|
||||
world_name_lookup = world.world_name_lookup
|
||||
|
||||
for player in world.player_ids:
|
||||
placement: PlandoItem
|
||||
for placement in world.plando_items[player]:
|
||||
if placement.location in key_drop_data:
|
||||
placement.warn(
|
||||
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||
continue
|
||||
item = ItemFactory(placement.item, player)
|
||||
target_world: int = placement.world
|
||||
if target_world is False or world.players == 1:
|
||||
target_world = player # in own world
|
||||
elif target_world is True: # in any other world
|
||||
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
||||
placement.location,
|
||||
set(world.player_ids) - {player}) if location.item_rule(item)
|
||||
)
|
||||
if not unfilled:
|
||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
||||
FillError)
|
||||
try:
|
||||
placement: PlandoItem
|
||||
for placement in world.plando_items[player]:
|
||||
if placement.location in key_drop_data:
|
||||
placement.warn(
|
||||
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||
continue
|
||||
item = ItemFactory(placement.item, player)
|
||||
target_world: int = placement.world
|
||||
if target_world is False or world.players == 1:
|
||||
target_world = player # in own world
|
||||
elif target_world is True: # in any other world
|
||||
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
||||
placement.location,
|
||||
set(world.player_ids) - {player}) if location.item_rule(item)
|
||||
)
|
||||
if not unfilled:
|
||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
||||
FillError)
|
||||
continue
|
||||
|
||||
target_world = world.random.choice(unfilled).player
|
||||
|
||||
elif target_world is None: # any random world
|
||||
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
||||
placement.location,
|
||||
set(world.player_ids)) if location.item_rule(item)
|
||||
)
|
||||
if not unfilled:
|
||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
||||
FillError)
|
||||
continue
|
||||
|
||||
target_world = world.random.choice(unfilled).player
|
||||
|
||||
elif type(target_world) == int: # target world by player id
|
||||
if target_world not in range(1, world.players + 1):
|
||||
placement.failed(
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
||||
ValueError)
|
||||
continue
|
||||
else: # find world by name
|
||||
if target_world not in world_name_lookup:
|
||||
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||
ValueError)
|
||||
continue
|
||||
target_world = world_name_lookup[target_world]
|
||||
|
||||
location = world.get_location(placement.location, target_world)
|
||||
if location.item:
|
||||
placement.failed(f"Cannot place item into already filled location {location}.")
|
||||
continue
|
||||
|
||||
target_world = world.random.choice(unfilled).player
|
||||
|
||||
elif target_world is None: # any random world
|
||||
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
||||
placement.location,
|
||||
set(world.player_ids)) if location.item_rule(item)
|
||||
)
|
||||
if not unfilled:
|
||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
||||
FillError)
|
||||
if location.can_fill(world.state, item, False):
|
||||
world.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
else:
|
||||
placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
|
||||
continue
|
||||
|
||||
target_world = world.random.choice(unfilled).player
|
||||
|
||||
elif type(target_world) == int: # target world by player id
|
||||
if target_world not in range(1, world.players + 1):
|
||||
placement.failed(
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
||||
ValueError)
|
||||
continue
|
||||
else: # find world by name
|
||||
if target_world not in world_name_lookup:
|
||||
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||
ValueError)
|
||||
continue
|
||||
target_world = world_name_lookup[target_world]
|
||||
|
||||
location = world.get_location(placement.location, target_world)
|
||||
if location.item:
|
||||
placement.failed(f"Cannot place item into already filled location {location}.")
|
||||
continue
|
||||
|
||||
if location.can_fill(world.state, item, False):
|
||||
world.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
else:
|
||||
placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
|
||||
continue
|
||||
|
||||
if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
|
||||
try:
|
||||
world.itempool.remove(item)
|
||||
except ValueError:
|
||||
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
|
||||
if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
|
||||
try:
|
||||
world.itempool.remove(item)
|
||||
except ValueError:
|
||||
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
|
||||
except Exception as e:
|
||||
raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e
|
||||
|
||||
2
Gui.py
@@ -468,7 +468,7 @@ def guiMain(args=None):
|
||||
if shopWitchShuffleVar.get():
|
||||
guiargs.shop_shuffle += "w"
|
||||
if shopPoolShuffleVar.get():
|
||||
guiargs.shop_shuffle_slots = 30
|
||||
guiargs.shop_item_slots = 30
|
||||
guiargs.shuffle_prizes = {"none": "",
|
||||
"bonk": "b",
|
||||
"general": "g",
|
||||
|
||||
@@ -5,10 +5,8 @@ import tkinter as tk
|
||||
from Utils import local_path
|
||||
|
||||
def set_icon(window):
|
||||
er16 = tk.PhotoImage(file=local_path('data', 'ER16.gif'))
|
||||
er32 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
|
||||
er48 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
|
||||
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
|
||||
logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
|
||||
window.tk.call('wm', 'iconphoto', window._w, logo)
|
||||
|
||||
# Although tkinter is intended to be thread safe, there are many reports of issues
|
||||
# some which may be platform specific, or depend on if the TCL library was compiled without
|
||||
|
||||
@@ -18,7 +18,7 @@ class AdjusterWorld(object):
|
||||
def __init__(self, sprite_pool):
|
||||
import random
|
||||
self.sprite_pool = {1: sprite_pool}
|
||||
self.rom_seeds = {1: random}
|
||||
self.slot_seeds = {1: random}
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
@@ -153,8 +153,8 @@ def adjust(args):
|
||||
|
||||
|
||||
def adjustGUI():
|
||||
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \
|
||||
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
|
||||
from tkinter import Tk, LEFT, BOTTOM, TOP, \
|
||||
StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
|
||||
from Gui import get_rom_options_frame, get_rom_frame
|
||||
from GuiUtils import set_icon
|
||||
from argparse import Namespace
|
||||
|
||||
111
LttPClient.py
@@ -1,18 +1,13 @@
|
||||
import argparse
|
||||
import atexit
|
||||
import time
|
||||
import functools
|
||||
import webbrowser
|
||||
import multiprocessing
|
||||
import socket
|
||||
import os
|
||||
import subprocess
|
||||
import base64
|
||||
import shutil
|
||||
from json import loads, dumps
|
||||
|
||||
from random import randrange
|
||||
|
||||
from Utils import get_item_name_from_id
|
||||
|
||||
exit_func = atexit.register(input, "Press enter to close.")
|
||||
@@ -73,7 +68,6 @@ class Context(CommonContext):
|
||||
self.snes_reconnect_address = None
|
||||
self.snes_recv_queue = asyncio.Queue()
|
||||
self.snes_request_lock = asyncio.Lock()
|
||||
self.is_sd2snes = False
|
||||
self.snes_write_buffer = []
|
||||
|
||||
self.awaiting_rom = False
|
||||
@@ -102,7 +96,7 @@ class Context(CommonContext):
|
||||
self.auth = self.rom
|
||||
auth = base64.b64encode(self.rom).decode()
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': auth, 'version': Utils._version_tuple,
|
||||
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
|
||||
'tags': get_tags(self),
|
||||
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
|
||||
}])
|
||||
@@ -143,8 +137,6 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
||||
|
||||
location_shop_order = [name for name, info in
|
||||
Shops.shop_table.items()] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order
|
||||
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
||||
|
||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
@@ -415,26 +407,30 @@ class SNESState(enum.IntEnum):
|
||||
SNES_ATTACHED = 3
|
||||
|
||||
|
||||
def launch_qusb2snes(ctx: Context):
|
||||
qusb2snes_path = Utils.get_options()["lttp_options"]["qusb2snes"]
|
||||
def launch_sni(ctx: Context):
|
||||
sni_path = Utils.get_options()["lttp_options"]["sni"]
|
||||
|
||||
if not os.path.isfile(qusb2snes_path):
|
||||
qusb2snes_path = Utils.local_path(qusb2snes_path)
|
||||
if not os.path.isdir(sni_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):
|
||||
logger.info(f"Attempting to start {qusb2snes_path}")
|
||||
if os.path.isfile(sni_path):
|
||||
logger.info(f"Attempting to start {sni_path}")
|
||||
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:
|
||||
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")
|
||||
|
||||
|
||||
async def _snes_connect(ctx: Context, address: str):
|
||||
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()
|
||||
succesful = False
|
||||
while not succesful:
|
||||
@@ -446,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
|
||||
if problem not in seen_problems:
|
||||
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:
|
||||
# this is the first problem. Let's try launching QUsb2snes if it isn't already running
|
||||
launch_qusb2snes(ctx)
|
||||
# this is the first problem. Let's try launching SNI if it isn't already running
|
||||
launch_sni(ctx)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
@@ -469,7 +465,7 @@ async def get_snes_devices(ctx: Context):
|
||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||
|
||||
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:
|
||||
await asyncio.sleep(1)
|
||||
await socket.send(dumps(DeviceList_Request))
|
||||
@@ -517,17 +513,6 @@ async def snes_connect(ctx: Context, address):
|
||||
await ctx.snes_socket.send(dumps(Attach_Request))
|
||||
ctx.snes_state = SNESState.SNES_ATTACHED
|
||||
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
|
||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
|
||||
@@ -621,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)))
|
||||
if len(data):
|
||||
logger.error(str(data))
|
||||
logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
|
||||
'Try un-selecting and re-selecting the SNES Device.')
|
||||
logger.warning('Communication Failure with SNI')
|
||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
return None
|
||||
@@ -641,45 +625,16 @@ async def snes_write(ctx: Context, write_list):
|
||||
return False
|
||||
|
||||
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||
|
||||
if ctx.is_sd2snes:
|
||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
||||
|
||||
try:
|
||||
for address, data in write_list:
|
||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
||||
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:
|
||||
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(cmd)
|
||||
await ctx.snes_socket.send(data)
|
||||
else:
|
||||
logger.warning(f"Could not send data to SNES: {cmd}")
|
||||
except websockets.ConnectionClosed:
|
||||
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
|
||||
logger.warning(f"Could not send data to SNES: {data}")
|
||||
except websockets.ConnectionClosed:
|
||||
return False
|
||||
|
||||
return True
|
||||
finally:
|
||||
@@ -709,9 +664,6 @@ def get_tags(ctx: Context):
|
||||
return tags
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def track_locations(ctx: Context, roomid, roomdata):
|
||||
new_locations = []
|
||||
|
||||
@@ -723,7 +675,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
|
||||
try:
|
||||
if roomid in location_shop_ids:
|
||||
misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5)
|
||||
misc_data = await snes_read(ctx, SHOP_ADDR, (len(Shops.shop_table) * 3) + 5)
|
||||
for cnt, b in enumerate(misc_data):
|
||||
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
|
||||
new_check(Shops.SHOP_ID_START + cnt)
|
||||
@@ -862,10 +814,11 @@ async def game_watcher(ctx: Context):
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
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'),
|
||||
ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received)))
|
||||
recv_index += 1
|
||||
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
|
||||
|
||||
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_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
||||
@@ -896,7 +849,7 @@ async def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
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('--password', default=None, help='Password of the multiworld host.')
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
@@ -919,14 +872,12 @@ async def main():
|
||||
logging.exception(e)
|
||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||
|
||||
port = None
|
||||
|
||||
ctx = Context(args.snes, args.connect, args.password, args.founditems)
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
|
||||
if ctx.server_task is None:
|
||||
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")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
245
Main.py
@@ -1,4 +1,3 @@
|
||||
import copy
|
||||
from itertools import zip_longest
|
||||
import logging
|
||||
import os
|
||||
@@ -21,15 +20,9 @@ from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
||||
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
|
||||
from worlds.hk import gen_hollow
|
||||
from worlds.hk import create_regions as hk_create_regions
|
||||
from worlds.factorio import gen_factorio, factorio_create_regions
|
||||
from worlds.factorio.Mod import generate_mod
|
||||
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
|
||||
from worlds.minecraft.Regions import minecraft_create_regions
|
||||
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
|
||||
from worlds.generic.Rules import locality_rules
|
||||
from worlds import Games, lookup_any_item_name_to_id
|
||||
from worlds import Games, lookup_any_item_name_to_id, AutoWorld
|
||||
import Patch
|
||||
|
||||
seeddigits = 20
|
||||
@@ -79,7 +72,7 @@ def main(args, seed=None):
|
||||
world.progressive = args.progressive.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.local_items = args.local_items.copy()
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
world.algorithm = args.algorithm
|
||||
world.shuffleganon = args.shuffleganon
|
||||
world.custom = args.custom
|
||||
@@ -94,12 +87,6 @@ def main(args, seed=None):
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.crystals_needed_for_ganon = {
|
||||
player: world.random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(
|
||||
args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
||||
world.crystals_needed_for_gt = {
|
||||
player: world.random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player])
|
||||
for player in range(1, world.players + 1)}
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.enemy_shuffle.copy()
|
||||
@@ -121,7 +108,6 @@ def main(args, seed=None):
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
world.shop_shuffle = args.shop_shuffle.copy()
|
||||
world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
|
||||
world.progression_balancing = args.progression_balancing.copy()
|
||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
@@ -133,18 +119,13 @@ def main(args, seed=None):
|
||||
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
setattr(world, hk_option, getattr(args, hk_option, {}))
|
||||
for factorio_option in Options.factorio_options:
|
||||
setattr(world, factorio_option, getattr(args, factorio_option, {}))
|
||||
for minecraft_option in Options.minecraft_options:
|
||||
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
|
||||
world.set_options(args)
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
|
||||
world.slot_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in
|
||||
range(1, world.players + 1)}
|
||||
|
||||
for player in range(1, world.players+1):
|
||||
for player in range(1, world.players + 1):
|
||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in world.shuffle[player]:
|
||||
@@ -154,7 +135,8 @@ def main(args, seed=None):
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("group-") or args.race:
|
||||
# renamed from team to group to not confuse with existing team name use
|
||||
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
world.er_seeds[player] = get_same_seed(world, (
|
||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
@@ -162,6 +144,10 @@ def main(args, seed=None):
|
||||
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info("Found World Types:")
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
logger.info(f" {name:30} {cls}")
|
||||
|
||||
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||
world.teams = len(parsed_names)
|
||||
for i, team in enumerate(parsed_names, 1):
|
||||
@@ -171,14 +157,15 @@ def main(args, seed=None):
|
||||
world.player_names[player].append(name)
|
||||
|
||||
logger.info('')
|
||||
for player in world.alttp_player_ids:
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
|
||||
item.game = world.game[player]
|
||||
world.push_precollected(item)
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
|
||||
# enforce pre-defined local items.
|
||||
@@ -205,26 +192,23 @@ def main(args, seed=None):
|
||||
world.non_local_items[player] -= item_name_groups['Pendants']
|
||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||
|
||||
for player in world.hk_player_ids:
|
||||
hk_create_regions(world, player)
|
||||
|
||||
for player in world.factorio_player_ids:
|
||||
factorio_create_regions(world, player)
|
||||
|
||||
for player in world.minecraft_player_ids:
|
||||
minecraft_create_regions(world, player)
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
if world.open_pyramid[player] == 'goal':
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
|
||||
'localganontriforcehunt', 'ganonpedestal'}
|
||||
elif world.open_pyramid[player] == 'auto':
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
|
||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
|
||||
'localganontriforcehunt', 'ganonpedestal'} and \
|
||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull',
|
||||
'dungeonscrossed'} or not world.shuffle_ganon)
|
||||
else:
|
||||
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
|
||||
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(
|
||||
world.open_pyramid[player], 'auto')
|
||||
|
||||
|
||||
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
||||
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player],
|
||||
world.triforce_pieces_required[player])
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
@@ -264,17 +248,12 @@ def main(args, seed=None):
|
||||
for player in world.player_ids:
|
||||
locality_rules(world, player)
|
||||
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
set_rules(world, player)
|
||||
|
||||
for player in world.hk_player_ids:
|
||||
gen_hollow(world, player)
|
||||
|
||||
for player in world.factorio_player_ids:
|
||||
gen_factorio(world, player)
|
||||
|
||||
for player in world.minecraft_player_ids:
|
||||
gen_minecraft(world, player)
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
|
||||
logger.info("Running Item Plando")
|
||||
|
||||
@@ -336,13 +315,13 @@ def main(args, seed=None):
|
||||
|
||||
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||
|
||||
palettes_options={}
|
||||
palettes_options['dungeon']=args.uw_palettes[player]
|
||||
palettes_options['overworld']=args.ow_palettes[player]
|
||||
palettes_options['hud']=args.hud_palettes[player]
|
||||
palettes_options['sword']=args.sword_palettes[player]
|
||||
palettes_options['shield']=args.shield_palettes[player]
|
||||
palettes_options['link']=args.link_palettes[player]
|
||||
palettes_options = {}
|
||||
palettes_options['dungeon'] = args.uw_palettes[player]
|
||||
palettes_options['overworld'] = args.ow_palettes[player]
|
||||
palettes_options['hud'] = args.hud_palettes[player]
|
||||
palettes_options['sword'] = args.sword_palettes[player]
|
||||
palettes_options['shield'] = args.shield_palettes[player]
|
||||
palettes_options['link'] = args.link_palettes[player]
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
||||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||
@@ -358,8 +337,8 @@ def main(args, seed=None):
|
||||
world.bigkeyshuffle[player]].count(True) == 1:
|
||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
|
||||
'-compassshuffle' if world.compassshuffle[player] else \
|
||||
'-universal_keys' if world.keyshuffle[player] == "universal" else \
|
||||
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
'-universal_keys' if world.keyshuffle[player] == "universal" else \
|
||||
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||
@@ -371,59 +350,59 @@ def main(args, seed=None):
|
||||
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
|
||||
if world.player_names[player][team] != 'Player%d' % player else ''
|
||||
outfilestuffs = {
|
||||
"logic": world.logic[player], # 0
|
||||
"difficulty": world.difficulty[player], # 1
|
||||
"item_functionality": world.item_functionality[player], # 2
|
||||
"mode": world.mode[player], # 3
|
||||
"goal": world.goal[player], # 4
|
||||
"timer": str(world.timer[player]), # 5
|
||||
"shuffle": world.shuffle[player], # 6
|
||||
"algorithm": world.algorithm, # 7
|
||||
"mscb": mcsb_name, # 8
|
||||
"retro": world.retro[player], # 9
|
||||
"progressive": world.progressive, # A
|
||||
"hints": 'True' if world.hints[player] else 'False' # B
|
||||
"logic": world.logic[player], # 0
|
||||
"difficulty": world.difficulty[player], # 1
|
||||
"item_functionality": world.item_functionality[player], # 2
|
||||
"mode": world.mode[player], # 3
|
||||
"goal": world.goal[player], # 4
|
||||
"timer": str(world.timer[player]), # 5
|
||||
"shuffle": world.shuffle[player], # 6
|
||||
"algorithm": world.algorithm, # 7
|
||||
"mscb": mcsb_name, # 8
|
||||
"retro": world.retro[player], # 9
|
||||
"progressive": world.progressive, # A
|
||||
"hints": 'True' if world.hints[player] else 'False' # B
|
||||
}
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B
|
||||
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B C
|
||||
# _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
|
||||
outfilestuffs["logic"], # 0
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B C
|
||||
# _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
|
||||
outfilestuffs["logic"], # 0
|
||||
|
||||
outfilestuffs["difficulty"], # 1
|
||||
outfilestuffs["item_functionality"], # 2
|
||||
outfilestuffs["mode"], # 3
|
||||
outfilestuffs["goal"], # 4
|
||||
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
|
||||
outfilestuffs["difficulty"], # 1
|
||||
outfilestuffs["item_functionality"], # 2
|
||||
outfilestuffs["mode"], # 3
|
||||
outfilestuffs["goal"], # 4
|
||||
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
|
||||
|
||||
outfilestuffs["shuffle"], # 6
|
||||
outfilestuffs["algorithm"], # 7
|
||||
outfilestuffs["mscb"], # 8
|
||||
outfilestuffs["shuffle"], # 6
|
||||
outfilestuffs["algorithm"], # 7
|
||||
outfilestuffs["mscb"], # 8
|
||||
|
||||
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
|
||||
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
|
||||
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
|
||||
) if not args.outputname else ''
|
||||
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
|
||||
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
|
||||
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
|
||||
) if not args.outputname else ''
|
||||
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||
rom.write_to_file(rompath, hide_enemizer=True)
|
||||
if args.create_diff:
|
||||
Patch.create_patch_file(rompath, player=player, player_name = world.player_names[player][team])
|
||||
Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
|
||||
return player, team, bytes(rom.name)
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor()
|
||||
multidata_task = None
|
||||
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
|
||||
rom_futures = []
|
||||
mod_futures = []
|
||||
output_file_futures = []
|
||||
for team in range(world.teams):
|
||||
for player in world.alttp_player_ids:
|
||||
rom_futures.append(pool.submit(_gen_rom, team, player))
|
||||
for player in world.factorio_player_ids:
|
||||
mod_futures.append(pool.submit(generate_mod, world, player))
|
||||
for player in world.player_ids:
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
@@ -433,7 +412,8 @@ def main(args, seed=None):
|
||||
return get_entrance_to_region(entrance.parent_region)
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
er_hint_data = {player: {} for player in range(1, world.players + 1) if
|
||||
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
from worlds.alttp.Regions import RegionType
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
@@ -459,7 +439,7 @@ def main(args, seed=None):
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'}\
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
@@ -471,8 +451,10 @@ def main(args, seed=None):
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]:
|
||||
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
||||
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
|
||||
world.retro[player]]:
|
||||
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||
region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
@@ -486,11 +468,9 @@ def main(args, seed=None):
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata(roms, mods):
|
||||
def write_multidata(roms, outputs):
|
||||
import base64
|
||||
import NetUtils
|
||||
for future in roms:
|
||||
@@ -501,31 +481,29 @@ def main(args, seed=None):
|
||||
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||
games = {}
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = (0, 0, 3)
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
||||
slot, team, rom_name in rom_names}
|
||||
precollected_items = {player: [] for player in range(1, world.players+1)}
|
||||
slot, team, rom_name in rom_names}
|
||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player].append(item.code)
|
||||
precollected_hints = {player: set() for player in range(1, world.players+1)}
|
||||
# for now special case Factorio visibility
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
# for now special case Factorio tech_tree_information
|
||||
sending_visible_players = set()
|
||||
for player in world.factorio_player_ids:
|
||||
if world.visibility[player]:
|
||||
if world.tech_tree_information[player].value == 2:
|
||||
sending_visible_players.add(player)
|
||||
|
||||
for i, team in enumerate(parsed_names):
|
||||
for player, name in enumerate(team, 1):
|
||||
if player not in world.alttp_player_ids:
|
||||
connect_names[name] = (i, player)
|
||||
for slot in world.hk_player_ids:
|
||||
slots_data = slot_data[slot] = {}
|
||||
for option_name in Options.hollow_knight_options:
|
||||
option = getattr(world, option_name)[slot]
|
||||
slots_data[option_name] = int(option.value)
|
||||
if world.hk_player_ids:
|
||||
for slot in world.hk_player_ids:
|
||||
slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot)
|
||||
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}
|
||||
for location in world.get_filled_locations():
|
||||
@@ -544,7 +522,7 @@ def main(args, seed=None):
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps({
|
||||
"slot_data" : slot_data,
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": parsed_names,
|
||||
"connect_names": connect_names,
|
||||
@@ -556,7 +534,7 @@ def main(args, seed=None):
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"precollected_hints": precollected_hints,
|
||||
"version": tuple(_version_tuple),
|
||||
"version": tuple(version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name
|
||||
@@ -565,10 +543,10 @@ def main(args, seed=None):
|
||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
for future in mods:
|
||||
future.result() # collect errors if they occured
|
||||
for future in outputs:
|
||||
future.result() # collect errors if they occured
|
||||
|
||||
multidata_task = pool.submit(write_multidata, rom_futures, mod_futures)
|
||||
multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
@@ -577,8 +555,6 @@ def main(args, seed=None):
|
||||
if multidata_task:
|
||||
multidata_task.result() # retrieve exception if one exists
|
||||
pool.shutdown() # wait for all queued tasks to complete
|
||||
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
|
||||
generate_mc_data(world, player)
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
@@ -632,7 +608,8 @@ def create_playthrough(world):
|
||||
to_delete = set()
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||
location.item.player)
|
||||
old_item = location.item
|
||||
location.item = None
|
||||
if world.can_beat_game(state_cache[num]):
|
||||
@@ -677,7 +654,8 @@ def create_playthrough(world):
|
||||
|
||||
collection_spheres.append(sphere)
|
||||
|
||||
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations))
|
||||
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
|
||||
len(sphere), len(required_locations))
|
||||
if not sphere:
|
||||
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
||||
|
||||
@@ -694,16 +672,25 @@ def create_playthrough(world):
|
||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||
return list(pathpairs)
|
||||
|
||||
world.spoiler.paths = dict()
|
||||
for player in range(1, world.players + 1):
|
||||
world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
|
||||
world.spoiler.paths = {}
|
||||
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
|
||||
for player in topology_worlds:
|
||||
world.spoiler.paths.update(
|
||||
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
||||
sphere if location.player == player})
|
||||
if player in world.alttp_player_ids:
|
||||
for path in dict(world.spoiler.paths).values():
|
||||
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state,
|
||||
world.get_region(
|
||||
'Big Bomb Shop',
|
||||
player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state,
|
||||
world.get_region(
|
||||
'Inverted Big Bomb Shop',
|
||||
player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||
|
||||
@@ -1,55 +1,48 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import importlib
|
||||
import pkg_resources
|
||||
|
||||
requirements_files = {'requirements.txt'}
|
||||
|
||||
if sys.version_info < (3, 8, 6):
|
||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||
|
||||
update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled
|
||||
update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
|
||||
|
||||
if not update_ran:
|
||||
for entry in os.scandir("worlds"):
|
||||
if entry.is_dir():
|
||||
req_file = os.path.join(entry.path, "requirements.txt")
|
||||
if os.path.exists(req_file):
|
||||
requirements_files.add(req_file)
|
||||
|
||||
|
||||
def update_command():
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade'])
|
||||
|
||||
|
||||
naming_specialties = {"PyYAML": "yaml", # PyYAML is imported as the name yaml
|
||||
"maseya-z3pr": "maseya",
|
||||
"factorio-rcon-py": "factorio_rcon"}
|
||||
for file in requirements_files:
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
|
||||
|
||||
|
||||
def update():
|
||||
global update_ran
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt')
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(os.path.dirname(__file__), 'requirements.txt')
|
||||
with open(path) as requirementsfile:
|
||||
for line in requirementsfile.readlines():
|
||||
module, remote_version = line.split(">=")
|
||||
module = naming_specialties.get(module, module)
|
||||
try:
|
||||
module = importlib.import_module(module)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Required python module {module} not found, press enter to install it')
|
||||
update_command()
|
||||
return
|
||||
else:
|
||||
if hasattr(module, "__version__"):
|
||||
module_version = module.__version__
|
||||
module = module.__name__ # also unloads the module to make it writable
|
||||
if type(module_version) == str:
|
||||
module_version = tuple(int(part.strip()) for part in module_version.split("."))
|
||||
remote_version = tuple(int(part.strip()) for part in remote_version.split("."))
|
||||
if module_version < remote_version:
|
||||
input(f'Required python module {module} is outdated ({module_version}<{remote_version}),'
|
||||
' press enter to upgrade it')
|
||||
update_command()
|
||||
return
|
||||
for req_file in requirements_files:
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(os.path.dirname(__file__), req_file)
|
||||
with open(path) as requirementsfile:
|
||||
requirements = pkg_resources.parse_requirements(requirementsfile)
|
||||
for requirement in requirements:
|
||||
requirement = str(requirement)
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
except pkg_resources.ResolutionError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
update_command()
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,6 +6,7 @@ import concurrent.futures
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
from shutil import which
|
||||
|
||||
|
||||
def feedback(text: str):
|
||||
@@ -45,7 +46,6 @@ if __name__ == "__main__":
|
||||
zip_multidata = multi_mystery_options["zip_multidata"]
|
||||
zip_format = multi_mystery_options["zip_format"]
|
||||
# zip_password = multi_mystery_options["zip_password"] not at this time
|
||||
player_name = multi_mystery_options["player_name"]
|
||||
meta_file_path = multi_mystery_options["meta_file_path"]
|
||||
weights_file_path = multi_mystery_options["weights_file_path"]
|
||||
pre_roll = multi_mystery_options["pre_roll"]
|
||||
@@ -76,9 +76,11 @@ if __name__ == "__main__":
|
||||
if os.path.exists("ArchipelagoMystery.exe"):
|
||||
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
|
||||
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:
|
||||
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)
|
||||
if os.path.exists(weights_file_path):
|
||||
@@ -124,15 +126,6 @@ if __name__ == "__main__":
|
||||
spoilername = f"AP_{seed_name}_Spoiler.txt"
|
||||
romfilename = ""
|
||||
|
||||
if player_name:
|
||||
for file in os.listdir(output_path):
|
||||
if player_name in file:
|
||||
import MultiClient
|
||||
import asyncio
|
||||
|
||||
asyncio.run(MultiClient.run_game(os.path.join(output_path, file)))
|
||||
break
|
||||
|
||||
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)):
|
||||
import zipfile
|
||||
|
||||
@@ -167,7 +160,7 @@ if __name__ == "__main__":
|
||||
def _handle_sfc_file(file: str):
|
||||
if zip_roms:
|
||||
pack_file(file)
|
||||
if zip_roms == 2 and player_name.lower() not in file.lower():
|
||||
if zip_roms == 2:
|
||||
remove_zipped_file(file)
|
||||
|
||||
|
||||
@@ -217,14 +210,15 @@ if __name__ == "__main__":
|
||||
if not args.disable_autohost:
|
||||
if os.path.exists(os.path.join(output_path, multidataname)):
|
||||
if os.path.exists("ArchipelagoServer.exe"):
|
||||
baseservercommand = "ArchipelagoServer.exe" # compiled windows
|
||||
baseservercommand = ["ArchipelagoServer.exe"] # compiled windows
|
||||
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:
|
||||
baseservercommand = f"py -{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.
|
||||
|
||||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
||||
subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)])
|
||||
except:
|
||||
import traceback
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
||||
lookup_any_location_id_to_name, lookup_any_location_name_to_id
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
_version_tuple, restricted_loads, Version
|
||||
version_tuple, restricted_loads, Version
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||
|
||||
colorama.init()
|
||||
@@ -39,6 +39,7 @@ all_items = frozenset(lookup_any_item_name_to_id)
|
||||
all_locations = frozenset(lookup_any_location_name_to_id)
|
||||
all_console_names = frozenset(all_items | all_locations)
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str] = []
|
||||
@@ -75,10 +76,10 @@ class Context(Node):
|
||||
self.save_filename = None
|
||||
self.saving = False
|
||||
self.player_names = {}
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.allow_forfeits = {}
|
||||
self.remote_items = set()
|
||||
self.locations:typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
|
||||
self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_password = server_password
|
||||
@@ -136,9 +137,9 @@ class Context(Node):
|
||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > Utils._version_tuple:
|
||||
if mdata_ver > Utils.version_tuple:
|
||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
||||
f"however this server is of version {Utils._version_tuple}")
|
||||
f"however this server is of version {Utils.version_tuple}")
|
||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||
self.minimum_client_versions = {}
|
||||
for player, version in clients_ver.items():
|
||||
@@ -166,7 +167,6 @@ class Context(Node):
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
@@ -174,7 +174,7 @@ class Context(Node):
|
||||
for key, value in server_options.items():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
@@ -200,7 +200,7 @@ class Context(Node):
|
||||
|
||||
return False
|
||||
|
||||
def _save(self, exit_save:bool=False) -> bool:
|
||||
def _save(self, exit_save: bool = False) -> bool:
|
||||
try:
|
||||
encoded_save = pickle.dumps(self.get_save())
|
||||
with open(self.save_filename, "wb") as f:
|
||||
@@ -244,7 +244,15 @@ class Context(Node):
|
||||
import atexit
|
||||
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:
|
||||
self.recheck_hints()
|
||||
d = {
|
||||
"connect_names": self.connect_names,
|
||||
"received_items": self.received_items,
|
||||
@@ -366,7 +374,8 @@ async def server(websocket, path, ctx: Context):
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logging.exception(e)
|
||||
finally:
|
||||
logging.info("Disconnected")
|
||||
if ctx.log_network:
|
||||
logging.info("Disconnected")
|
||||
await ctx.disconnect(client)
|
||||
|
||||
|
||||
@@ -374,12 +383,14 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
await ctx.send_msgs(client, [{
|
||||
'cmd': 'RoomInfo',
|
||||
'password': ctx.password is not None,
|
||||
'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client
|
||||
in ctx.endpoints if client.auth],
|
||||
'players': [
|
||||
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
|
||||
client.name) for client
|
||||
in ctx.endpoints if client.auth],
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ctx.tags,
|
||||
'version': Utils._version_tuple,
|
||||
'version': Utils.version_tuple,
|
||||
'forfeit_mode': ctx.forfeit_mode,
|
||||
'remaining_mode': ctx.remaining_mode,
|
||||
'hint_cost': ctx.hint_cost,
|
||||
@@ -403,9 +414,11 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
async def on_client_left(ctx: Context, client: Client):
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||
ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
|
||||
ctx.notify_all(
|
||||
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
@@ -447,7 +460,7 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
|
||||
|
||||
def send_new_items(ctx: Context):
|
||||
for client in ctx.endpoints:
|
||||
if client.auth: # can't send to disconnected client
|
||||
if client.auth: # can't send to disconnected client
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if len(items) > client.send_index:
|
||||
asyncio.create_task(ctx.send_msgs(client, [{
|
||||
@@ -504,7 +517,6 @@ def notify_team(ctx: Context, team: int, text: str):
|
||||
ctx.broadcast_team(team, [['Print', {"text": text}]])
|
||||
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
|
||||
hints = []
|
||||
seeked_item_id = lookup_any_item_name_to_id[item]
|
||||
@@ -520,7 +532,6 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
|
||||
|
||||
|
||||
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||
|
||||
seeked_location: int = Regions.lookup_name_to_id[location]
|
||||
item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
|
||||
if item_id:
|
||||
@@ -540,6 +551,7 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
text += f" at {hint.entrance}"
|
||||
return text + (". (found)" if hint.found else ".")
|
||||
|
||||
|
||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
parts = []
|
||||
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
||||
@@ -557,9 +569,12 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
NetUtils.add_json_text(parts, ")")
|
||||
|
||||
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[str, bool, str]:
|
||||
|
||||
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str] = all_console_names) -> typing.Tuple[
|
||||
str, bool, str]:
|
||||
picks = fuzzy_process.extract(input_text, possible_answers, limit=2)
|
||||
if len(picks) > 1:
|
||||
dif = picks[0][1] - picks[1][1]
|
||||
@@ -684,11 +699,12 @@ class CommonCommandProcessor(CommandProcessor):
|
||||
"""List all current options. Warning: lists password."""
|
||||
self.output("Current options:")
|
||||
for option in self.ctx.simple_options:
|
||||
if option == "server_password" and self.marker == "!": #Do not display the server password to the client.
|
||||
self.output(f"Option server_password is set to {('*' * random.randint(4,16))}")
|
||||
if option == "server_password" and self.marker == "!": # Do not display the server password to the client.
|
||||
self.output(f"Option server_password is set to {('*' * random.randint(4, 16))}")
|
||||
else:
|
||||
self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
|
||||
|
||||
|
||||
class ClientMessageProcessor(CommonCommandProcessor):
|
||||
marker = "!"
|
||||
|
||||
@@ -715,11 +731,14 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
"""Allow remote administration of the multiworld server"""
|
||||
|
||||
output = f"!admin {command}"
|
||||
if output.lower().startswith("!admin login"): # disallow others from seeing the supplied password, whether or not it is correct.
|
||||
if output.lower().startswith(
|
||||
"!admin login"): # disallow others from seeing the supplied password, whether or not it is correct.
|
||||
output = f"!admin login {('*' * random.randint(4, 16))}"
|
||||
elif output.lower().startswith("!admin /option server_password"): # disallow others from knowing what the new remote administration password is.
|
||||
elif output.lower().startswith(
|
||||
"!admin /option server_password"): # disallow others from knowing what the new remote administration password is.
|
||||
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
||||
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output) # Otherwise notify the others what is happening.
|
||||
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team,
|
||||
self.client.slot) + ': ' + output) # Otherwise notify the others what is happening.
|
||||
|
||||
if not self.ctx.server_password:
|
||||
self.output("Sorry, Remote administration is disabled")
|
||||
@@ -727,7 +746,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
if not command:
|
||||
if self.is_authenticated():
|
||||
self.output("Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.")
|
||||
self.output(
|
||||
"Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.")
|
||||
else:
|
||||
self.output("Usage: !admin login [password]")
|
||||
return True
|
||||
@@ -810,7 +830,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
"Sorry, !remaining requires you to have beaten the game on this server")
|
||||
return False
|
||||
|
||||
|
||||
def _cmd_missing(self) -> bool:
|
||||
"""List all missing location checks from the server's perspective"""
|
||||
|
||||
@@ -850,7 +869,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if usable:
|
||||
new_item = NetworkItem(Items.item_table[item_name][2], -1, self.client.slot)
|
||||
get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item)
|
||||
self.ctx.notify_all('Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, self.client.slot))
|
||||
self.ctx.notify_all(
|
||||
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
|
||||
self.client.slot))
|
||||
send_new_items(self.ctx)
|
||||
return True
|
||||
else:
|
||||
@@ -959,7 +980,7 @@ def get_client_points(ctx: Context, client: Client) -> int:
|
||||
|
||||
async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
try:
|
||||
cmd:str = args["cmd"]
|
||||
cmd: str = args["cmd"]
|
||||
except:
|
||||
logging.exception(f"Could not get command from {args}")
|
||||
raise
|
||||
@@ -1011,10 +1032,10 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
errors.add('IncompatibleVersion')
|
||||
|
||||
# only exact version match allowed
|
||||
if ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||
errors.add('IncompatibleVersion')
|
||||
if errors:
|
||||
logging.info(f"A client connection was refused due to: {errors}")
|
||||
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)}])
|
||||
else:
|
||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||
@@ -1045,7 +1066,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
client.send_index = len(items)
|
||||
await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0,
|
||||
await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0,
|
||||
"items": items}])
|
||||
|
||||
elif cmd == 'LocationChecks':
|
||||
@@ -1067,11 +1088,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
|
||||
if cmd == 'Say':
|
||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
||||
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text" : 'Say'}])
|
||||
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'Say'}])
|
||||
return
|
||||
|
||||
client.messageprocessor(args["text"])
|
||||
|
||||
|
||||
def update_client_status(ctx: Context, client: Client, new_status: ClientStatus):
|
||||
current = ctx.client_game_state[client.team, client.slot]
|
||||
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
|
||||
@@ -1083,6 +1105,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
||||
|
||||
ctx.client_game_state[client.team, client.slot] = new_status
|
||||
|
||||
|
||||
class ServerCommandProcessor(CommonCommandProcessor):
|
||||
def __init__(self, ctx: Context):
|
||||
self.ctx = ctx
|
||||
@@ -1190,7 +1213,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
self.ctx.allow_forfeits[(team, slot)] = False
|
||||
self.output(f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.")
|
||||
self.output(
|
||||
f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.")
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
|
||||
@@ -1270,6 +1294,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
f"{', '.join(known)}")
|
||||
return False
|
||||
|
||||
|
||||
async def console(ctx: Context):
|
||||
session = prompt_toolkit.PromptSession()
|
||||
while ctx.running:
|
||||
@@ -1356,7 +1381,7 @@ async def auto_shutdown(ctx, to_cancel=None):
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
logging.basicConfig(force = True,
|
||||
logging.basicConfig(force=True,
|
||||
format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
|
||||
248
Mystery.py
@@ -9,11 +9,12 @@ from collections import Counter
|
||||
import string
|
||||
|
||||
import ModuleUpdate
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.generic import PlandoItem, PlandoConnection
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import parse_yaml
|
||||
from Utils import parse_yaml, version_tuple, __version__, tuplize_version
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from Main import get_seed, seeddigits
|
||||
@@ -23,7 +24,9 @@ from worlds.alttp.Items import item_name_groups, item_table
|
||||
from worlds.alttp import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.alttp.Regions import location_table, key_drop_data
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
categories = set(AutoWorldRegister.world_types)
|
||||
|
||||
def mystery_argparse():
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
@@ -61,9 +64,11 @@ def mystery_argparse():
|
||||
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||
return args
|
||||
|
||||
|
||||
def get_seed_name(random):
|
||||
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
|
||||
|
||||
|
||||
def main(args=None, callback=ERmain):
|
||||
if not args:
|
||||
args = mystery_argparse()
|
||||
@@ -79,14 +84,14 @@ def main(args=None, callback=ERmain):
|
||||
weights_cache = {}
|
||||
if args.weights:
|
||||
try:
|
||||
weights_cache[args.weights] = get_weights(args.weights)
|
||||
weights_cache[args.weights] = read_weights_yaml(args.weights)
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
|
||||
print(f"Weights: {args.weights} >> "
|
||||
f"{get_choice('description', weights_cache[args.weights], 'No description specified')}")
|
||||
if args.meta:
|
||||
try:
|
||||
weights_cache[args.meta] = get_weights(args.meta)
|
||||
weights_cache[args.meta] = read_weights_yaml(args.meta)
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
|
||||
meta_weights = weights_cache[args.meta]
|
||||
@@ -99,7 +104,7 @@ def main(args=None, callback=ERmain):
|
||||
if path:
|
||||
try:
|
||||
if path not in weights_cache:
|
||||
weights_cache[path] = get_weights(path)
|
||||
weights_cache[path] = read_weights_yaml(path)
|
||||
print(f"P{player} Weights: {path} >> "
|
||||
f"{get_choice('description', weights_cache[path], 'No description specified')}")
|
||||
|
||||
@@ -254,7 +259,7 @@ def main(args=None, callback=ERmain):
|
||||
callback(erargs, seed)
|
||||
|
||||
|
||||
def get_weights(path):
|
||||
def read_weights_yaml(path):
|
||||
try:
|
||||
if urllib.parse.urlparse(path).scheme:
|
||||
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
|
||||
@@ -342,19 +347,6 @@ goals = {
|
||||
'ice_rod_hunt': 'icerodhunt',
|
||||
}
|
||||
|
||||
# remove sometime before 1.0.0, warn before
|
||||
legacy_boss_shuffle_options = {
|
||||
# legacy, will go away:
|
||||
'simple': 'basic',
|
||||
'random': 'full',
|
||||
'normal': 'full'
|
||||
}
|
||||
|
||||
legacy_goals = {
|
||||
'dungeons': 'bosses',
|
||||
'fast_ganon': 'crystals',
|
||||
}
|
||||
|
||||
|
||||
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
@@ -382,13 +374,12 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
try:
|
||||
if roll_percentage(option_set["percentage"]):
|
||||
logging.debug(f"Linked option {option_set['name']} triggered.")
|
||||
if "options" in option_set:
|
||||
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
|
||||
if "rom_options" in option_set:
|
||||
rom_weights = weights.get("rom", dict())
|
||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
|
||||
option_set["name"])
|
||||
weights["rom"] = rom_weights
|
||||
new_options = option_set["options"]
|
||||
for category_name, category_options in new_options.items():
|
||||
currently_targeted_weights = weights
|
||||
if category_name:
|
||||
currently_targeted_weights = currently_targeted_weights[category_name]
|
||||
update_weights(currently_targeted_weights, category_options, "Linked", option_set["name"])
|
||||
else:
|
||||
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||
except Exception as e:
|
||||
@@ -402,35 +393,32 @@ def roll_triggers(weights: dict) -> dict:
|
||||
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
|
||||
for i, option_set in enumerate(weights["triggers"]):
|
||||
try:
|
||||
currently_targeted_weights = weights
|
||||
category = option_set.get("option_category", None)
|
||||
if category:
|
||||
currently_targeted_weights = currently_targeted_weights[category]
|
||||
key = get_choice("option_name", option_set)
|
||||
if key not in weights:
|
||||
if key not in currently_targeted_weights:
|
||||
logging.warning(f'Specified option name {option_set["option_name"]} did not '
|
||||
f'match with a root option. '
|
||||
f'This is probably in error.')
|
||||
trigger_result = get_choice("option_result", option_set)
|
||||
result = get_choice(key, weights)
|
||||
result = get_choice(key, currently_targeted_weights)
|
||||
currently_targeted_weights[key] = result
|
||||
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
||||
if "options" in option_set:
|
||||
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
|
||||
for category_name, category_options in option_set["options"].items():
|
||||
currently_targeted_weights = weights
|
||||
if category_name:
|
||||
currently_targeted_weights = currently_targeted_weights[category_name]
|
||||
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
|
||||
|
||||
if "rom_options" in option_set:
|
||||
rom_weights = weights.get("rom", dict())
|
||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
|
||||
option_set["option_name"])
|
||||
weights["rom"] = rom_weights
|
||||
weights[key] = result
|
||||
except Exception as e:
|
||||
raise ValueError(f"Your trigger number {i+1} is destroyed. "
|
||||
raise ValueError(f"Your trigger number {i + 1} is destroyed. "
|
||||
f"Please fix your triggers.") from e
|
||||
return weights
|
||||
|
||||
|
||||
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
||||
if boss_shuffle in legacy_boss_shuffle_options:
|
||||
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
||||
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
|
||||
f"please use {new_boss_shuffle} instead")
|
||||
return new_boss_shuffle
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
elif "bosses" in plando_options:
|
||||
@@ -438,10 +426,6 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
for boss in options:
|
||||
if boss in legacy_boss_shuffle_options:
|
||||
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
||||
logging.warning(f"Boss shuffle {boss} is deprecated, "
|
||||
f"please use {remainder_shuffle} instead")
|
||||
if boss in boss_shuffle_options:
|
||||
remainder_shuffle = boss_shuffle_options[boss]
|
||||
elif "-" in boss:
|
||||
@@ -507,14 +491,34 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
if "triggers" in weights:
|
||||
weights = roll_triggers(weights)
|
||||
|
||||
requirements = weights.get("requires", {})
|
||||
if requirements:
|
||||
version = requirements.get("version", __version__)
|
||||
if tuplize_version(version) > version_tuple:
|
||||
raise Exception(f"Settings reports required version of generator is at least {version}, "
|
||||
f"however generator is of version {__version__}")
|
||||
required_plando_options = requirements.get("plando", "")
|
||||
if required_plando_options:
|
||||
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
|
||||
required_plando_options -= plando_options
|
||||
if required_plando_options:
|
||||
if len(required_plando_options) == 1:
|
||||
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
|
||||
f"which is not enabled.")
|
||||
else:
|
||||
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
|
||||
f"which are not enabled.")
|
||||
|
||||
ret = argparse.Namespace()
|
||||
ret.name = get_choice('name', weights)
|
||||
ret.accessibility = get_choice('accessibility', weights)
|
||||
ret.progression_balancing = get_choice('progression_balancing', weights, True)
|
||||
ret.game = get_choice("game", weights, "A Link to the Past")
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in weights:
|
||||
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
||||
game_weights = weights[ret.game]
|
||||
ret.local_items = set()
|
||||
for item_name in weights.get('local_items', []):
|
||||
for item_name in game_weights.get('local_items', []):
|
||||
items = item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in lookup_any_item_name_to_id:
|
||||
@@ -523,7 +527,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
|
||||
|
||||
ret.non_local_items = set()
|
||||
for item_name in weights.get('non_local_items', []):
|
||||
for item_name in game_weights.get('non_local_items', []):
|
||||
items = item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in lookup_any_item_name_to_id:
|
||||
@@ -531,7 +535,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
inventoryweights = game_weights.get('start_inventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
@@ -541,29 +545,34 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
ret.startinventory = startitems
|
||||
ret.start_hints = set(weights.get('start_hints', []))
|
||||
ret.start_hints = set(game_weights.get('start_hints', []))
|
||||
|
||||
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, weights, plando_options)
|
||||
elif ret.game == "Hollow Knight":
|
||||
for option_name, option in Options.hollow_knight_options.items():
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
|
||||
elif ret.game == "Factorio":
|
||||
for option_name, option in Options.factorio_options.items():
|
||||
if option_name in weights:
|
||||
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
|
||||
setattr(ret, option_name, option.from_any(weights[option_name]))
|
||||
else:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||
else:
|
||||
setattr(ret, option_name, option(option.default))
|
||||
elif ret.game == "Minecraft":
|
||||
for option_name, option in Options.minecraft_options.items():
|
||||
if option_name in weights:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||
if ret.game in AutoWorldRegister.world_types:
|
||||
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
|
||||
if option_name in game_weights:
|
||||
try:
|
||||
if issubclass(option, Options.OptionDict):
|
||||
setattr(ret, option_name, option.from_any(game_weights[option_name]))
|
||||
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:
|
||||
setattr(ret, option_name, option(option.default))
|
||||
if ret.game == "Minecraft":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
if "connections" in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement, "both")
|
||||
))
|
||||
elif ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
else:
|
||||
raise Exception(f"Unsupported game {ret.game}")
|
||||
return ret
|
||||
@@ -571,11 +580,11 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
|
||||
logging.warning("Only NMG, OWG and No Logic supported")
|
||||
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")
|
||||
glitches_required = 'none'
|
||||
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
|
||||
'minor_glitches': 'minorglitches'}[
|
||||
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
|
||||
glitches_required]
|
||||
|
||||
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
|
||||
@@ -612,23 +621,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
goal = get_choice('goals', weights, 'ganon')
|
||||
|
||||
if goal in legacy_goals:
|
||||
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
|
||||
goal = legacy_goals[goal]
|
||||
ret.goal = goals[goal]
|
||||
|
||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||
# fast ganon + ganon at hole
|
||||
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
|
||||
|
||||
ret.crystals_gt = prefer_int(get_choice('tower_open', weights))
|
||||
|
||||
ret.crystals_ganon = prefer_int(get_choice('ganon_open', weights))
|
||||
|
||||
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
|
||||
|
||||
ret.triforce_pieces_required = int(get_choice('triforce_pieces_required', weights, 20))
|
||||
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90)
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
||||
|
||||
# sum a percentage to required
|
||||
if extra_pieces == 'percentage':
|
||||
@@ -636,7 +637,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||
# vanilla mode (specify how many pieces are)
|
||||
elif extra_pieces == 'available':
|
||||
ret.triforce_pieces_available = int(get_choice('triforce_pieces_available', weights, 30))
|
||||
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
|
||||
get_choice('triforce_pieces_available', weights, 30))
|
||||
# required pieces + fixed extra
|
||||
elif extra_pieces == 'extra':
|
||||
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
|
||||
@@ -644,11 +646,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
# change minimum to required pieces to avoid problems
|
||||
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
|
||||
shuffle_slots = get_choice('shop_shuffle_slots', weights, '0')
|
||||
if str(shuffle_slots).lower() == "random":
|
||||
ret.shop_shuffle_slots = random.randint(0, 30)
|
||||
else:
|
||||
ret.shop_shuffle_slots = int(shuffle_slots)
|
||||
|
||||
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
|
||||
if not ret.shop_shuffle:
|
||||
@@ -670,7 +667,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
||||
|
||||
|
||||
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
||||
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
||||
@@ -782,49 +778,43 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
get_choice("direction", placement, "both")
|
||||
))
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
|
||||
ret.sprite_pool = romweights['sprite_pool'] if 'sprite_pool' in romweights else []
|
||||
ret.sprite = get_choice('sprite', romweights, "Link")
|
||||
if 'random_sprite_on_event' in romweights:
|
||||
randomoneventweights = romweights['random_sprite_on_event']
|
||||
if get_choice('enabled', randomoneventweights, False):
|
||||
ret.sprite = 'randomon'
|
||||
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
|
||||
ret.sprite_pool = weights.get('sprite_pool', [])
|
||||
ret.sprite = get_choice('sprite', weights, "Link")
|
||||
if 'random_sprite_on_event' in weights:
|
||||
randomoneventweights = weights['random_sprite_on_event']
|
||||
if get_choice('enabled', randomoneventweights, False):
|
||||
ret.sprite = 'randomon'
|
||||
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
|
||||
|
||||
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
and 'sprite' in romweights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
|
||||
for key, value in romweights['sprite'].items():
|
||||
if key.startswith('random'):
|
||||
ret.sprite_pool += ['random'] * int(value)
|
||||
else:
|
||||
ret.sprite_pool += [key] * int(value)
|
||||
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
|
||||
for key, value in weights['sprite'].items():
|
||||
if key.startswith('random'):
|
||||
ret.sprite_pool += ['random'] * int(value)
|
||||
else:
|
||||
ret.sprite_pool += [key] * int(value)
|
||||
|
||||
ret.disablemusic = get_choice('disablemusic', romweights, False)
|
||||
ret.triforcehud = get_choice('triforcehud', romweights, 'hide_goal')
|
||||
ret.quickswap = get_choice('quickswap', romweights, True)
|
||||
ret.fastmenu = get_choice('menuspeed', romweights, "normal")
|
||||
ret.reduceflashing = get_choice('reduceflashing', romweights, False)
|
||||
ret.heartcolor = get_choice('heartcolor', romweights, "red")
|
||||
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights, "normal"))
|
||||
ret.ow_palettes = get_choice('ow_palettes', romweights, "default")
|
||||
ret.uw_palettes = get_choice('uw_palettes', romweights, "default")
|
||||
ret.hud_palettes = get_choice('hud_palettes', romweights, "default")
|
||||
ret.sword_palettes = get_choice('sword_palettes', romweights, "default")
|
||||
ret.shield_palettes = get_choice('shield_palettes', romweights, "default")
|
||||
ret.link_palettes = get_choice('link_palettes', romweights, "default")
|
||||
|
||||
else:
|
||||
ret.quickswap = True
|
||||
ret.sprite = "Link"
|
||||
ret.disablemusic = get_choice('disablemusic', weights, False)
|
||||
ret.triforcehud = get_choice('triforcehud', weights, 'hide_goal')
|
||||
ret.quickswap = get_choice('quickswap', weights, True)
|
||||
ret.fastmenu = get_choice('menuspeed', weights, "normal")
|
||||
ret.reduceflashing = get_choice('reduceflashing', weights, False)
|
||||
ret.heartcolor = get_choice('heartcolor', weights, "red")
|
||||
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', weights, "normal"))
|
||||
ret.ow_palettes = get_choice('ow_palettes', weights, "default")
|
||||
ret.uw_palettes = get_choice('uw_palettes', weights, "default")
|
||||
ret.hud_palettes = get_choice('hud_palettes', weights, "default")
|
||||
ret.sword_palettes = get_choice('sword_palettes', weights, "default")
|
||||
ret.shield_palettes = get_choice('shield_palettes', weights, "default")
|
||||
ret.link_palettes = get_choice('link_palettes', weights, "default")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -307,7 +307,9 @@ class Hint(typing.NamedTuple):
|
||||
else:
|
||||
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
|
||||
def local(self):
|
||||
|
||||
263
Options.py
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
import random
|
||||
|
||||
|
||||
class AssembleOptions(type):
|
||||
@@ -7,8 +8,9 @@ class AssembleOptions(type):
|
||||
options = attrs["options"] = {}
|
||||
name_lookup = attrs["name_lookup"] = {}
|
||||
for base in bases:
|
||||
options.update(base.options)
|
||||
name_lookup.update(name_lookup)
|
||||
if hasattr(base, "options"):
|
||||
options.update(base.options)
|
||||
name_lookup.update(name_lookup)
|
||||
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("option_")}
|
||||
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
||||
@@ -19,7 +21,6 @@ class AssembleOptions(type):
|
||||
name.startswith("alias_")})
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class Option(metaclass=AssembleOptions):
|
||||
value: int
|
||||
name_lookup: typing.Dict[int, str]
|
||||
@@ -88,6 +89,8 @@ class Toggle(Option):
|
||||
def get_option_name(self):
|
||||
return bool(self.value)
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
default = 1
|
||||
|
||||
class Choice(Option):
|
||||
def __init__(self, value: int):
|
||||
@@ -109,6 +112,44 @@ class Choice(Option):
|
||||
return cls.from_text(str(data))
|
||||
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
|
||||
def __init__(self, value: int):
|
||||
if value < self.range_start:
|
||||
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
||||
elif value > self.range_end:
|
||||
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Range:
|
||||
text = text.lower()
|
||||
if text.startswith("random"):
|
||||
if text == "random-low":
|
||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_start), 0)))
|
||||
elif text == "random-high":
|
||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
|
||||
elif text == "random-middle":
|
||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
|
||||
else:
|
||||
return cls(random.randint(cls.range_start, cls.range_end))
|
||||
return cls(int(text))
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> Range:
|
||||
if type(data) == int:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self):
|
||||
return str(self.value)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
|
||||
class OptionNameSet(Option):
|
||||
default = frozenset()
|
||||
|
||||
@@ -139,226 +180,28 @@ class OptionDict(Option):
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
option_no_glitches = 0
|
||||
option_minor_glitches = 1
|
||||
option_overworld_glitches = 2
|
||||
option_no_logic = 4
|
||||
alias_owg = 2
|
||||
|
||||
|
||||
class Objective(Choice):
|
||||
option_crystals = 0
|
||||
# option_pendants = 1
|
||||
option_triforce_pieces = 2
|
||||
option_pedestal = 3
|
||||
option_bingo = 4
|
||||
def get_option_name(self):
|
||||
return str(self.value)
|
||||
|
||||
|
||||
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):
|
||||
option_locations = 0
|
||||
option_items = 1
|
||||
option_beatable = 2
|
||||
|
||||
|
||||
class Crystals(Choice):
|
||||
# can't use IntEnum since there's also random
|
||||
option_0 = 0
|
||||
option_1 = 1
|
||||
option_2 = 2
|
||||
option_3 = 3
|
||||
option_4 = 4
|
||||
option_5 = 5
|
||||
option_6 = 6
|
||||
option_7 = 7
|
||||
option_random = -1
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
mapshuffle = Toggle
|
||||
compassshuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkeyshuffle = Toggle
|
||||
hints = Toggle
|
||||
|
||||
RandomizeDreamers = Toggle
|
||||
RandomizeSkills = Toggle
|
||||
RandomizeCharms = Toggle
|
||||
RandomizeKeys = Toggle
|
||||
RandomizeGeoChests = Toggle
|
||||
RandomizeMaskShards = Toggle
|
||||
RandomizeVesselFragments = Toggle
|
||||
RandomizeCharmNotches = Toggle
|
||||
RandomizePaleOre = Toggle
|
||||
RandomizeRancidEggs = Toggle
|
||||
RandomizeRelics = Toggle
|
||||
RandomizeMaps = Toggle
|
||||
RandomizeStags = Toggle
|
||||
RandomizeGrubs = Toggle
|
||||
RandomizeWhisperingRoots = Toggle
|
||||
RandomizeRocks = Toggle
|
||||
RandomizeSoulTotems = Toggle
|
||||
RandomizePalaceTotems = Toggle
|
||||
RandomizeLoreTablets = Toggle
|
||||
RandomizeLifebloodCocoons = Toggle
|
||||
RandomizeFlames = Toggle
|
||||
|
||||
hollow_knight_randomize_options: typing.Dict[str, Option] = {
|
||||
"RandomizeDreamers": RandomizeDreamers,
|
||||
"RandomizeSkills": RandomizeSkills,
|
||||
"RandomizeCharms": RandomizeCharms,
|
||||
"RandomizeKeys": RandomizeKeys,
|
||||
"RandomizeGeoChests": RandomizeGeoChests,
|
||||
"RandomizeMaskShards": RandomizeMaskShards,
|
||||
"RandomizeVesselFragments": RandomizeVesselFragments,
|
||||
"RandomizeCharmNotches": RandomizeCharmNotches,
|
||||
"RandomizePaleOre": RandomizePaleOre,
|
||||
"RandomizeRancidEggs": RandomizeRancidEggs,
|
||||
"RandomizeRelics": RandomizeRelics,
|
||||
"RandomizeMaps": RandomizeMaps,
|
||||
"RandomizeStags": RandomizeStags,
|
||||
"RandomizeGrubs": RandomizeGrubs,
|
||||
"RandomizeWhisperingRoots": RandomizeWhisperingRoots,
|
||||
"RandomizeRocks": RandomizeRocks,
|
||||
"RandomizeSoulTotems": RandomizeSoulTotems,
|
||||
"RandomizePalaceTotems": RandomizePalaceTotems,
|
||||
"RandomizeLoreTablets": RandomizeLoreTablets,
|
||||
"RandomizeLifebloodCocoons": RandomizeLifebloodCocoons,
|
||||
"RandomizeFlames": RandomizeFlames
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
|
||||
class TechCost(Choice):
|
||||
option_very_easy = 0
|
||||
option_easy = 1
|
||||
option_kind = 2
|
||||
option_normal = 3
|
||||
option_hard = 4
|
||||
option_very_hard = 5
|
||||
option_insane = 6
|
||||
default = 3
|
||||
|
||||
|
||||
class FreeSamples(Choice):
|
||||
option_none = 0
|
||||
option_single_craft = 1
|
||||
option_half_stack = 2
|
||||
option_stack = 3
|
||||
default = 3
|
||||
|
||||
|
||||
class TechTreeLayout(Choice):
|
||||
option_single = 0
|
||||
option_small_diamonds = 1
|
||||
option_medium_diamonds = 2
|
||||
option_pyramid = 3
|
||||
option_funnel = 4
|
||||
default = 0
|
||||
|
||||
|
||||
class Visibility(Choice):
|
||||
option_none = 0
|
||||
option_sending = 1
|
||||
default = 1
|
||||
|
||||
|
||||
class FactorioStartItems(OptionDict):
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"tech_cost": TechCost,
|
||||
"free_samples": FreeSamples,
|
||||
"visibility": Visibility,
|
||||
"random_tech_ingredients": Toggle,
|
||||
"starting_items": FactorioStartItems}
|
||||
|
||||
|
||||
class AdvancementGoal(Choice):
|
||||
option_few = 0
|
||||
option_normal = 1
|
||||
option_many = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class CombatDifficulty(Choice):
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
default = 1
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
"include_hard_advancements": Toggle,
|
||||
"include_insane_advancements": Toggle,
|
||||
"include_postgame_advancements": Toggle,
|
||||
"shuffle_structures": Toggle
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
from worlds.alttp.Options import Logic
|
||||
import argparse
|
||||
mapshuffle = Toggle
|
||||
compassshuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkeyshuffle = Toggle
|
||||
hints = Toggle
|
||||
test = argparse.Namespace()
|
||||
test.logic = Logic.from_text("no_logic")
|
||||
test.mapshuffle = mapshuffle.from_text("ON")
|
||||
|
||||
172
Utils.py
@@ -12,8 +12,9 @@ class Version(typing.NamedTuple):
|
||||
minor: int
|
||||
build: int
|
||||
|
||||
__version__ = "0.1.1"
|
||||
_version_tuple = tuplize_version(__version__)
|
||||
|
||||
__version__ = "0.1.4"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
import os
|
||||
@@ -22,6 +23,7 @@ import sys
|
||||
import pickle
|
||||
import functools
|
||||
import io
|
||||
import collections
|
||||
|
||||
from yaml import load, dump, safe_load
|
||||
|
||||
@@ -52,7 +54,6 @@ def snes_to_pc(value):
|
||||
def parse_player_names(names, players, teams):
|
||||
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
||||
if len(names) != len(set(names)):
|
||||
import collections
|
||||
name_counter = collections.Counter(names)
|
||||
raise ValueError(f"Duplicate Player names is not supported, "
|
||||
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
||||
@@ -68,6 +69,21 @@ def parse_player_names(names, players, teams):
|
||||
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:
|
||||
return getattr(sys, 'frozen', False)
|
||||
|
||||
@@ -118,20 +134,10 @@ def open_file(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
|
||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_public_ipv4() -> str:
|
||||
import socket
|
||||
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
|
||||
return ip
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_public_ipv6() -> str:
|
||||
import socket
|
||||
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
|
||||
return ip
|
||||
|
||||
|
||||
@cache_argsless
|
||||
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.
|
||||
options = {
|
||||
"general_options": {
|
||||
"output_path": "output",
|
||||
},
|
||||
"factorio_options": {
|
||||
"executable": "factorio\\bin\\x64\\factorio",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
|
||||
"rom_start": True,
|
||||
# Refer to host.yaml for comments as to what all these options mean.
|
||||
options = {
|
||||
"general_options": {
|
||||
"output_path": "output",
|
||||
},
|
||||
"factorio_options": {
|
||||
"executable": "factorio\\bin\\x64\\factorio",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
|
||||
},
|
||||
"server_options": {
|
||||
"host": None,
|
||||
"port": 38281,
|
||||
"password": None,
|
||||
"multidata": None,
|
||||
"savefile": None,
|
||||
"disable_save": False,
|
||||
"loglevel": "info",
|
||||
"server_password": None,
|
||||
"disable_item_cheat": False,
|
||||
"location_check_points": 1,
|
||||
"hint_cost": 1000,
|
||||
"forfeit_mode": "goal",
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
"log_network": 0
|
||||
},
|
||||
"multi_mystery_options": {
|
||||
"teams": 1,
|
||||
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||
"player_files_path": "Players",
|
||||
"players": 0,
|
||||
"weights_file_path": "weights.yaml",
|
||||
"meta_file_path": "meta.yaml",
|
||||
"pre_roll": False,
|
||||
"player_name": "",
|
||||
"create_spoiler": 1,
|
||||
"zip_roms": 0,
|
||||
"zip_diffs": 2,
|
||||
"zip_spoiler": 0,
|
||||
"zip_multidata": 1,
|
||||
"zip_format": 1,
|
||||
"glitch_triforce_room": 1,
|
||||
"race": 0,
|
||||
"cpu_threads": 0,
|
||||
"max_attempts": 0,
|
||||
"take_first_working": False,
|
||||
"keep_all_seeds": False,
|
||||
"log_output_path": "Output Logs",
|
||||
"log_level": None,
|
||||
"plando_options": "bosses",
|
||||
}
|
||||
},
|
||||
"server_options": {
|
||||
"host": None,
|
||||
"port": 38281,
|
||||
"password": None,
|
||||
"multidata": None,
|
||||
"savefile": None,
|
||||
"disable_save": False,
|
||||
"loglevel": "info",
|
||||
"server_password": None,
|
||||
"disable_item_cheat": False,
|
||||
"location_check_points": 1,
|
||||
"hint_cost": 10,
|
||||
"forfeit_mode": "goal",
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
"log_network": 0
|
||||
},
|
||||
"multi_mystery_options": {
|
||||
"teams": 1,
|
||||
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||
"player_files_path": "Players",
|
||||
"players": 0,
|
||||
"weights_file_path": "weights.yaml",
|
||||
"meta_file_path": "meta.yaml",
|
||||
"pre_roll": False,
|
||||
"create_spoiler": 1,
|
||||
"zip_roms": 0,
|
||||
"zip_diffs": 2,
|
||||
"zip_apmcs": 1,
|
||||
"zip_spoiler": 0,
|
||||
"zip_multidata": 1,
|
||||
"zip_format": 1,
|
||||
"glitch_triforce_room": 1,
|
||||
"race": 0,
|
||||
"cpu_threads": 0,
|
||||
"max_attempts": 0,
|
||||
"take_first_working": False,
|
||||
"keep_all_seeds": False,
|
||||
"log_output_path": "Output Logs",
|
||||
"log_level": None,
|
||||
"plando_options": "bosses",
|
||||
}
|
||||
}
|
||||
|
||||
get_default_options.options = options
|
||||
return get_default_options.options
|
||||
return options
|
||||
|
||||
|
||||
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)
|
||||
return dest
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
locations = ("options.yaml", "host.yaml",
|
||||
@@ -345,12 +349,12 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
f"Enter yes, no or never: ")
|
||||
if adjust_wanted and adjust_wanted.startswith("y"):
|
||||
if hasattr(adjuster_settings, "sprite_pool"):
|
||||
from Adjuster import AdjusterWorld
|
||||
from LttPAdjuster import AdjusterWorld
|
||||
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
|
||||
|
||||
adjusted = True
|
||||
import Adjuster
|
||||
_, romfile = Adjuster.adjust(adjuster_settings)
|
||||
import LttPAdjuster
|
||||
_, romfile = LttPAdjuster.adjust(adjuster_settings)
|
||||
|
||||
if hasattr(adjuster_settings, "world"):
|
||||
delattr(adjuster_settings, "world")
|
||||
@@ -367,7 +371,7 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
return romfile, adjusted
|
||||
return romfile, False
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_unique_identifier():
|
||||
uuid = persistent_load().get("client", {}).get("uuid", None)
|
||||
if uuid:
|
||||
@@ -404,4 +408,10 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
|
||||
def restricted_loads(s):
|
||||
"""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
|
||||
@@ -2,6 +2,10 @@ import os
|
||||
import multiprocessing
|
||||
import logging
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
|
||||
ModuleUpdate.update()
|
||||
|
||||
from WebHostLib import app as raw_app
|
||||
from waitress import serve
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
import base64
|
||||
import socket
|
||||
|
||||
import jinja2.exceptions
|
||||
from pony.flask import Pony
|
||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||
from flask_caching import Cache
|
||||
@@ -74,6 +75,51 @@ def register_session():
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||
def page_not_found(err):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
games_list = {
|
||||
"zelda3": ("The Legend of Zelda: A Link to the Past",
|
||||
"""
|
||||
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of Link,
|
||||
a boy who is destined to save the land of Hyrule. Delve through three palaces and nine dungeons on
|
||||
your quest to rescue the descendents of the seven wise men and defeat the evil Ganon!"""),
|
||||
"factorio": ("Factorio",
|
||||
"""
|
||||
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
|
||||
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
|
||||
research new technologies, and become more efficient in your quest to build a rocket and return home.
|
||||
"""),
|
||||
"minecraft": ("Minecraft",
|
||||
"""
|
||||
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
|
||||
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!""")
|
||||
}
|
||||
|
||||
|
||||
# Game sub-pages
|
||||
@app.route('/games/<string:game>/<string:page>')
|
||||
def game_pages(game, page):
|
||||
return render_template(f"/games/{game}/{page}.html")
|
||||
|
||||
|
||||
# Game landing pages
|
||||
@app.route('/games/<game>')
|
||||
def game_page(game):
|
||||
return render_template(f"/games/{game}/{game}.html")
|
||||
|
||||
|
||||
# List of supported games
|
||||
@app.route('/games')
|
||||
def games():
|
||||
return render_template("games/games.html", games_list=games_list)
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
def tutorial(game, file, lang):
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang)
|
||||
@@ -84,13 +130,8 @@ def tutorial_landing():
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/player-settings')
|
||||
def player_settings_simple():
|
||||
return render_template("playerSettings.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def player_settings():
|
||||
def weighted_settings():
|
||||
return render_template("weightedSettings.html")
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import concurrent.futures
|
||||
import sys
|
||||
import typing
|
||||
import time
|
||||
import os
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
|
||||
@@ -15,10 +16,13 @@ from Utils import restricted_loads
|
||||
|
||||
class CommonLocker():
|
||||
"""Uses a file lock to signal that something is already running"""
|
||||
|
||||
def __init__(self, lockname: str):
|
||||
lock_folder = "file_locks"
|
||||
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.lockfile = f"./{self.lockname}.lck"
|
||||
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
||||
|
||||
|
||||
class AlreadyRunningException(Exception):
|
||||
@@ -26,9 +30,6 @@ class AlreadyRunningException(Exception):
|
||||
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import os
|
||||
|
||||
|
||||
class Locker(CommonLocker):
|
||||
def __enter__(self):
|
||||
try:
|
||||
|
||||
@@ -104,6 +104,7 @@ class WebHostContext(Context):
|
||||
def get_random_port():
|
||||
return random.randint(49152, 65535)
|
||||
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict):
|
||||
# establish DB connection for multidata and multisave
|
||||
db.bind(**ponyconfig)
|
||||
@@ -144,7 +145,9 @@ def run_server_process(room_id, ponyconfig: dict):
|
||||
await ctx.shutdown_task
|
||||
logging.info("Shutting down")
|
||||
|
||||
asyncio.run(main())
|
||||
from .autolauncher import Locker
|
||||
with Locker(room_id):
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
from WebHostLib import LOGS_FOLDER
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
flask>=2.0.0
|
||||
flask>=2.0.1
|
||||
pony>=0.7.14
|
||||
waitress>=2.0.0
|
||||
flask-caching>=1.10.1
|
||||
Flask-Compress>=1.9.0
|
||||
Flask-Compress>=1.10.1
|
||||
Flask-Limiter>=1.4
|
||||
|
||||
@@ -17,6 +17,47 @@ window.addEventListener('load', () => {
|
||||
paging: false,
|
||||
info: false,
|
||||
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
|
||||
// the tbody and render two separate tables.
|
||||
|
||||
@@ -33,19 +33,17 @@ use-system-read-write-data-directories=false
|
||||
|
||||
## 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
|
||||
|
||||
3. Install the generated Factorio AP Mod
|
||||
|
||||
4. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
||||
2. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
||||
|
||||
* 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.
|
||||
* It should start up, create a world and become ready for Factorio connections.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
* / 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.
|
||||
|
||||
|
||||
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.
|
||||
@@ -1,7 +1,7 @@
|
||||
# A Link to the Past Randomizer Setup Guide
|
||||
|
||||
## Benötigte Software
|
||||
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
|
||||
- Ein Emulator, der lua-scripts abspielen kann
|
||||
@@ -15,7 +15,7 @@
|
||||
### Windows
|
||||
1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die
|
||||
aktuellste Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**.
|
||||
Für normale Multiworld-Spiele lädst du die `Setup.BerserkerMultiworld.exe` herunter.
|
||||
Für normale Multiworld-Spiele lädst du die `Setup.Archipelago.exe` herunter.
|
||||
- Für den Doorrandomizer muss die alternative doors-Variante geladen werden.
|
||||
- Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software
|
||||
bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
## Required Software
|
||||
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of running Lua scripts
|
||||
@@ -21,7 +21,7 @@
|
||||
### Windows Setup
|
||||
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
|
||||
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal
|
||||
multiworld games, you want `Setup.BerserkerMultiWorld.exe`
|
||||
multiworld games, you want `Setup.Archipelago.exe`
|
||||
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
|
||||
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
|
||||
installed this software before and are simply upgrading now, you will not be prompted to locate your
|
||||
@@ -144,7 +144,7 @@ on successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
The recommended way to host a game is to use the hosting service provided on
|
||||
[the website](https://berserkermulti.world/generate). The process is relatively simple:
|
||||
[the website](/generate). The process is relatively simple:
|
||||
|
||||
1. Collect YAML files from your players.
|
||||
2. Create a zip file containing your players' YAML files.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
## Software requerido
|
||||
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
|
||||
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
|
||||
- Un emulador capaz de ejecutar scripts Lua
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
### Instalación en Windows
|
||||
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
|
||||
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.BerserkerMultiWorld.exe`
|
||||
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
|
||||
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar 'Setup.BerserkerMultiWorld.Doors.exe'
|
||||
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del archivo una segunda vez.
|
||||
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
|
||||
@@ -136,7 +136,7 @@ por unirte satisfactoriamente a una partida de multiworld!
|
||||
|
||||
## Hospedando una partida de multiworld
|
||||
La manera recomendad para hospedar una partida es usar el servicio proveído en
|
||||
[el sitio web](https://berserkermulti.world/generate). El proceso es relativamente sencillo:
|
||||
[el sitio web](/generate). El proceso es relativamente sencillo:
|
||||
|
||||
1. Recolecta los ficheros YAML de todos los jugadores que participen.
|
||||
2. Crea un fichero ZIP conteniendo esos ficheros.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
## Logiciels requis
|
||||
- [Utilitaires du MultiWorld](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
||||
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
|
||||
- Un émulateur capable d'éxécuter des scripts Lua
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Configuration
|
||||
1. Plando features have to be enabled first, before they can be used (opt-in).
|
||||
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\BerserkerMultiWorld`),
|
||||
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`),
|
||||
then open the host.yaml file with a text editor.
|
||||
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the
|
||||
value to
|
||||
@@ -13,7 +13,7 @@
|
||||
### Bosses
|
||||
|
||||
- This module is enabled by default and available to be used on
|
||||
[https://archipelago.gg/generate](https://archipelago.gg/generate)
|
||||
[https://archipelago.gg/generate](/generate)
|
||||
- Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted.
|
||||
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
|
||||
it defaults to vanilla
|
||||
|
||||
10
WebHostLib/static/styles/404.css
Normal file
@@ -0,0 +1,10 @@
|
||||
#page-not-found{
|
||||
width: 40em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#page-not-found h1{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
3
WebHostLib/static/styles/factorio/factorio.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#factorio{
|
||||
margin: 1rem;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
61
WebHostLib/static/styles/games.css
Normal file
@@ -0,0 +1,61 @@
|
||||
#games{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#games p{
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#games code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#games #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#games h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#games h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#games h3, #games h4, #games h5, #games h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#games a{
|
||||
color: #ffef00;
|
||||
}
|
||||
@@ -4,9 +4,6 @@
|
||||
}
|
||||
|
||||
html{
|
||||
background-image: url('../static/backgrounds/oceans/oceans-0002.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 250px 250px;
|
||||
font-family: 'Jost', sans-serif;
|
||||
font-size: 1.1rem;
|
||||
color: #000000;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/dirt/dirt-0005-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 900px 900px;
|
||||
}
|
||||
|
||||
#base-header{
|
||||
background: url('../../static/backgrounds/header/dirt-header.png') repeat-x;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
#base-header{
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#base-header {
|
||||
background: url('../../static/backgrounds/header/grass-header.png') repeat-x;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/oceans/oceans-0002.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 250px 250px;
|
||||
}
|
||||
|
||||
#base-header{
|
||||
background: url('../../static/backgrounds/header/ocean-header.png') repeat-x;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#host-room{
|
||||
width: calc(100% - 5rem);
|
||||
margin-left: auto;
|
||||
|
||||
@@ -7,7 +7,6 @@ html{
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
#landing-header{
|
||||
@@ -53,18 +52,19 @@ html{
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
#uploads-button{
|
||||
top: 65px;
|
||||
#far-left-button{
|
||||
top: 115px;
|
||||
left: calc(50% - 416px - 200px - 75px);
|
||||
background-image: url("/static/static/button-images/button-a.png");
|
||||
background-size: 200px auto;
|
||||
width: 200px;
|
||||
height: calc(156px - 40px);
|
||||
padding-top: 40px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#setup-guide-button{
|
||||
top: 270px;
|
||||
#mid-left-button{
|
||||
top: 320px;
|
||||
left: calc(50% - 416px - 200px + 140px);
|
||||
background-image: url("/static/static/button-images/button-b.png");
|
||||
background-size: 260px auto;
|
||||
@@ -73,8 +73,8 @@ html{
|
||||
padding-top: 35px;
|
||||
}
|
||||
|
||||
#player-settings-button{
|
||||
top: 350px;
|
||||
#mid-button{
|
||||
top: 400px;
|
||||
left: calc(50% - 100px);
|
||||
background-image: url("/static/static/button-images/button-a.png");
|
||||
background-size: 200px auto;
|
||||
@@ -83,8 +83,8 @@ html{
|
||||
padding-top: 38px;
|
||||
}
|
||||
|
||||
#discord-button{
|
||||
top: 250px;
|
||||
#mid-right-button{
|
||||
top: 300px;
|
||||
left: calc(50% + 416px - 166px);
|
||||
background-image: url("/static/static/button-images/button-c.png");
|
||||
background-size: 250px auto;
|
||||
@@ -94,8 +94,8 @@ html{
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#generate-button{
|
||||
top: 75px;
|
||||
#far-right-button{
|
||||
top: 125px;
|
||||
left: calc(50% + 416px + 75px);
|
||||
background-image: url("/static/static/button-images/button-b.png");
|
||||
background-size: 260px auto;
|
||||
@@ -111,7 +111,7 @@ html{
|
||||
#landing-clouds #cloud1{
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 265px;
|
||||
top: 365px;
|
||||
width: 400px;
|
||||
height: 350px;
|
||||
|
||||
@@ -147,23 +147,23 @@ html{
|
||||
@keyframes c1-float{
|
||||
from{
|
||||
left: 10px;
|
||||
top: 265px;
|
||||
top: 365px;
|
||||
}
|
||||
25%{
|
||||
left: 14px;
|
||||
top: 267px;
|
||||
top: 367px;
|
||||
}
|
||||
50%{
|
||||
left: 17px;
|
||||
top: 265px;
|
||||
top: 365px;
|
||||
}
|
||||
75%{
|
||||
left: 14px;
|
||||
top: 262px;
|
||||
top: 362px;
|
||||
}
|
||||
to{
|
||||
left: 10px;
|
||||
top: 265px;
|
||||
top: 365px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,32 +241,32 @@ html{
|
||||
}
|
||||
|
||||
#landing-deco-1{
|
||||
top: 430px;
|
||||
top: 480px;
|
||||
left: calc(50% - 276px);
|
||||
}
|
||||
|
||||
#landing-deco-2{
|
||||
top: 200px;
|
||||
top: 250px;
|
||||
left: calc(50% + 150px);
|
||||
}
|
||||
|
||||
#landing-deco-3{
|
||||
top: 300px;
|
||||
top: 350px;
|
||||
left: calc(50% - 150px);
|
||||
}
|
||||
|
||||
#landing-deco-4{
|
||||
top: 240px;
|
||||
top: 290px;
|
||||
left: calc(50% - 580px);
|
||||
}
|
||||
|
||||
#landing-deco-5{
|
||||
top: 40px;
|
||||
top: 90px;
|
||||
left: calc(50% + 450px);
|
||||
}
|
||||
|
||||
#landing-deco-6{
|
||||
top: 412px;
|
||||
top: 462px;
|
||||
left: calc(50% + 196px);
|
||||
}
|
||||
|
||||
|
||||
3
WebHostLib/static/styles/minecraft/minecraft.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#minecraft{
|
||||
margin: 1rem;
|
||||
}
|
||||
129
WebHostLib/static/styles/minecraft/player-settings.css
Normal file
@@ -0,0 +1,129 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-settings #player-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select{
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: unset;
|
||||
}
|
||||
|
||||
#game-options table label, #rom-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%);
|
||||
filter: grayscale(100%) contrast(75%) brightness(75%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/dirt/dirt-0005-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 900px 900px;
|
||||
}
|
||||
|
||||
#tracker-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#tutorial-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#tutorial-landing{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#weighted-settings{
|
||||
width: 60rem;
|
||||
margin-left: auto;
|
||||
|
||||
129
WebHostLib/static/styles/zelda3/player-settings.css
Normal file
@@ -0,0 +1,129 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-settings #player-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select{
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: unset;
|
||||
}
|
||||
|
||||
#game-options table label, #rom-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
3
WebHostLib/static/styles/zelda3/zelda3.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#zelda3{
|
||||
margin: 1rem;
|
||||
}
|
||||
17
WebHostLib/templates/404.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
|
||||
{% block head %}
|
||||
<title>Page Not Found (404)</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/404.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="page-not-found" class="grass-island">
|
||||
<h1>This page is out of logic!</h1>
|
||||
The page you're looking for doesn't exist.<br />
|
||||
<a href="/">Click here to return to safety.</a>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
15
WebHostLib/templates/games/factorio/factorio.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Factorio</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/factorio/factorio.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="factorio">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
WebHostLib/templates/games/factorio/player-settings.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Factorio Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/factorio/player-settings.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="player-settings">
|
||||
<div id="user-message"></div>
|
||||
<h1>Factorio Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||
page. There, you will find examples of all available sprites as well.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
|
||||
|
||||
<div>
|
||||
More content coming soon™.
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
WebHostLib/templates/games/games.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Player Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/games.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="games">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game, (display_name, description) in games_list.items() %}
|
||||
<h3><a href="{{ url_for("game_page", game=game) }}">{{ display_name}}</a></h3>
|
||||
<p>{{ description}}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
15
WebHostLib/templates/games/minecraft/minecraft.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Minecraft</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/minecraft/minecraft.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="minecraft">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
WebHostLib/templates/games/minecraft/player-settings.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Minecraft Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/minecraft/player-settings.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="player-settings">
|
||||
<div id="user-message"></div>
|
||||
<h1>Minecraft Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||
page. There, you will find examples of all available sprites as well.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
|
||||
|
||||
<div>
|
||||
More content coming soon™.
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,18 +1,18 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Player Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerSettings.css") }}" />
|
||||
<title>A Link to the Past Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/player-settings.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerSettings.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/zelda3/player-settings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="player-settings">
|
||||
<div id="user-message"></div>
|
||||
<h1>Start Game</h1>
|
||||
<h1>A Link to the Past Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||
15
WebHostLib/templates/games/zelda3/zelda3.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>A Link to the Past</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/zelda3.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="zelda3">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -31,7 +31,7 @@
|
||||
<p>
|
||||
After generation is complete, you will have the option to download a patch file.
|
||||
This patch file can be opened with the
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/releases">client</a>, which can be
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
|
||||
used to to create a rom file. In-browser patching is planned for the future.
|
||||
</p>
|
||||
<div id="generate-game-form-wrapper">
|
||||
|
||||
63
WebHostLib/templates/genericTracker.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span class="info">This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for name, count in inventory.items() %}
|
||||
<tr>
|
||||
<td>{{ name | item_name }}</td>
|
||||
<td>{{ count }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>Checked</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for name in checked_locations %}
|
||||
<tr>
|
||||
<td>{{ name | location_name}}</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
{% for name in not_checked_locations %}
|
||||
<tr>
|
||||
<td>{{ name | location_name}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -11,10 +11,8 @@
|
||||
<a href="/">archipelago</a>
|
||||
</div>
|
||||
<div id="base-header-right">
|
||||
<a href="/player-settings">start game</a>
|
||||
<a href="/uploads">host game</a>
|
||||
<a href="/games">games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="/generate">upload config</a>
|
||||
<a href="https://discord.gg/8Z65BR2">discord</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2021 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a>
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago">Source Code</a>
|
||||
-
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/wiki">Wiki</a>
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/wiki">Wiki</a>
|
||||
-
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/graphs/contributors">Contributors</a>
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">Contributors</a>
|
||||
-
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/issues">Bug Report</a>
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/issues">Bug Report</a>
|
||||
</div>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,17 +6,18 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="landing-wrapper">
|
||||
<div id="landing-header">
|
||||
<h4>the legend of zelda: a link to the past</h4>
|
||||
<h1>MULTIWORLD RANDOMIZER</h1>
|
||||
<h1>ARCHIPELAGO</h1>
|
||||
<h4>multiworld randomizer ecosystem</h4>
|
||||
</div>
|
||||
<div id="landing-links">
|
||||
<a href="/player-settings" id="player-settings-button">start<br />playing</a>
|
||||
<a href="/uploads" id="uploads-button">host<br />game</a>
|
||||
<a href="/tutorial" id="setup-guide-button">setup guides</a>
|
||||
<a href="/generate" id="generate-button">upload config</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="discord-button">discord</a>
|
||||
<a href="/games" id="mid-button">start<br />playing</a>
|
||||
<a id="far-left-button"></a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
||||
<a href="/uploads" id="far-right-button">Host Game</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
||||
</div>
|
||||
<div id="landing-clouds">
|
||||
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
|
||||
@@ -33,19 +34,18 @@
|
||||
</div>
|
||||
<div id="landing" class="grass-island">
|
||||
<div id="landing-body">
|
||||
<p id="first-line">Welcome to the Archipelago Multiworld Randomizer!</p>
|
||||
<p>This is a <span data-tooltip="Allegedly.">randomizer</span> for The Legend of Zelda: A
|
||||
Link to the Past.</p>
|
||||
<p>It is also a multi-world, meaning Link's items may have been placed into other players' games.
|
||||
When a player picks up an item which does not belong to them, it is sent back to the player
|
||||
it belongs to.</p>
|
||||
<p>On this website you are able to generate and host multiworld games, and item and location
|
||||
trackers are provided for games hosted here.</p>
|
||||
<p id="first-line">Welcome to Archipelago!</p>
|
||||
<p>
|
||||
This is a cross-game modification system which randomizes different games, then uses the result to
|
||||
build a single unified multi-player game. Items from one game may be present in another, and
|
||||
you will need your fellow players to find items you need in their games to help you complete
|
||||
your own.
|
||||
</p>
|
||||
<p>
|
||||
This project is the cumulative effort of many
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/graphs/contributors">talented people.</a>
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">talented people.</a>
|
||||
Together, they have spent countless hours creating a huge repository of
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities">source code</a> which has turned
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago">source code</a> which has turned
|
||||
our crazy idea into a reality.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -98,20 +98,20 @@
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<th rowspan="2" class="center-column">Last<br>Activity</th>
|
||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% 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">
|
||||
</th>
|
||||
{% 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">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
</th>
|
||||
{%- endif -%}
|
||||
@@ -141,7 +141,7 @@
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- 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 -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
|
||||
@@ -5,7 +5,7 @@ from werkzeug.exceptions import abort
|
||||
import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from worlds.alttp import Items, Regions
|
||||
from worlds.alttp import Items
|
||||
from WebHostLib import app, cache, Room
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
@@ -327,7 +327,8 @@ def get_static_room_data(room: Room):
|
||||
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
||||
|
||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||
player_big_key_locations, player_small_key_locations, multidata["precollected_items"]
|
||||
player_big_key_locations, player_small_key_locations, multidata["precollected_items"], \
|
||||
multidata["games"]
|
||||
_multidata_cache[room.seed.id] = result
|
||||
return result
|
||||
|
||||
@@ -344,9 +345,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
abort(404)
|
||||
|
||||
# Collect seed information and pare it down to a single player
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, precollected_items = get_static_room_data(room)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
player_big_key_locations, player_small_key_locations, precollected_items, games = get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
seed_checks_in_area = seed_checks_in_area[tracked_player]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
inventory = collections.Counter()
|
||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
@@ -377,52 +378,58 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
if games[tracked_player] == "A Link to the Past":
|
||||
# Note the presence of the triforce item
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
if game_state == 30:
|
||||
inventory[106] = 1 # Triforce
|
||||
|
||||
# Note the presence of the triforce item
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
if game_state == 30:
|
||||
inventory[106] = 1 # Triforce
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
"Progressive Sword": 94,
|
||||
"Progressive Glove": 97,
|
||||
"Progressive Bow": 100,
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
}
|
||||
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
"Progressive Sword": 94,
|
||||
"Progressive Glove": 97,
|
||||
"Progressive Bow": 100,
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
}
|
||||
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]))
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level+1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name+"_acquired"] = acquired
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level+1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name+"_acquired"] = acquired
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
|
||||
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
|
||||
return render_template("playerTracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
||||
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
|
||||
checks_in_area=seed_checks_in_area, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
**display_data)
|
||||
return render_template("lttpTracker.html", inventory=inventory,
|
||||
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
|
||||
checks_in_area=seed_checks_in_area[tracked_player], acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
**display_data)
|
||||
else:
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
return render_template("genericTracker.html",
|
||||
inventory=inventory,
|
||||
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@@ -432,7 +439,7 @@ def getTracker(tracker: UUID):
|
||||
if not room:
|
||||
abort(404)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
|
||||
player_small_key_locations, precollected_items = get_static_room_data(room)
|
||||
player_small_key_locations, precollected_items, games = get_static_room_data(room)
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
@@ -447,7 +454,6 @@ def getTracker(tracker: UUID):
|
||||
else:
|
||||
multisave = {}
|
||||
if "hints" in multisave:
|
||||
|
||||
for (team, slot), slot_hints in multisave["hints"].items():
|
||||
hints[team] |= set(slot_hints)
|
||||
|
||||
@@ -486,7 +492,7 @@ def getTracker(tracker: UUID):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[(team, player)] = name
|
||||
long_player_names = player_names.copy()
|
||||
for (team, player), alias in multisave.get("name_aliases", []):
|
||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||
player_names[(team, player)] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||
|
||||
|
||||
BIN
data/ER.icns
BIN
data/ER.ico
|
Before Width: | Height: | Size: 38 KiB |
BIN
data/ER16.gif
|
Before Width: | Height: | Size: 123 B |
BIN
data/ER32.gif
|
Before Width: | Height: | Size: 370 B |
BIN
data/ER48.gif
|
Before Width: | Height: | Size: 882 B |
1
data/factorio/machines.json
Normal file
@@ -0,0 +1 @@
|
||||
{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}}
|
||||
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 24 KiB |
BIN
data/factorio/mod/graphics/icons/ap_unimportant.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
@@ -20,4 +20,16 @@ function get_any_stack_size(name)
|
||||
end
|
||||
-- failsafe
|
||||
return 1
|
||||
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
|
||||
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,17 +1,22 @@
|
||||
{% macro dict_to_lua(dict) -%}
|
||||
{
|
||||
{% for key, value in dict.items() %}
|
||||
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
{%- endmacro %}
|
||||
{% from "macros.lua" import dict_to_lua %}
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require "lib"
|
||||
require "util"
|
||||
|
||||
FREE_SAMPLES = {{ free_samples }}
|
||||
SLOT_NAME = "{{ slot_name }}"
|
||||
SEED_NAME = "{{ seed_name }}"
|
||||
--SUPPRESS_INVENTORY_EVENTS = false
|
||||
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
|
||||
|
||||
{% if not imported_blueprints -%}
|
||||
function set_permissions()
|
||||
local group = game.permissions.get_group("Default")
|
||||
group.set_allows_action(defines.input_action.open_blueprint_library_gui, false)
|
||||
group.set_allows_action(defines.input_action.import_blueprint, false)
|
||||
group.set_allows_action(defines.input_action.import_blueprint_string, false)
|
||||
group.set_allows_action(defines.input_action.import_blueprints_filtered, false)
|
||||
end
|
||||
{%- endif %}
|
||||
|
||||
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
|
||||
function on_force_created(event)
|
||||
@@ -68,7 +73,7 @@ function update_player(index)
|
||||
local sent
|
||||
--player.print(serpent.block(data['pending_samples']))
|
||||
local stack = {}
|
||||
--SUPPRESS_INVENTORY_EVENTS = true
|
||||
|
||||
for name, count in pairs(samples) do
|
||||
stack.name = name
|
||||
stack.count = count
|
||||
@@ -92,16 +97,14 @@ function update_player(index)
|
||||
samples[name] = nil -- Remove from the list
|
||||
end
|
||||
end
|
||||
--SUPPRESS_INVENTORY_EVENTS = false
|
||||
|
||||
end
|
||||
|
||||
-- Update players upon them connecting, since updates while they're offline are suppressed.
|
||||
script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end)
|
||||
|
||||
function update_player_event(event)
|
||||
--if not SUPPRESS_INVENTORY_EVENTS then
|
||||
update_player(event.player_index)
|
||||
--end
|
||||
end
|
||||
|
||||
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
|
||||
@@ -120,6 +123,7 @@ function add_samples(force, name, count)
|
||||
end
|
||||
|
||||
script.on_init(function()
|
||||
{% if not imported_blueprints %}set_permissions(){% endif %}
|
||||
global.forcedata = {}
|
||||
global.playerdata = {}
|
||||
-- Fire dummy events for all currently existing forces.
|
||||
@@ -137,66 +141,49 @@ script.on_init(function()
|
||||
end
|
||||
end)
|
||||
|
||||
-- for testing
|
||||
script.on_event(defines.events.on_tick, function(event)
|
||||
if event.tick%600 == 300 then
|
||||
dumpInfo(game.forces["player"])
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
local technology = event.research
|
||||
dumpInfo(technology.force)
|
||||
if FREE_SAMPLES == 0 then
|
||||
return -- Nothing else to do
|
||||
end
|
||||
if not technology.effects then
|
||||
return -- No technology effects, so nothing to do.
|
||||
end
|
||||
for _, effect in pairs(technology.effects) do
|
||||
if effect.type == "unlock-recipe" then
|
||||
local recipe = game.recipe_prototypes[effect.recipe]
|
||||
for _, result in pairs(recipe.products) do
|
||||
if result.type == "item" and result.amount then
|
||||
local name = result.name
|
||||
local count
|
||||
if FREE_SAMPLES == 1 then
|
||||
count = result.amount
|
||||
else
|
||||
count = get_any_stack_size(result.name)
|
||||
if FREE_SAMPLES == 2 then
|
||||
count = math.ceil(count / 2)
|
||||
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
||||
dumpInfo(technology.force) --is sendable
|
||||
else
|
||||
if FREE_SAMPLES == 0 then
|
||||
return -- Nothing else to do
|
||||
end
|
||||
if not technology.effects then
|
||||
return -- No technology effects, so nothing to do.
|
||||
end
|
||||
for _, effect in pairs(technology.effects) do
|
||||
if effect.type == "unlock-recipe" then
|
||||
local recipe = game.recipe_prototypes[effect.recipe]
|
||||
for _, result in pairs(recipe.products) do
|
||||
if result.type == "item" and result.amount then
|
||||
local name = result.name
|
||||
if FREE_SAMPLE_BLACKLIST[name] ~= 1 then
|
||||
local count
|
||||
if FREE_SAMPLES == 1 then
|
||||
count = result.amount
|
||||
else
|
||||
count = get_any_stack_size(result.name)
|
||||
if FREE_SAMPLES == 2 then
|
||||
count = math.ceil(count / 2)
|
||||
end
|
||||
end
|
||||
add_samples(technology.force, name, count)
|
||||
end
|
||||
end
|
||||
add_samples(technology.force, name, count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
function dumpInfo(force)
|
||||
local research_done = {}
|
||||
local data_collection = {
|
||||
["research_done"] = research_done,
|
||||
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
||||
["slot_name"] = SLOT_NAME,
|
||||
["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)
|
||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.print("Sent progress to Archipelago.")
|
||||
log("Archipelago Bridge Data available for game tick ".. game.tick .. ".") -- notifies client
|
||||
end
|
||||
|
||||
|
||||
|
||||
function chain_lookup(table, ...)
|
||||
for _, k in ipairs{...} do
|
||||
table = table[k]
|
||||
@@ -207,33 +194,76 @@ function chain_lookup(table, ...)
|
||||
return table
|
||||
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
|
||||
dumpInfo(game.forces.player)
|
||||
force = game.forces.player
|
||||
else
|
||||
dumpInfo(game.players[call.player_index].force)
|
||||
force = game.players[call.player_index].force
|
||||
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)
|
||||
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
local force = game.forces["player"]
|
||||
chunks = {}
|
||||
for substring in call.parameter:gmatch("%S+") do -- split on " "
|
||||
table.insert(chunks, substring)
|
||||
if global.index_sync == nil then
|
||||
global.index_sync = {}
|
||||
end
|
||||
local tech
|
||||
local force = game.forces["player"]
|
||||
chunks = split(call.parameter, "\t")
|
||||
local tech_name = chunks[1]
|
||||
local source = chunks[2] or "Archipelago"
|
||||
local tech = force.technologies[tech_name]
|
||||
if tech ~= nil then
|
||||
if tech.researched ~= true then
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
local index = chunks[2]
|
||||
local source = chunks[3] or "Archipelago"
|
||||
if progressive_technologies[tech_name] ~= nil then
|
||||
if global.index_sync[index] == nil then -- not yet received prog item
|
||||
global.index_sync[index] = tech_name
|
||||
local tech_stack = progressive_technologies[tech_name]
|
||||
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
|
||||
else
|
||||
game.print("Unknown Technology " .. tech_name)
|
||||
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) }}
|
||||
@@ -1,13 +1,16 @@
|
||||
{% from "macros.lua" import dict_to_recipe %}
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
|
||||
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }}
|
||||
{%- 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 original_tech
|
||||
local new_tree_copy
|
||||
allowed_ingredients = {}
|
||||
{%- for tech_name, technology in custom_data["custom_technologies"].items() %}
|
||||
{%- for tech_name, technology in custom_technologies.items() %}
|
||||
allowed_ingredients["{{ tech_name }}"] = {
|
||||
{%- for ingredient in technology.ingredients %}
|
||||
["{{ingredient}}"] = 1,
|
||||
@@ -22,7 +25,7 @@ template_tech.effects = {}
|
||||
template_tech.prerequisites = {}
|
||||
|
||||
function prep_copy(new_copy, old_tech)
|
||||
old_tech.enabled = false
|
||||
old_tech.hidden = true
|
||||
new_copy.unit = table.deepcopy(old_tech.unit)
|
||||
local ingredient_filter = allowed_ingredients[old_tech.name]
|
||||
if ingredient_filter ~= nil then
|
||||
@@ -30,31 +33,64 @@ function prep_copy(new_copy, old_tech)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
|
||||
function set_ap_icon(tech)
|
||||
tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
|
||||
tech.icons = nil
|
||||
tech.icon_size = 128
|
||||
end
|
||||
|
||||
function set_ap_unimportant_icon(tech)
|
||||
tech.icon = "__{{ mod_name }}__/graphics/icons/ap_unimportant.png"
|
||||
tech.icons = nil
|
||||
tech.icon_size = 128
|
||||
end
|
||||
|
||||
function copy_factorio_icon(tech, tech_source)
|
||||
tech.icon = table.deepcopy(technologies[tech_source].icon)
|
||||
tech.icons = table.deepcopy(technologies[tech_source].icons)
|
||||
tech.icon_size = table.deepcopy(technologies[tech_source].icon_size)
|
||||
end
|
||||
|
||||
function adjust_energy(recipe_name, factor)
|
||||
local recipe = data.raw.recipe[recipe_name]
|
||||
local energy = recipe.energy_required
|
||||
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["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 #}
|
||||
{%- for original_tech_name, item_name, receiving_player in locations %}
|
||||
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||
original_tech = technologies["{{original_tech_name}}"]
|
||||
{#- the tech researched by the local player #}
|
||||
new_tree_copy = table.deepcopy(template_tech)
|
||||
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
||||
prep_copy(new_tree_copy, original_tech)
|
||||
{% if tech_cost != 1 %}
|
||||
if new_tree_copy.unit.count then
|
||||
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||
end
|
||||
{% endif %}
|
||||
{% if item_name in tech_table and visibility %}
|
||||
{#- copy Factorio Technology Icon #}
|
||||
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
|
||||
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
|
||||
new_tree_copy.icon_size = table.deepcopy(technologies["{{ item_name }}"].icon_size)
|
||||
{% else %}
|
||||
{#- use default AP icon if no Factorio graphics exist #}
|
||||
new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
|
||||
new_tree_copy.icons = nil
|
||||
new_tree_copy.icon_size = 512
|
||||
{% if tech_cost_scale != 1 %}
|
||||
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||
{% endif %}
|
||||
{%- 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_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 -%}
|
||||
{#- 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 %}
|
||||
{%- endif -%}
|
||||
{#- connect Technology #}
|
||||
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
||||
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||
@@ -63,5 +99,11 @@ table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
||||
{% endif -%}
|
||||
{#- add new Technology to game #}
|
||||
data:extend{new_tree_copy}
|
||||
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if recipe_time_scale %}
|
||||
{%- for recipe_name, recipe in recipes.items() %}
|
||||
{%- if recipe.category != "mining" %}
|
||||
adjust_energy("{{ recipe_name }}", {{ random.triangular(*recipe_time_scale) }})
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
{% endif %}
|
||||
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,18 +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]
|
||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
||||
{%- if visibility -%}
|
||||
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||
{%- 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 }}
|
||||
{% else %}
|
||||
ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
|
||||
{%- else %}
|
||||
ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
|
||||
[technology-description]
|
||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
||||
{%- if visibility -%}
|
||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
|
||||
{% else %}
|
||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
|
||||
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||
{%- 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 %}.
|
||||
{%- 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. For purposes of hints, this location is called "{{ original_tech_name }}".
|
||||
{%- else %}
|
||||
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 -%}
|
||||
{% endfor %}
|
||||
29
data/factorio/mod_template/macros.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
{% macro dict_to_lua(dict) -%}
|
||||
{
|
||||
{%- for key, value in dict.items() -%}
|
||||
["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %}
|
||||
{% endfor -%}
|
||||
}
|
||||
{%- 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) -%}
|
||||
{
|
||||
{%- for key, value in dict.items() -%}
|
||||
{"{{ key }}", {{ value | safe }}}{% if not loop.last %},{% endif %}
|
||||
{% endfor -%}
|
||||
}
|
||||
{%- endmacro %}
|
||||
BIN
data/icon.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
80
factorio_inno_setup_38.iss
Normal file
@@ -0,0 +1,80 @@
|
||||
#define sourcepath "build_factorio\exe.win-amd64-3.8\"
|
||||
#define MyAppName "Archipelago Factorio Client"
|
||||
#define MyAppExeName "ArchipelagoGraphicalFactorioClient.exe"
|
||||
#define MyAppIcon "icon.ico"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
AppId={{D13CEBD0-F1D5-4435-A4A6-5243F934613F}}
|
||||
AppName={#MyAppName}
|
||||
AppVerName={#MyAppName}
|
||||
DefaultDirName={commonappdata}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
DefaultGroupName=Archipelago
|
||||
OutputDir=setups
|
||||
OutputBaseFilename=Setup {#MyAppName}
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
LZMANumBlockThreads=8
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ChangesAssociations=yes
|
||||
ArchitecturesAllowed=x64
|
||||
AllowNoIcons=yes
|
||||
SetupIconFile={#MyAppIcon}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
SignTool= signtool
|
||||
LicenseFile= LICENSE
|
||||
WizardStyle= modern
|
||||
SetupLogging=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
|
||||
|
||||
|
||||
[Dirs]
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
|
||||
[Code]
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
function IsVCRedist64BitNeeded(): boolean;
|
||||
var
|
||||
strVersion: string;
|
||||
begin
|
||||
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
|
||||
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Not even an old version installed
|
||||
Log('VC Redist x64 is not already installed');
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
10
host.yaml
@@ -21,7 +21,7 @@ server_options:
|
||||
location_check_points: 1
|
||||
# Relative point cost to receive a hint via !hint for players
|
||||
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
|
||||
hint_cost: 1000 # Set to 0 if you want free hints
|
||||
hint_cost: 10 # Set to 0 if you want free hints
|
||||
# Forfeit modes
|
||||
# "disabled" -> clients can't forfeit,
|
||||
# "enabled" -> clients can always forfeit
|
||||
@@ -63,10 +63,6 @@ multi_mystery_options:
|
||||
# If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml
|
||||
# as well as the generated pre-rolled yaml.
|
||||
pre_roll: false
|
||||
# Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
|
||||
# Does nothing if the name is not found
|
||||
# Example: player_name = "Berserker"
|
||||
player_name: "" # The hosts name
|
||||
# Create a spoiler file
|
||||
# 0 -> None
|
||||
# 1 -> Full spoiler
|
||||
@@ -113,8 +109,8 @@ multi_mystery_options:
|
||||
lttp_options:
|
||||
# File name of the v1.0 J rom
|
||||
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
|
||||
qusb2snes: "QUsb2Snes\\QUsb2Snes.exe"
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
|
||||
@@ -82,7 +82,7 @@ begin
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.28.29325') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
||||
@@ -82,7 +82,7 @@ begin
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.28.29325') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
||||
103
meta.yaml
@@ -11,54 +11,55 @@
|
||||
# inverted
|
||||
# This means, if mode is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead.
|
||||
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
|
||||
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
|
||||
on: 0 # Force every player into progression balancing
|
||||
off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK
|
||||
null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on
|
||||
goals:
|
||||
ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
fast_ganon: 250 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT
|
||||
dungeons: 50 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 100 # Pull the Triforce from the Master Sword pedestal
|
||||
triforce-hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ganon_pedestal: 10 # Pull the Master Sword pedestal, then kill Ganon
|
||||
null: 0 # Maintain individual goals
|
||||
mode:
|
||||
standard: 10
|
||||
open: 60
|
||||
inverted: 10
|
||||
null: 10 # Maintain individual world states
|
||||
tower_open:
|
||||
'0': 8
|
||||
'1': 7
|
||||
'2': 6
|
||||
'3': 5
|
||||
'4': 4
|
||||
'5': 3
|
||||
'6': 2
|
||||
'7': 1
|
||||
random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
|
||||
ganon_open:
|
||||
'0': 3
|
||||
'1': 4
|
||||
'2': 5
|
||||
'3': 6
|
||||
'4': 7
|
||||
'5': 8
|
||||
'6': 9
|
||||
'7': 10
|
||||
random: 5 # This will mean differing completion times. But leaving it for that surprise effect
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
30: 50
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 50
|
||||
# Do not use meta rom options at this time
|
||||
null:
|
||||
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
|
||||
on: 0 # Force every player into progression balancing
|
||||
off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK
|
||||
null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on
|
||||
A Link to the Past:
|
||||
goals:
|
||||
ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
fast_ganon: 250 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT
|
||||
dungeons: 50 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 100 # Pull the Triforce from the Master Sword pedestal
|
||||
triforce-hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ganon_pedestal: 10 # Pull the Master Sword pedestal, then kill Ganon
|
||||
null: 0 # Maintain individual goals
|
||||
mode:
|
||||
standard: 10
|
||||
open: 60
|
||||
inverted: 10
|
||||
null: 10 # Maintain individual world states
|
||||
tower_open:
|
||||
'0': 8
|
||||
'1': 7
|
||||
'2': 6
|
||||
'3': 5
|
||||
'4': 4
|
||||
'5': 3
|
||||
'6': 2
|
||||
'7': 1
|
||||
random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
|
||||
ganon_open:
|
||||
'0': 3
|
||||
'1': 4
|
||||
'2': 5
|
||||
'3': 6
|
||||
'4': 7
|
||||
'5': 8
|
||||
'6': 9
|
||||
'7': 10
|
||||
random: 5 # This will mean differing completion times. But leaving it for that surprise effect
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
30: 50
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 50
|
||||
@@ -25,9 +25,10 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
|
||||
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
|
||||
game:
|
||||
A Link to the Past: 1
|
||||
Hollow Knight: 1
|
||||
Factorio: 1
|
||||
Minecraft: 1
|
||||
requires:
|
||||
version: 0.1.3 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
@@ -36,437 +37,352 @@ accessibility:
|
||||
progression_balancing:
|
||||
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
# Can be uncommented to use it
|
||||
# startinventory: # Begin the file with the listed items/upgrades
|
||||
# The following 4 options can be uncommented and moved into a game's section they should affect
|
||||
# start_inventory: # Begin the file with the listed items/upgrades
|
||||
# Please only use items for the correct game, use triggers if need to be have seperated lists.
|
||||
# Pegasus Boots: on
|
||||
# Bomb Upgrade (+10): 4
|
||||
# Arrow Upgrade (+10): 4
|
||||
# start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client.
|
||||
# - Moon Pearl
|
||||
# Factorio options:
|
||||
tech_tree_layout:
|
||||
single: 1
|
||||
small_diamonds: 1
|
||||
medium_diamonds: 1
|
||||
pyramid: 1
|
||||
funnel: 1
|
||||
max_science_pack:
|
||||
automation_science_pack: 0
|
||||
logistic_science_pack: 0
|
||||
military_science_pack: 0
|
||||
chemical_science_pack: 0
|
||||
production_science_pack: 0
|
||||
utility_science_pack: 0
|
||||
space_science_pack: 1
|
||||
tech_cost:
|
||||
very_easy : 0
|
||||
easy : 0
|
||||
kind : 0
|
||||
normal : 1
|
||||
hard : 0
|
||||
very_hard : 0
|
||||
insane : 0
|
||||
free_samples:
|
||||
none: 1
|
||||
single_craft: 0
|
||||
half_stack: 0
|
||||
stack: 0
|
||||
visibility:
|
||||
none: 0
|
||||
sending: 1
|
||||
random_tech_ingredients:
|
||||
on: 1
|
||||
off: 0
|
||||
starting_items:
|
||||
burner-mining-drill: 19
|
||||
stone-furnace: 19
|
||||
# Minecraft options:
|
||||
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
|
||||
few: 0 # 30 advancements
|
||||
normal: 1 # 50
|
||||
many: 0 # 70
|
||||
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
|
||||
on: 0
|
||||
off: 1
|
||||
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
|
||||
on: 0
|
||||
off: 1
|
||||
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
on: 0
|
||||
off: 1
|
||||
# A Link to the Past options:
|
||||
### Logic Section ###
|
||||
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
none: 50 # No glitches required
|
||||
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
|
||||
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
|
||||
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
|
||||
# Other players items are placed into your world under OWG logic
|
||||
dark_room_logic: # Logic for unlit dark rooms
|
||||
lamp: 50 # require the Lamp for these rooms to be considered accessible.
|
||||
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
|
||||
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
|
||||
restrict_dungeon_item_on_boss: # aka ambrosia boss items
|
||||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
|
||||
off: 50
|
||||
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
local_keys: # Keep small keys and big keys local to your world
|
||||
on: 0
|
||||
off: 50
|
||||
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
|
||||
mc: 0 # Shuffle maps and compasses
|
||||
none: 50 # Shuffle none of the 4
|
||||
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
|
||||
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
|
||||
ub: 0 # universal small keys and shuffled big keys
|
||||
# you can add more combos of these letters here
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
default: 0 # Show when compass is picked up if the compass itself is shuffled
|
||||
off: 0 # Never show item count in dungeons
|
||||
progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
on: 50 # All items are progressive
|
||||
off: 0 # No items are progressive
|
||||
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
|
||||
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
|
||||
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
|
||||
restricted: 0 # Less strict than simple
|
||||
full: 0 # Less strict than restricted
|
||||
crossed: 0 # Less strict than full
|
||||
insanity: 0 # Very few grouping rules. Good luck
|
||||
# you can also define entrance shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
|
||||
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
goals:
|
||||
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
|
||||
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
|
||||
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
|
||||
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
||||
open_pyramid:
|
||||
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
||||
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
||||
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
||||
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
0: 0
|
||||
5: 50
|
||||
10: 50
|
||||
15: 0
|
||||
20: 0
|
||||
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
100: 0 #No extra
|
||||
150: 50 #Half the required will be added as extra
|
||||
200: 0 #There are the double of the required ones available.
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 0
|
||||
30: 50
|
||||
40: 0
|
||||
50: 0
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
15: 0
|
||||
20: 50
|
||||
30: 0
|
||||
40: 0
|
||||
50: 0
|
||||
tower_open: # Crystals required to open GT
|
||||
'0': 80
|
||||
'1': 70
|
||||
'2': 60
|
||||
'3': 50
|
||||
'4': 40
|
||||
'5': 30
|
||||
'6': 20
|
||||
'7': 10
|
||||
random: 0
|
||||
ganon_open: # Crystals required to hurt Ganon
|
||||
'0': 80
|
||||
'1': 70
|
||||
'2': 60
|
||||
'3': 50
|
||||
'4': 40
|
||||
'5': 30
|
||||
'6': 20
|
||||
'7': 10
|
||||
random: 0
|
||||
mode:
|
||||
standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
|
||||
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
|
||||
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
|
||||
retro:
|
||||
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
|
||||
off: 50
|
||||
hints:
|
||||
'on': 50 # Hint tiles sometimes give item location hints
|
||||
'off': 0 # Hint tiles provide gameplay tips
|
||||
swordless:
|
||||
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
off: 1
|
||||
item_pool:
|
||||
easy: 0 # Doubled upgrades, progressives, and etc
|
||||
normal: 50 # Item availability remains unchanged from vanilla game
|
||||
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
|
||||
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
|
||||
item_functionality:
|
||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||
normal: 50 # Vanilla item functionality
|
||||
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
|
||||
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
|
||||
tile_shuffle: # Randomize the tile layouts in flying tile rooms
|
||||
on: 0
|
||||
off: 50
|
||||
misery_mire_medallion: # required medallion to open Misery Mire front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
### Enemizer Section ###
|
||||
boss_shuffle:
|
||||
none: 50 # Vanilla bosses
|
||||
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
|
||||
normal: 0 # 3 bosses can occur twice
|
||||
chaos: 0 # Any boss can appear any amount of times
|
||||
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
|
||||
enemy_shuffle: # Randomize enemy placement
|
||||
on: 0
|
||||
off: 50
|
||||
killable_thieves: # Make thieves killable
|
||||
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
|
||||
off: 50
|
||||
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
|
||||
on: 0
|
||||
off: 50
|
||||
enemy_damage:
|
||||
default: 50 # Vanilla enemy damage
|
||||
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
|
||||
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
enemy_health:
|
||||
default: 50 # Vanilla enemy HP
|
||||
easy: 0 # Enemies have reduced health
|
||||
hard: 0 # Enemies have increased health
|
||||
expert: 0 # Enemies have greatly increased health
|
||||
pot_shuffle:
|
||||
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
|
||||
'off': 50 # Default pot item locations
|
||||
### End of Enemizer Section ###
|
||||
beemizer: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
|
||||
0: 50 # No bee traps are placed
|
||||
1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
|
||||
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
|
||||
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
|
||||
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
|
||||
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
|
||||
### Shop Settings ###
|
||||
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
random: 0 # 0 to 30 evenly distributed
|
||||
shop_shuffle:
|
||||
none: 50
|
||||
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
||||
f: 0 # Generate new default inventories for every shop independently
|
||||
i: 0 # Shuffle default inventories of the shops around
|
||||
p: 0 # Randomize the prices of the items in shop inventories
|
||||
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
|
||||
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
|
||||
ip: 0 # Shuffle inventories and randomize prices
|
||||
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
|
||||
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
|
||||
# You can add more combos
|
||||
### End of Shop Section ###
|
||||
shuffle_prizes: # aka drops
|
||||
none: 0 # do not shuffle prize packs
|
||||
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
|
||||
b: 0 # shuffle "bonk" prize packs
|
||||
bg: 0 # shuffle both
|
||||
timer:
|
||||
none: 50 # No timer will be displayed.
|
||||
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
|
||||
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
|
||||
ohko: 0 # Timer always at zero. Permanent OHKO.
|
||||
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
|
||||
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
|
||||
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
|
||||
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
|
||||
10: 50
|
||||
20: 0
|
||||
30: 0
|
||||
60: 0
|
||||
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
|
||||
-2: 50
|
||||
1: 0
|
||||
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
|
||||
1: 0
|
||||
2: 50
|
||||
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
|
||||
4: 50
|
||||
10: 0
|
||||
15: 0
|
||||
# Can be uncommented to use it
|
||||
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
|
||||
# - "Moon Pearl"
|
||||
# - "Small Keys"
|
||||
# - "Big Keys"
|
||||
glitch_boots:
|
||||
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
|
||||
off: 0
|
||||
# meta_ignore, linked_options and triggers work for any game
|
||||
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
|
||||
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
|
||||
# - "Progressive Weapons"
|
||||
|
||||
Factorio:
|
||||
tech_tree_layout:
|
||||
single: 1
|
||||
small_diamonds: 1
|
||||
medium_diamonds: 1
|
||||
large_diamonds: 1
|
||||
small_pyramids: 1
|
||||
medium_pyramids: 1
|
||||
large_pyramids: 1
|
||||
small_funnels: 1
|
||||
medium_funnels: 1
|
||||
large_funnels: 1
|
||||
recipe_time: # randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.
|
||||
vanilla: 1
|
||||
fast: 0 # 25% to 100% of original time
|
||||
normal: 0 # 50 % to 200% of original time
|
||||
slow: 0 # 100% 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:
|
||||
automation_science_pack: 0
|
||||
logistic_science_pack: 0
|
||||
military_science_pack: 0
|
||||
chemical_science_pack: 0
|
||||
production_science_pack: 0
|
||||
utility_science_pack: 0
|
||||
space_science_pack: 1
|
||||
tech_cost:
|
||||
very_easy : 0
|
||||
easy : 0
|
||||
kind : 0
|
||||
normal : 1
|
||||
hard : 0
|
||||
very_hard : 0
|
||||
insane : 0
|
||||
free_samples:
|
||||
none: 1
|
||||
single_craft: 0
|
||||
half_stack: 0
|
||||
stack: 0
|
||||
progressive:
|
||||
on: 1
|
||||
off: 0
|
||||
tech_tree_information:
|
||||
none: 0
|
||||
advancement: 0 # show which items are a logical advancement
|
||||
full: 1 # show full info on each tech node
|
||||
imported_blueprints: # can be turned off to prevent access to blueprints created outside the current world
|
||||
on: 1
|
||||
off: 0
|
||||
starting_items:
|
||||
burner-mining-drill: 19
|
||||
stone-furnace: 19
|
||||
Minecraft:
|
||||
advancement_goal: 50 # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
|
||||
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
|
||||
on: 0
|
||||
off: 1
|
||||
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
|
||||
on: 0
|
||||
off: 1
|
||||
shuffle_structures: # Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
on: 0
|
||||
off: 1
|
||||
A Link to the Past:
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
none: 50 # No glitches required
|
||||
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
|
||||
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
|
||||
hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
|
||||
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
|
||||
# Other players items are placed into your world under HMG logic
|
||||
dark_room_logic: # Logic for unlit dark rooms
|
||||
lamp: 50 # require the Lamp for these rooms to be considered accessible.
|
||||
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
|
||||
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
|
||||
restrict_dungeon_item_on_boss: # aka ambrosia boss items
|
||||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
|
||||
off: 50
|
||||
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
local_keys: # Keep small keys and big keys local to your world
|
||||
on: 0
|
||||
off: 50
|
||||
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
|
||||
mc: 0 # Shuffle maps and compasses
|
||||
none: 50 # Shuffle none of the 4
|
||||
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
|
||||
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
|
||||
ub: 0 # universal small keys and shuffled big keys
|
||||
# you can add more combos of these letters here
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
default: 0 # Show when compass is picked up if the compass itself is shuffled
|
||||
off: 0 # Never show item count in dungeons
|
||||
progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
on: 50 # All items are progressive
|
||||
off: 0 # No items are progressive
|
||||
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
|
||||
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
|
||||
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
|
||||
restricted: 0 # Less strict than simple
|
||||
full: 0 # Less strict than restricted
|
||||
crossed: 0 # Less strict than full
|
||||
insanity: 0 # Very few grouping rules. Good luck
|
||||
# you can also define entrance shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
|
||||
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
goals:
|
||||
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
|
||||
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
|
||||
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
|
||||
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
||||
open_pyramid:
|
||||
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
||||
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
||||
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
||||
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
0: 0
|
||||
5: 50
|
||||
10: 50
|
||||
15: 0
|
||||
20: 0
|
||||
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
100: 0 #No extra
|
||||
150: 50 #Half the required will be added as extra
|
||||
200: 0 #There are the double of the required ones available.
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 0
|
||||
30: 50
|
||||
40: 0
|
||||
50: 0
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
15: 0
|
||||
20: 50
|
||||
30: 0
|
||||
40: 0
|
||||
50: 0
|
||||
crystals_needed_for_gt: # Crystals required to open GT
|
||||
0: 0
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
5: 0
|
||||
6: 0
|
||||
7: 0
|
||||
random: 0
|
||||
random-low: 50 # any valid number, weighted towards the lower end
|
||||
random-middle: 0 # any valid number, weighted towards the central range
|
||||
random-high: 0 # any valid number, weighted towards the higher end
|
||||
crystals_needed_for_ganon: # Crystals required to hurt Ganon
|
||||
0: 0
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
5: 0
|
||||
6: 0
|
||||
7: 0
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 50
|
||||
mode:
|
||||
- inverted # Never play inverted seeds
|
||||
standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
|
||||
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
|
||||
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
|
||||
retro:
|
||||
- on # Never play retro seeds
|
||||
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
|
||||
off: 50
|
||||
hints:
|
||||
'on': 50 # Hint tiles sometimes give item location hints
|
||||
'off': 0 # Hint tiles provide gameplay tips
|
||||
swordless:
|
||||
- on # Never play a swordless seed
|
||||
linked_options:
|
||||
- name: crosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
percentage: 0 # Set this to the percentage chance you want crosskeys
|
||||
- name: localcrosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
local_items: # Forces keys to be local to your own world
|
||||
- "Small Keys"
|
||||
- "Big Keys"
|
||||
percentage: 0 # Set this to the percentage chance you want local crosskeys
|
||||
- name: enemizer
|
||||
options:
|
||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||
basic: 1
|
||||
full: 1
|
||||
chaos: 1
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
random: 1
|
||||
enemy_health:
|
||||
easy: 1
|
||||
hard: 1
|
||||
expert: 1
|
||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||
# triggers that replace options upon rolling certain options
|
||||
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
|
||||
trigger_disabled: 50
|
||||
randomized: 0 # Swords are placed randomly throughout the world
|
||||
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # swordless mode
|
||||
triggers:
|
||||
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
|
||||
- option_name: legacy_weapons
|
||||
option_result: randomized
|
||||
options:
|
||||
swordless: off
|
||||
- option_name: legacy_weapons
|
||||
option_result: assured
|
||||
options:
|
||||
swordless: off
|
||||
startinventory:
|
||||
Progressive Sword: 1
|
||||
- option_name: legacy_weapons
|
||||
option_result: vanilla
|
||||
options:
|
||||
swordless: off
|
||||
plando_items:
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Pyramid Fairy - Left
|
||||
- Blacksmith
|
||||
- Link's Uncle
|
||||
- option_name: legacy_weapons
|
||||
option_result: swordless
|
||||
options:
|
||||
swordless: on
|
||||
# end of legacy weapons block
|
||||
- option_name: enemy_damage # targets enemy_damage
|
||||
option_result: shuffled # if it rolls shuffled
|
||||
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
||||
options: # then inserts these options
|
||||
swordless: off
|
||||
### door rando only options (not supported at all yet on this branch) ###
|
||||
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
vanilla: 50 # Everything should be like in vanilla
|
||||
basic: 0 # Dungeons are shuffled within themselves
|
||||
crossed: 0 # Dungeons are shuffled across each other
|
||||
# you can also define door shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same dungeon layout as another player and share dungeon layout information.
|
||||
# however, other settings like intensity, universal keys, etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
intensity: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
1: 50 # Shuffles normal doors and spiral staircases
|
||||
2: 0 # And shuffles open edges and straight staircases
|
||||
3: 0 # And shuffles dungeon lobbies
|
||||
random: 0 # Picks one of those at random
|
||||
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249.
|
||||
off: 50
|
||||
experimental: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
on: 0 # Enables experimental features.
|
||||
off: 50
|
||||
debug: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
on: 0 # Enables debugging features. Currently, these are the Item collection counter. (overwrites total triforce pieces) and Castle Gate closed indicator.
|
||||
off: 50
|
||||
### end of door rando only options ###
|
||||
rom:
|
||||
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
off: 1
|
||||
item_pool:
|
||||
easy: 0 # Doubled upgrades, progressives, and etc
|
||||
normal: 50 # Item availability remains unchanged from vanilla game
|
||||
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
|
||||
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
|
||||
item_functionality:
|
||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||
normal: 50 # Vanilla item functionality
|
||||
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
|
||||
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
|
||||
tile_shuffle: # Randomize the tile layouts in flying tile rooms
|
||||
on: 0
|
||||
off: 50
|
||||
misery_mire_medallion: # required medallion to open Misery Mire front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
### Enemizer Section ###
|
||||
boss_shuffle:
|
||||
none: 50 # Vanilla bosses
|
||||
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
|
||||
full: 0 # 3 bosses can occur twice
|
||||
chaos: 0 # Any boss can appear any amount of times
|
||||
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
|
||||
enemy_shuffle: # Randomize enemy placement
|
||||
on: 0
|
||||
off: 50
|
||||
killable_thieves: # Make thieves killable
|
||||
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
|
||||
off: 50
|
||||
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
|
||||
on: 0
|
||||
off: 50
|
||||
enemy_damage:
|
||||
default: 50 # Vanilla enemy damage
|
||||
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
|
||||
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
enemy_health:
|
||||
default: 50 # Vanilla enemy HP
|
||||
easy: 0 # Enemies have reduced health
|
||||
hard: 0 # Enemies have increased health
|
||||
expert: 0 # Enemies have greatly increased health
|
||||
pot_shuffle:
|
||||
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
|
||||
'off': 50 # Default pot item locations
|
||||
### End of Enemizer Section ###
|
||||
beemizer: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
|
||||
0: 50 # No bee traps are placed
|
||||
1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
|
||||
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
|
||||
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
|
||||
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
|
||||
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
|
||||
### Shop Settings ###
|
||||
shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
random: 0 # 0 to 30 evenly distributed
|
||||
shop_shuffle:
|
||||
none: 50
|
||||
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
||||
f: 0 # Generate new default inventories for every shop independently
|
||||
i: 0 # Shuffle default inventories of the shops around
|
||||
p: 0 # Randomize the prices of the items in shop inventories
|
||||
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
|
||||
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
|
||||
ip: 0 # Shuffle inventories and randomize prices
|
||||
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
|
||||
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
|
||||
# You can add more combos
|
||||
### End of Shop Section ###
|
||||
shuffle_prizes: # aka drops
|
||||
none: 0 # do not shuffle prize packs
|
||||
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
|
||||
b: 0 # shuffle "bonk" prize packs
|
||||
bg: 0 # shuffle both
|
||||
timer:
|
||||
none: 50 # No timer will be displayed.
|
||||
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
|
||||
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
|
||||
ohko: 0 # Timer always at zero. Permanent OHKO.
|
||||
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
|
||||
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
|
||||
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
|
||||
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
|
||||
10: 50
|
||||
20: 0
|
||||
30: 0
|
||||
60: 0
|
||||
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
|
||||
-2: 50
|
||||
1: 0
|
||||
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
|
||||
1: 0
|
||||
2: 50
|
||||
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
|
||||
4: 50
|
||||
10: 0
|
||||
15: 0
|
||||
glitch_boots:
|
||||
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
|
||||
off: 0
|
||||
# rom options section
|
||||
random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
|
||||
enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
|
||||
on: 0
|
||||
@@ -508,7 +424,7 @@ rom:
|
||||
randomonslash: 0 # Random sprite on sword slashes
|
||||
randomonitem: 0 # Random sprite on getting items.
|
||||
randomonbonk: 0 # Random sprite on bonk.
|
||||
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
|
||||
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
|
||||
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
|
||||
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
|
||||
disablemusic: # If "on", all in-game music will be disabled
|
||||
@@ -594,3 +510,102 @@ rom:
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
|
||||
# triggers that replace options upon rolling certain options
|
||||
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
|
||||
trigger_disabled: 50
|
||||
randomized: 0 # Swords are placed randomly throughout the world
|
||||
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # swordless mode
|
||||
# meta_ignore, linked_options and triggers work for any game
|
||||
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
|
||||
mode:
|
||||
- inverted # Never play inverted seeds
|
||||
retro:
|
||||
- on # Never play retro seeds
|
||||
swordless:
|
||||
- on # Never play a swordless seed
|
||||
|
||||
linked_options:
|
||||
- name: crosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
percentage: 0 # Set this to the percentage chance you want crosskeys
|
||||
- name: localcrosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
local_items: # Forces keys to be local to your own world
|
||||
- "Small Keys"
|
||||
- "Big Keys"
|
||||
percentage: 0 # Set this to the percentage chance you want local crosskeys
|
||||
- name: enemizer
|
||||
options:
|
||||
A Link to the Past:
|
||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||
basic: 1
|
||||
full: 1
|
||||
chaos: 1
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
random: 1
|
||||
enemy_health:
|
||||
easy: 1
|
||||
hard: 1
|
||||
expert: 1
|
||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||
triggers:
|
||||
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
|
||||
- option_name: legacy_weapons
|
||||
option_result: randomized
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
- option_name: legacy_weapons
|
||||
option_result: assured
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
start_inventory:
|
||||
Progressive Sword: 1
|
||||
- option_name: legacy_weapons
|
||||
option_result: vanilla
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
plando_items:
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Pyramid Fairy - Left
|
||||
- Blacksmith
|
||||
- Link's Uncle
|
||||
- option_name: legacy_weapons
|
||||
option_result: swordless
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: on
|
||||
# end of legacy weapons block
|
||||
- option_name: enemy_damage # targets enemy_damage
|
||||
option_category: A Link to the Past
|
||||
option_result: shuffled # if it rolls shuffled
|
||||
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
||||
options: # then inserts these options
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
@@ -1,11 +1,7 @@
|
||||
colorama>=0.4.4
|
||||
websockets>=9.0.2
|
||||
websockets>=9.1
|
||||
PyYAML>=5.4.1
|
||||
fuzzywuzzy>=0.18.0
|
||||
bsdiff4>=1.2.1
|
||||
prompt_toolkit>=3.0.18
|
||||
prompt_toolkit>=3.0.19
|
||||
appdirs>=1.4.4
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
xxtea>=2.0.0.post0
|
||||
factorio-rcon-py>=1.2.1
|
||||
jinja2>=3.0.0
|
||||
jinja2>=3.0.1
|
||||
115
setup.py
@@ -38,20 +38,20 @@ def _threaded_hash(filepath):
|
||||
os.makedirs(buildfolder, exist_ok=True)
|
||||
|
||||
|
||||
def manifest_creation():
|
||||
def manifest_creation(folder):
|
||||
hashes = {}
|
||||
manifestpath = os.path.join(buildfolder, "manifest.json")
|
||||
manifestpath = os.path.join(folder, "manifest.json")
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
pool = ThreadPoolExecutor()
|
||||
for dirpath, dirnames, filenames in os.walk(buildfolder):
|
||||
for dirpath, dirnames, filenames in os.walk(folder):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path)
|
||||
hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path)
|
||||
import json
|
||||
from Utils import _version_tuple
|
||||
from Utils import version_tuple
|
||||
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
|
||||
"hashes": {path: hash.result() for path, hash in hashes.items()},
|
||||
"version": _version_tuple}
|
||||
"version": version_tuple}
|
||||
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
||||
print("Created Manifest")
|
||||
|
||||
@@ -88,7 +88,7 @@ cx_Freeze.setup(
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": [],
|
||||
"zip_exclude_packages": ["worlds"],
|
||||
"include_files": [],
|
||||
"include_msvcr": True,
|
||||
"replace_paths": [("*", "")],
|
||||
@@ -113,7 +113,7 @@ def installfile(path, keep_content=False):
|
||||
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:
|
||||
installfile(Path(data))
|
||||
@@ -131,34 +131,91 @@ else:
|
||||
file = z3pr.__file__
|
||||
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:
|
||||
for exe in exes:
|
||||
print(f"Signing {exe.target_name}")
|
||||
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
||||
print(f"Signing QUsb2Snes")
|
||||
os.system(signtool + os.path.join(buildfolder, "Qusb2Snes", "QUsb2Snes.exe"))
|
||||
print(f"Signing SNI")
|
||||
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
|
||||
|
||||
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
|
||||
for file in os.listdir(alttpr_sprites_folder):
|
||||
if file != ".gitignore":
|
||||
os.remove(alttpr_sprites_folder / file)
|
||||
|
||||
manifest_creation()
|
||||
manifest_creation(buildfolder)
|
||||
|
||||
buildfolder = Path("build_factorio", folder)
|
||||
sbuildfolder = str(buildfolder)
|
||||
libfolder = Path(buildfolder, "lib")
|
||||
library = Path(libfolder, "library.zip")
|
||||
print("Outputting Factorio Client to: " + sbuildfolder)
|
||||
|
||||
os.makedirs(buildfolder, exist_ok=True)
|
||||
|
||||
scripts = {"FactorioClient.py": "ArchipelagoConsoleFactorioClient"}
|
||||
|
||||
exes = []
|
||||
|
||||
for script, scriptname in scripts.items():
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script=script,
|
||||
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
|
||||
icon=icon,
|
||||
))
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script="FactorioClientGUI.py",
|
||||
target_name="ArchipelagoGraphicalFactorioClient" + ("" if sys.platform == "linux" else ".exe"),
|
||||
icon=icon,
|
||||
base="Win32GUI"
|
||||
))
|
||||
|
||||
import datetime
|
||||
|
||||
buildtime = datetime.datetime.utcnow()
|
||||
|
||||
cx_Freeze.setup(
|
||||
name="Archipelago Factorio Client",
|
||||
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
|
||||
description="Archipelago Factorio Client",
|
||||
executables=exes,
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["websockets", "kivy"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["kivy", "worlds"],
|
||||
"include_files": [],
|
||||
"include_msvcr": True,
|
||||
"replace_paths": [("*", "")],
|
||||
"optimize": 2,
|
||||
"build_exe": buildfolder
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"]
|
||||
from kivy_deps import sdl2, glew
|
||||
for folder in sdl2.dep_bins+glew.dep_bins:
|
||||
shutil.copytree(folder, buildfolder, dirs_exist_ok=True)
|
||||
for data in extra_data:
|
||||
installfile(Path(data))
|
||||
|
||||
|
||||
os.makedirs(buildfolder / "Players", exist_ok=True)
|
||||
shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml")
|
||||
|
||||
if signtool:
|
||||
for exe in exes:
|
||||
print(f"Signing {exe.target_name}")
|
||||
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
||||
|
||||
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
|
||||
for file in os.listdir(alttpr_sprites_folder):
|
||||
if file != ".gitignore":
|
||||
os.remove(alttpr_sprites_folder / file)
|
||||
|
||||
manifest_creation(buildfolder)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
@@ -8,11 +9,16 @@ from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import create_regions
|
||||
from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class TestDungeon(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
args = Namespace()
|
||||
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.remove_exits = [] # Block dungeon exits
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from worlds.hk import HKWorld
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.hk.Regions import create_regions
|
||||
from worlds.hk import gen_hollow
|
||||
from worlds import AutoWorld
|
||||
from worlds.hk.Options import hollow_knight_randomize_options, hollow_knight_skip_options
|
||||
|
||||
from test.TestBase import TestBase
|
||||
|
||||
@@ -9,8 +10,11 @@ class TestVanilla(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.game[1] = "Hollow Knight"
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_randomize_options:
|
||||
getattr(self.world, hk_option)[1] = True
|
||||
create_regions(self.world, 1)
|
||||
gen_hollow(self.world, 1)
|
||||
self.world.worlds[1] = HKWorld(self.world, 1)
|
||||
for hk_option in hollow_knight_randomize_options:
|
||||
setattr(self.world, hk_option, {1: True})
|
||||
for hk_option, option in hollow_knight_skip_options.items():
|
||||
setattr(self.world, hk_option, {1: option.default})
|
||||
AutoWorld.call_single(self.world, "create_regions", 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 worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||
@@ -9,10 +11,15 @@ from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
class TestInverted(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
args = Namespace()
|
||||
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.mode[1] = "inverted"
|
||||
create_inverted_regions(self.world, 1)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||
@@ -9,10 +11,15 @@ from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
class TestInvertedMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
args = Namespace()
|
||||
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.logic[1] = "minorglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||
@@ -9,10 +11,16 @@ from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class TestInvertedOWG(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
args = Namespace()
|
||||
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.mode[1] = "inverted"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
||||
@@ -201,14 +201,9 @@ class TestAdvancements(TestMinecraft):
|
||||
["Hot Tourist Destinations", False, [], ['Ingot Crafting']],
|
||||
["Hot Tourist Destinations", False, [], ['Flint and Steel']],
|
||||
["Hot Tourist Destinations", False, [], ['Progressive Tools']],
|
||||
["Hot Tourist Destinations", False, [], ['Progressive Weapons']],
|
||||
["Hot Tourist Destinations", False, [], ['Progressive Armor', 'Shield']],
|
||||
["Hot Tourist Destinations", False, [], ['Fishing Rod']],
|
||||
["Hot Tourist Destinations", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Bucket', 'Fishing Rod']],
|
||||
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']],
|
||||
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Flint and Steel', 'Bucket', 'Fishing Rod']],
|
||||
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']],
|
||||
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']],
|
||||
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']],
|
||||
])
|
||||
|
||||
def test_42015(self):
|
||||
@@ -979,7 +974,8 @@ class TestAdvancements(TestMinecraft):
|
||||
["Sticky Situation", False, []],
|
||||
["Sticky Situation", False, [], ['Bottles']],
|
||||
["Sticky Situation", False, [], ['Ingot Crafting']],
|
||||
["Sticky Situation", True, ['Bottles', 'Ingot Crafting']],
|
||||
["Sticky Situation", False, [], ['Campfire']],
|
||||
["Sticky Situation", True, ['Bottles', 'Ingot Crafting', 'Campfire']],
|
||||
])
|
||||
|
||||
def test_42075(self):
|
||||
@@ -1099,16 +1095,17 @@ class TestAdvancements(TestMinecraft):
|
||||
self.run_location_tests([
|
||||
["When Pigs Fly", False, []],
|
||||
["When Pigs Fly", False, [], ['Ingot Crafting']],
|
||||
["When Pigs Fly", False, [], ['Flint and Steel']],
|
||||
["When Pigs Fly", False, [], ['Progressive Tools']],
|
||||
["When Pigs Fly", False, [], ['Progressive Weapons']],
|
||||
["When Pigs Fly", False, [], ['Progressive Armor', 'Shield']],
|
||||
["When Pigs Fly", False, [], ['Fishing Rod']],
|
||||
["When Pigs Fly", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
["When Pigs Fly", False, ['Progressive Weapons'], ['Flint and Steel', 'Progressive Weapons', 'Progressive Weapons']],
|
||||
["When Pigs Fly", False, ['Progressive Tools', 'Progressive Tools', 'Progressive Weapons'], ['Bucket', 'Progressive Tools', 'Progressive Weapons', 'Progressive Weapons']],
|
||||
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']],
|
||||
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']],
|
||||
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']],
|
||||
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Fishing Rod']],
|
||||
["When Pigs Fly", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Shield', 'Ingot Crafting', 'Progressive Tools', 'Fishing Rod']],
|
||||
])
|
||||
|
||||
def test_42089(self):
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import worlds.minecraft.Options
|
||||
from test.TestBase import TestBase
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.minecraft import minecraft_gen_item_pool
|
||||
from worlds.minecraft.Regions import minecraft_create_regions, link_minecraft_structures
|
||||
from worlds.minecraft.Rules import set_rules
|
||||
from worlds import AutoWorld
|
||||
from worlds.minecraft import MinecraftWorld
|
||||
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
|
||||
def MCItemFactory(items, player: int):
|
||||
@@ -28,16 +29,17 @@ class TestMinecraft(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.game[1] = "Minecraft"
|
||||
self.world.worlds[1] = MinecraftWorld(self.world, 1)
|
||||
exclusion_pools = ['hard', 'insane', 'postgame']
|
||||
for pool in exclusion_pools:
|
||||
setattr(self.world, f"include_{pool}_advancements", [False, False])
|
||||
setattr(self.world, "advancement_goal", [0, Options.AdvancementGoal(value=0)])
|
||||
setattr(self.world, "shuffle_structures", [False, False])
|
||||
setattr(self.world, "combat_difficulty", [0, Options.CombatDifficulty(value=1)])
|
||||
minecraft_create_regions(self.world, 1)
|
||||
link_minecraft_structures(self.world, 1)
|
||||
minecraft_gen_item_pool(self.world, 1)
|
||||
set_rules(self.world, 1)
|
||||
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
|
||||
setattr(self.world, "shuffle_structures", {1: Toggle(False)})
|
||||
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
|
||||
setattr(self.world, "bee_traps", {1: Toggle(False)})
|
||||
AutoWorld.call_single(self.world, "create_regions", 1)
|
||||
AutoWorld.call_single(self.world, "generate_basic", 1)
|
||||
AutoWorld.call_single(self.world, "set_rules", 1)
|
||||
|
||||
def _get_items(self, item_pool, all_except):
|
||||
if all_except and len(all_except) > 0:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_entrances
|
||||
@@ -9,10 +11,15 @@ from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
class TestMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
args = Namespace()
|
||||
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.difficulty_requirements[1] = difficulties['normal']
|
||||
create_regions(self.world, 1)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_entrances
|
||||
@@ -9,10 +11,16 @@ from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class TestVanillaOWG(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
args = Namespace()
|
||||
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.logic[1] = "owglitches"
|
||||
create_regions(self.world, 1)
|
||||
|
||||