mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-27 19:23:27 -07:00
Compare commits
243 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 | ||
|
|
b43e4fae86 | ||
|
|
1f17aa394e | ||
|
|
a1d7bc558c | ||
|
|
de31fc320c | ||
|
|
685de847c4 | ||
|
|
40751f267b | ||
|
|
3e1941a561 | ||
|
|
8e27ad3547 | ||
|
|
c4f5db9c84 | ||
|
|
19896e1fae | ||
|
|
23678b814d | ||
|
|
13fe1f2ea2 | ||
|
|
c24d6a0785 | ||
|
|
b2f3fd56f4 | ||
|
|
b82d6cec31 | ||
|
|
c5ff962ea1 | ||
|
|
4aa56c1a7f | ||
|
|
681279cb2b | ||
|
|
c4ea879651 | ||
|
|
8cdf9d2ddc | ||
|
|
daa959e353 | ||
|
|
d5cdff5ec9 | ||
|
|
109eb5b9dc | ||
|
|
fb192b989d | ||
|
|
d35adc5868 | ||
|
|
c0bf4f58ad | ||
|
|
f24a81fdaf | ||
|
|
40ff0e867c | ||
|
|
a231850911 | ||
|
|
1b2283b173 | ||
|
|
729088fd85 | ||
|
|
88d75a41ae | ||
|
|
237b44ca66 | ||
|
|
6fef30d9b3 |
112
.gitignore
vendored
112
.gitignore
vendored
@@ -12,14 +12,16 @@
|
|||||||
*.db3
|
*.db3
|
||||||
*multidata
|
*multidata
|
||||||
*multisave
|
*multisave
|
||||||
|
*.archipelago
|
||||||
|
*.apsave
|
||||||
|
|
||||||
build
|
build
|
||||||
|
/build_factorio/
|
||||||
bundle/components.wxs
|
bundle/components.wxs
|
||||||
dist
|
dist
|
||||||
README.html
|
README.html
|
||||||
.vs/
|
.vs/
|
||||||
EnemizerCLI/
|
EnemizerCLI/
|
||||||
.mypy_cache/
|
|
||||||
RaceRom.py
|
RaceRom.py
|
||||||
weights/
|
weights/
|
||||||
/MultiMystery/
|
/MultiMystery/
|
||||||
@@ -36,3 +38,111 @@ success.txt
|
|||||||
output/
|
output/
|
||||||
Output Logs/
|
Output Logs/
|
||||||
/factorio/
|
/factorio/
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
Archipelago.zip
|
||||||
|
|||||||
256
BaseClasses.py
256
BaseClasses.py
@@ -23,6 +23,7 @@ class MultiWorld():
|
|||||||
plando_items: List[PlandoItem]
|
plando_items: List[PlandoItem]
|
||||||
plando_connections: List[PlandoConnection]
|
plando_connections: List[PlandoConnection]
|
||||||
er_seeds: Dict[int, str]
|
er_seeds: Dict[int, str]
|
||||||
|
worlds: Dict[int, "AutoWorld.World"]
|
||||||
|
|
||||||
class AttributeProxy():
|
class AttributeProxy():
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
@@ -32,8 +33,6 @@ class MultiWorld():
|
|||||||
return self.rule(player)
|
return self.rule(player)
|
||||||
|
|
||||||
def __init__(self, players: int):
|
def __init__(self, players: int):
|
||||||
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
|
|
||||||
|
|
||||||
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
||||||
self.players = players
|
self.players = players
|
||||||
self.teams = 1
|
self.teams = 1
|
||||||
@@ -44,6 +43,7 @@ class MultiWorld():
|
|||||||
self.shops = []
|
self.shops = []
|
||||||
self.itempool = []
|
self.itempool = []
|
||||||
self.seed = None
|
self.seed = None
|
||||||
|
self.seed_name: str = "Unavailable"
|
||||||
self.precollected_items = []
|
self.precollected_items = []
|
||||||
self.state = CollectionState(self)
|
self.state = CollectionState(self)
|
||||||
self._cached_entrances = None
|
self._cached_entrances = None
|
||||||
@@ -112,8 +112,6 @@ class MultiWorld():
|
|||||||
set_player_attr('bush_shuffle', False)
|
set_player_attr('bush_shuffle', False)
|
||||||
set_player_attr('beemizer', 0)
|
set_player_attr('beemizer', 0)
|
||||||
set_player_attr('escape_assist', [])
|
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('open_pyramid', False)
|
||||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||||
set_player_attr('treasure_hunt_count', 0)
|
set_player_attr('treasure_hunt_count', 0)
|
||||||
@@ -130,7 +128,6 @@ class MultiWorld():
|
|||||||
set_player_attr('triforce_pieces_available', 30)
|
set_player_attr('triforce_pieces_available', 30)
|
||||||
set_player_attr('triforce_pieces_required', 20)
|
set_player_attr('triforce_pieces_required', 20)
|
||||||
set_player_attr('shop_shuffle', 'off')
|
set_player_attr('shop_shuffle', 'off')
|
||||||
set_player_attr('shop_shuffle_slots', 0)
|
|
||||||
set_player_attr('shuffle_prizes', "g")
|
set_player_attr('shuffle_prizes', "g")
|
||||||
set_player_attr('sprite_pool', [])
|
set_player_attr('sprite_pool', [])
|
||||||
set_player_attr('dark_room_logic', "lamp")
|
set_player_attr('dark_room_logic', "lamp")
|
||||||
@@ -140,39 +137,42 @@ class MultiWorld():
|
|||||||
set_player_attr('plando_connections', [])
|
set_player_attr('plando_connections', [])
|
||||||
set_player_attr('game', "A Link to the Past")
|
set_player_attr('game', "A Link to the Past")
|
||||||
set_player_attr('completion_condition', lambda state: True)
|
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 = {}
|
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.custom_data[player] = {}
|
||||||
# self.worlds = []
|
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||||
# for i in range(players):
|
for option in world_type.options:
|
||||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
setattr(self, option, getattr(args, option, {}))
|
||||||
|
self.worlds[player] = world_type(self, player)
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = secrets.SystemRandom()
|
self.random = secrets.SystemRandom()
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def player_ids(self):
|
def player_ids(self):
|
||||||
yield from range(1, self.players + 1)
|
return tuple(range(1, self.players + 1))
|
||||||
|
|
||||||
@property
|
# Todo: make these automatic, or something like get_players_for_game(game_name)
|
||||||
|
@functools.cached_property
|
||||||
def alttp_player_ids(self):
|
def alttp_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def hk_player_ids(self):
|
def hk_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def factorio_player_ids(self):
|
def factorio_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def minecraft_player_ids(self):
|
def minecraft_player_ids(self):
|
||||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
|
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
|
||||||
|
|
||||||
|
|
||||||
def get_name_string_for_object(self, obj) -> str:
|
def get_name_string_for_object(self, obj) -> str:
|
||||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||||
@@ -237,53 +237,12 @@ class MultiWorld():
|
|||||||
def get_all_state(self, keys=False) -> CollectionState:
|
def get_all_state(self, keys=False) -> CollectionState:
|
||||||
ret = CollectionState(self)
|
ret = CollectionState(self)
|
||||||
|
|
||||||
def soft_collect(item):
|
|
||||||
if item.game == "A Link to the Past" and item.name.startswith('Progressive '):
|
|
||||||
# ALttP items
|
|
||||||
if 'Sword' in item.name:
|
|
||||||
if ret.has('Golden Sword', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[
|
|
||||||
item.player].progressive_sword_limit >= 4:
|
|
||||||
ret.prog_items['Golden Sword', item.player] += 1
|
|
||||||
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[
|
|
||||||
item.player].progressive_sword_limit >= 3:
|
|
||||||
ret.prog_items['Tempered Sword', item.player] += 1
|
|
||||||
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
|
||||||
ret.prog_items['Master Sword', item.player] += 1
|
|
||||||
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
|
||||||
ret.prog_items['Fighter Sword', item.player] += 1
|
|
||||||
elif 'Glove' in item.name:
|
|
||||||
if ret.has('Titans Mitts', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Power Glove', item.player):
|
|
||||||
ret.prog_items['Titans Mitts', item.player] += 1
|
|
||||||
else:
|
|
||||||
ret.prog_items['Power Glove', item.player] += 1
|
|
||||||
elif 'Shield' in item.name:
|
|
||||||
if ret.has('Mirror Shield', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
|
||||||
ret.prog_items['Mirror Shield', item.player] += 1
|
|
||||||
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
|
||||||
ret.prog_items['Red Shield', item.player] += 1
|
|
||||||
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
|
||||||
ret.prog_items['Blue Shield', item.player] += 1
|
|
||||||
elif 'Bow' in item.name:
|
|
||||||
if ret.has('Silver', item.player):
|
|
||||||
pass
|
|
||||||
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
|
||||||
ret.prog_items['Silver Bow', item.player] += 1
|
|
||||||
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
|
||||||
ret.prog_items['Bow', item.player] += 1
|
|
||||||
elif item.advancement or item.smallkey or item.bigkey:
|
|
||||||
ret.prog_items[item.name, item.player] += 1
|
|
||||||
|
|
||||||
for item in self.itempool:
|
for item in self.itempool:
|
||||||
soft_collect(item)
|
self.worlds[item.player].collect(ret, item)
|
||||||
|
|
||||||
if keys:
|
if keys:
|
||||||
for p in self.alttp_player_ids:
|
for p in self.alttp_player_ids:
|
||||||
|
world = self.worlds[p]
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
for item in ItemFactory(
|
for item in ItemFactory(
|
||||||
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
||||||
@@ -298,7 +257,7 @@ class MultiWorld():
|
|||||||
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
||||||
'Small Key (Ganons Tower)'] * 4,
|
'Small Key (Ganons Tower)'] * 4,
|
||||||
p):
|
p):
|
||||||
soft_collect(item)
|
world.collect(ret, item)
|
||||||
ret.sweep_for_events()
|
ret.sweep_for_events()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -812,6 +771,9 @@ class CollectionState(object):
|
|||||||
rules.append(self.has('Moon Pearl', player))
|
rules.append(self.has('Moon Pearl', player))
|
||||||
return all(rules)
|
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
|
# Minecraft logic functions
|
||||||
def has_iron_ingots(self, player: int):
|
def has_iron_ingots(self, player: int):
|
||||||
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
|
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
|
||||||
@@ -879,81 +841,32 @@ class CollectionState(object):
|
|||||||
self.has('Progressive Armor', player) and self.has('Shield', player)
|
self.has('Progressive Armor', player) and self.has('Shield', player)
|
||||||
|
|
||||||
def can_kill_wither(self, player: int):
|
def can_kill_wither(self, player: int):
|
||||||
build_wither = self.fortress_loot(player) and (self.can_reach('The Nether', 'Region', player) or self.can_piglin_trade(player))
|
|
||||||
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
|
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
|
||||||
if self.combat_difficulty(player) == 'easy':
|
if self.combat_difficulty(player) == 'easy':
|
||||||
return build_wither and normal_kill and self.has('Archery', player)
|
return self.fortress_loot(player) and normal_kill and self.has('Archery', player)
|
||||||
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
|
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
|
||||||
return build_wither and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
|
return self.fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
|
||||||
return build_wither and normal_kill
|
return self.fortress_loot(player) and normal_kill
|
||||||
|
|
||||||
def can_kill_ender_dragon(self, player: int):
|
def can_kill_ender_dragon(self, player: int):
|
||||||
|
# Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned.
|
||||||
|
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Ingot Crafting', player)
|
||||||
if self.combat_difficulty(player) == 'easy':
|
if self.combat_difficulty(player) == 'easy':
|
||||||
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.has('Archery', player) and \
|
return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
|
||||||
self.can_brew_potions(player) and self.can_enchant(player)
|
self.has('Archery', player) and self.can_brew_potions(player) and self.can_enchant(player)
|
||||||
if self.combat_difficulty(player) == 'hard':
|
if self.combat_difficulty(player) == 'hard':
|
||||||
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
return respawn_dragon and ((self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
||||||
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
|
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player)))
|
||||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
|
return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
|
||||||
|
|
||||||
|
|
||||||
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
||||||
if location:
|
if location:
|
||||||
self.locations_checked.add(location)
|
self.locations_checked.add(location)
|
||||||
changed = False
|
|
||||||
|
|
||||||
# TODO: create a mapping for progressive items in each game and use that
|
changed = self.world.worlds[item.player].collect(self, item)
|
||||||
if item.game == "A Link to the Past":
|
|
||||||
if item.name.startswith('Progressive '):
|
|
||||||
if 'Sword' in item.name:
|
|
||||||
if self.has('Golden Sword', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
|
||||||
item.player].progressive_sword_limit >= 4:
|
|
||||||
self.prog_items['Golden Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
|
|
||||||
self.prog_items['Tempered Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
|
||||||
self.prog_items['Master Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
|
||||||
self.prog_items['Fighter Sword', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif 'Glove' in item.name:
|
|
||||||
if self.has('Titans Mitts', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Power Glove', item.player):
|
|
||||||
self.prog_items['Titans Mitts', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
self.prog_items['Power Glove', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif 'Shield' in item.name:
|
|
||||||
if self.has('Mirror Shield', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
|
||||||
self.prog_items['Mirror Shield', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
|
||||||
self.prog_items['Red Shield', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
|
||||||
self.prog_items['Blue Shield', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
elif 'Bow' in item.name:
|
|
||||||
if self.has('Silver Bow', item.player):
|
|
||||||
pass
|
|
||||||
elif self.has('Bow', item.player):
|
|
||||||
self.prog_items['Silver Bow', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
self.prog_items['Bow', item.player] += 1
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
|
if not changed and event:
|
||||||
if not changed and (event or item.advancement):
|
|
||||||
self.prog_items[item.name, item.player] += 1
|
self.prog_items[item.name, item.player] += 1
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
@@ -1193,6 +1106,14 @@ class Location():
|
|||||||
return True
|
return True
|
||||||
return False
|
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):
|
def __repr__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
@@ -1221,7 +1142,7 @@ class Item():
|
|||||||
zora_credit_text = None
|
zora_credit_text = None
|
||||||
fluteboy_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.name = name
|
||||||
self.advancement = advancement
|
self.advancement = advancement
|
||||||
self.player = player
|
self.player = player
|
||||||
@@ -1229,11 +1150,11 @@ class Item():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def hint_text(self):
|
def hint_text(self):
|
||||||
return getattr(self, "_hint_text", self.name)
|
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pedestal_hint_text(self):
|
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):
|
def __eq__(self, other):
|
||||||
return self.name == other.name and self.player == other.player
|
return self.name == other.name and self.player == other.player
|
||||||
@@ -1415,8 +1336,6 @@ class Spoiler(object):
|
|||||||
'shuffle': self.world.shuffle,
|
'shuffle': self.world.shuffle,
|
||||||
'item_pool': self.world.difficulty,
|
'item_pool': self.world.difficulty,
|
||||||
'item_functionality': self.world.item_functionality,
|
'item_functionality': self.world.item_functionality,
|
||||||
'gt_crystals': self.world.crystals_needed_for_gt,
|
|
||||||
'ganon_crystals': self.world.crystals_needed_for_ganon,
|
|
||||||
'open_pyramid': self.world.open_pyramid,
|
'open_pyramid': self.world.open_pyramid,
|
||||||
'accessibility': self.world.accessibility,
|
'accessibility': self.world.accessibility,
|
||||||
'hints': self.world.hints,
|
'hints': self.world.hints,
|
||||||
@@ -1440,7 +1359,6 @@ class Spoiler(object):
|
|||||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||||
'shop_shuffle': self.world.shop_shuffle,
|
'shop_shuffle': self.world.shop_shuffle,
|
||||||
'shop_shuffle_slots': self.world.shop_shuffle_slots,
|
|
||||||
'shuffle_prizes': self.world.shuffle_prizes,
|
'shuffle_prizes': self.world.shuffle_prizes,
|
||||||
'sprite_pool': self.world.sprite_pool,
|
'sprite_pool': self.world.sprite_pool,
|
||||||
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
||||||
@@ -1467,6 +1385,7 @@ class Spoiler(object):
|
|||||||
return json.dumps(out)
|
return json.dumps(out)
|
||||||
|
|
||||||
def to_file(self, filename):
|
def to_file(self, filename):
|
||||||
|
import Options
|
||||||
self.parse_data()
|
self.parse_data()
|
||||||
|
|
||||||
def bool_to_text(variable: Union[bool, str]) -> str:
|
def bool_to_text(variable: Union[bool, str]) -> str:
|
||||||
@@ -1489,16 +1408,12 @@ class Spoiler(object):
|
|||||||
outfile.write('Progression Balanced: %s\n' % (
|
outfile.write('Progression Balanced: %s\n' % (
|
||||||
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
||||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||||
if player in self.world.hk_player_ids:
|
options = self.world.worlds[player].options
|
||||||
import Options
|
if options:
|
||||||
for hk_option in Options.hollow_knight_options:
|
for f_option in options:
|
||||||
res = getattr(self.world, hk_option)[player]
|
res = getattr(self.world, f_option)[player]
|
||||||
outfile.write(f'{hk_option+":":33}{res}\n')
|
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.minecraft_player_ids:
|
|
||||||
import Options
|
|
||||||
for mc_option in Options.minecraft_options:
|
|
||||||
res = getattr(self.world, mc_option)[player]
|
|
||||||
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
|
||||||
if player in self.world.alttp_player_ids:
|
if player in self.world.alttp_player_ids:
|
||||||
for team in range(self.world.teams):
|
for team in range(self.world.teams):
|
||||||
outfile.write('%s%s\n' % (
|
outfile.write('%s%s\n' % (
|
||||||
@@ -1527,8 +1442,6 @@ class Spoiler(object):
|
|||||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||||
if self.metadata['shuffle'][player] != "vanilla":
|
if self.metadata['shuffle'][player] != "vanilla":
|
||||||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||||
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
|
|
||||||
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
|
|
||||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||||
|
|
||||||
@@ -1551,8 +1464,6 @@ class Spoiler(object):
|
|||||||
"f" in self.metadata["shop_shuffle"][player]))
|
"f" in self.metadata["shop_shuffle"][player]))
|
||||||
outfile.write('Custom Potion Shop: %s\n' %
|
outfile.write('Custom Potion Shop: %s\n' %
|
||||||
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
||||||
outfile.write('Shop Slots: %s\n' %
|
|
||||||
self.metadata["shop_shuffle_slots"][player])
|
|
||||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||||
outfile.write(
|
outfile.write(
|
||||||
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
||||||
@@ -1575,17 +1486,31 @@ class Spoiler(object):
|
|||||||
'<=>' if entry['direction'] == 'both' else
|
'<=>' if entry['direction'] == 'both' else
|
||||||
'<=' if entry['direction'] == 'exit' else '=>',
|
'<=' if entry['direction'] == 'exit' else '=>',
|
||||||
entry['exit']) for entry in self.entrances.values()]))
|
entry['exit']) for entry in self.entrances.values()]))
|
||||||
outfile.write('\n\nMedallions:\n')
|
|
||||||
for dungeon, medallion in self.medallions.items():
|
if self.medallions:
|
||||||
outfile.write(f'\n{dungeon}: {medallion}')
|
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:
|
if self.startinventory:
|
||||||
outfile.write('\n\nStarting Inventory:\n\n')
|
outfile.write('\n\nStarting Inventory:\n\n')
|
||||||
outfile.write('\n'.join(self.startinventory))
|
outfile.write('\n'.join(self.startinventory))
|
||||||
|
|
||||||
outfile.write('\n\nLocations:\n\n')
|
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'.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))
|
if self.shops:
|
||||||
for player in range(1, self.world.players + 1):
|
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':
|
if self.world.boss_shuffle[player] != 'none':
|
||||||
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
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')
|
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:
|
if self.unreachables:
|
||||||
outfile.write('\n\nUnreachable Items:\n\n')
|
outfile.write('\n\nUnreachable Items:\n\n')
|
||||||
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||||
outfile.write('\n\nPaths:\n\n')
|
|
||||||
|
|
||||||
path_listings = []
|
if self.paths:
|
||||||
for location, path in sorted(self.paths.items()):
|
outfile.write('\n\nPaths:\n\n')
|
||||||
path_lines = []
|
path_listings = []
|
||||||
for region, exit in path:
|
for location, path in sorted(self.paths.items()):
|
||||||
if exit is not None:
|
path_lines = []
|
||||||
path_lines.append("{} -> {}".format(region, exit))
|
for region, exit in path:
|
||||||
else:
|
if exit is not None:
|
||||||
path_lines.append(region)
|
path_lines.append("{} -> {}".format(region, exit))
|
||||||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
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.alttp.Items import item_name_groups
|
||||||
from worlds.generic import PlandoItem, PlandoConnection
|
from worlds.generic import PlandoItem, PlandoConnection
|
||||||
104
CommonClient.py
104
CommonClient.py
@@ -4,9 +4,7 @@ import typing
|
|||||||
import asyncio
|
import asyncio
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import prompt_toolkit
|
|
||||||
import websockets
|
import websockets
|
||||||
from prompt_toolkit.patch_stdout import patch_stdout
|
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
@@ -47,16 +45,9 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
|
|
||||||
def _cmd_received(self) -> bool:
|
def _cmd_received(self) -> bool:
|
||||||
"""List all received items"""
|
"""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):
|
for index, item in enumerate(self.ctx.items_received, 1):
|
||||||
self.ctx.ui_node.notify_item_received(self.ctx.player_names[item.player], self.ctx.item_name_getter(item.item),
|
self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}")
|
||||||
self.ctx.location_name_getter(item.location), index,
|
|
||||||
len(self.ctx.items_received),
|
|
||||||
self.ctx.item_name_getter(item.item) in Items.progression_items)
|
|
||||||
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)))
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cmd_missing(self) -> bool:
|
def _cmd_missing(self) -> bool:
|
||||||
@@ -116,7 +107,7 @@ class CommonContext():
|
|||||||
self.team = None
|
self.team = None
|
||||||
self.slot = None
|
self.slot = None
|
||||||
self.auth = None
|
self.auth = None
|
||||||
self.ui_node = None
|
self.seed_name = None
|
||||||
|
|
||||||
self.locations_checked: typing.Set[int] = set()
|
self.locations_checked: typing.Set[int] = set()
|
||||||
self.locations_scouted: typing.Set[int] = set()
|
self.locations_scouted: typing.Set[int] = set()
|
||||||
@@ -129,7 +120,7 @@ class CommonContext():
|
|||||||
self.input_requests = 0
|
self.input_requests = 0
|
||||||
|
|
||||||
# game state
|
# game state
|
||||||
self.player_names: typing.Dict[int: str] = {0: "Server"}
|
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
|
||||||
self.exit_event = asyncio.Event()
|
self.exit_event = asyncio.Event()
|
||||||
self.watcher_event = asyncio.Event()
|
self.watcher_event = asyncio.Event()
|
||||||
|
|
||||||
@@ -194,7 +185,7 @@ class CommonContext():
|
|||||||
|
|
||||||
def consume_players_package(self, package: typing.List[tuple]):
|
def consume_players_package(self, package: typing.List[tuple]):
|
||||||
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
|
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
|
||||||
self.player_names[0] = "Server"
|
self.player_names[0] = "Archipelago"
|
||||||
|
|
||||||
def event_invalid_slot(self):
|
def event_invalid_slot(self):
|
||||||
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
|
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
|
||||||
@@ -217,15 +208,10 @@ class CommonContext():
|
|||||||
logger.info(args["text"])
|
logger.info(args["text"])
|
||||||
|
|
||||||
def on_print_json(self, args: dict):
|
def on_print_json(self, args: dict):
|
||||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
|
||||||
pass # don't want info on other player's local pickups.
|
|
||||||
logger.info(self.jsontotextparser(args["data"]))
|
logger.info(self.jsontotextparser(args["data"]))
|
||||||
|
|
||||||
|
|
||||||
async def server_loop(ctx: CommonContext, address=None):
|
async def server_loop(ctx: CommonContext, address=None):
|
||||||
ui_node = getattr(ctx, "ui_node", None)
|
|
||||||
if ui_node:
|
|
||||||
ui_node.send_connection_status(ctx)
|
|
||||||
cached_address = None
|
cached_address = None
|
||||||
if ctx.server and ctx.server.socket:
|
if ctx.server and ctx.server.socket:
|
||||||
logger.error('Already connected')
|
logger.error('Already connected')
|
||||||
@@ -237,8 +223,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
|||||||
# Wait for the user to provide a multiworld server address
|
# Wait for the user to provide a multiworld server address
|
||||||
if not address:
|
if not address:
|
||||||
logger.info('Please connect to an Archipelago server.')
|
logger.info('Please connect to an Archipelago server.')
|
||||||
if ui_node:
|
|
||||||
ui_node.poll_for_server_ip()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
address = f"ws://{address}" if "://" not in address else address
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
@@ -250,8 +234,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
|||||||
ctx.server = Endpoint(socket)
|
ctx.server = Endpoint(socket)
|
||||||
logger.info('Connected')
|
logger.info('Connected')
|
||||||
ctx.server_address = address
|
ctx.server_address = address
|
||||||
if ui_node:
|
|
||||||
ui_node.send_connection_status(ctx)
|
|
||||||
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
|
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
|
||||||
async for data in ctx.server.socket:
|
async for data in ctx.server.socket:
|
||||||
for msg in decode(data):
|
for msg in decode(data):
|
||||||
@@ -273,8 +255,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
|||||||
await ctx.connection_closed()
|
await ctx.connection_closed()
|
||||||
if ctx.server_address:
|
if ctx.server_address:
|
||||||
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
|
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
|
||||||
if ui_node:
|
|
||||||
ui_node.send_connection_status(ctx)
|
|
||||||
asyncio.create_task(server_autoreconnect(ctx))
|
asyncio.create_task(server_autoreconnect(ctx))
|
||||||
ctx.current_reconnect_delay *= 2
|
ctx.current_reconnect_delay *= 2
|
||||||
|
|
||||||
@@ -292,41 +272,42 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
logger.exception(f"Could not get command from {args}")
|
logger.exception(f"Could not get command from {args}")
|
||||||
raise
|
raise
|
||||||
if cmd == 'RoomInfo':
|
if cmd == 'RoomInfo':
|
||||||
logger.info('--------------------------------')
|
if ctx.seed_name and ctx.seed_name != args["seed_name"]:
|
||||||
logger.info('Room Information:')
|
logger.info("The server is running a different multiworld than your client is. (invalid seed_name)")
|
||||||
logger.info('--------------------------------')
|
|
||||||
version = args["version"]
|
|
||||||
ctx.server_version = tuple(version)
|
|
||||||
version = ".".join(str(item) for item in version)
|
|
||||||
|
|
||||||
logger.info(f'Server protocol version: {version}')
|
|
||||||
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
|
||||||
if args['password']:
|
|
||||||
logger.info('Password required')
|
|
||||||
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
|
|
||||||
logger.info(f"Remaining setting: {args['remaining_mode']}")
|
|
||||||
logger.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
|
|
||||||
f" for each location checked.")
|
|
||||||
ctx.hint_cost = int(args['hint_cost'])
|
|
||||||
ctx.check_points = int(args['location_check_points'])
|
|
||||||
ctx.forfeit_mode = args['forfeit_mode']
|
|
||||||
ctx.remaining_mode = args['remaining_mode']
|
|
||||||
if ctx.ui_node:
|
|
||||||
ctx.ui_node.send_game_info(ctx)
|
|
||||||
if len(args['players']) < 1:
|
|
||||||
logger.info('No player connected')
|
|
||||||
else:
|
else:
|
||||||
args['players'].sort()
|
logger.info('--------------------------------')
|
||||||
current_team = -1
|
logger.info('Room Information:')
|
||||||
logger.info('Players:')
|
logger.info('--------------------------------')
|
||||||
for network_player in args['players']:
|
version = args["version"]
|
||||||
if network_player.team != current_team:
|
ctx.server_version = tuple(version)
|
||||||
logger.info(f' Team #{network_player.team + 1}')
|
version = ".".join(str(item) for item in version)
|
||||||
current_team = network_player.team
|
|
||||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
logger.info(f'Server protocol version: {version}')
|
||||||
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
|
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
||||||
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
|
if args['password']:
|
||||||
await ctx.server_auth(args['password'])
|
logger.info('Password required')
|
||||||
|
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
|
||||||
|
logger.info(f"Remaining setting: {args['remaining_mode']}")
|
||||||
|
logger.info(f"A !hint costs {args['hint_cost']}% of checks points and you get {args['location_check_points']}"
|
||||||
|
f" for each location checked. Use !hint for more information.")
|
||||||
|
ctx.hint_cost = int(args['hint_cost'])
|
||||||
|
ctx.check_points = int(args['location_check_points'])
|
||||||
|
ctx.forfeit_mode = args['forfeit_mode']
|
||||||
|
ctx.remaining_mode = args['remaining_mode']
|
||||||
|
if len(args['players']) < 1:
|
||||||
|
logger.info('No player connected')
|
||||||
|
else:
|
||||||
|
args['players'].sort()
|
||||||
|
current_team = -1
|
||||||
|
logger.info('Players:')
|
||||||
|
for network_player in args['players']:
|
||||||
|
if network_player.team != current_team:
|
||||||
|
logger.info(f' Team #{network_player.team + 1}')
|
||||||
|
current_team = network_player.team
|
||||||
|
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||||
|
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
|
||||||
|
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
|
||||||
|
await ctx.server_auth(args['password'])
|
||||||
|
|
||||||
elif cmd == 'DataPackage':
|
elif cmd == 'DataPackage':
|
||||||
logger.info("Got new ID/Name Datapackage")
|
logger.info("Got new ID/Name Datapackage")
|
||||||
@@ -346,9 +327,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
logger.error('Invalid password')
|
logger.error('Invalid password')
|
||||||
ctx.password = None
|
ctx.password = None
|
||||||
await ctx.server_auth(True)
|
await ctx.server_auth(True)
|
||||||
else:
|
elif errors:
|
||||||
raise Exception("Unknown connection errors: " + str(errors))
|
raise Exception("Unknown connection errors: " + str(errors))
|
||||||
raise Exception('Connection refused by the multiworld host, no reason provided')
|
else:
|
||||||
|
raise Exception('Connection refused by the multiworld host, no reason provided')
|
||||||
|
|
||||||
elif cmd == 'Connected':
|
elif cmd == 'Connected':
|
||||||
ctx.team = args["team"]
|
ctx.team = args["team"]
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import string
|
import string
|
||||||
import copy
|
import copy
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import factorio_rcon
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
import asyncio
|
import asyncio
|
||||||
from queue import Queue, Empty
|
from queue import Queue
|
||||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import random
|
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
|
from worlds.factorio.Technologies import lookup_id_to_name
|
||||||
|
|
||||||
rcon_port = 24242
|
rcon_port = 24242
|
||||||
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
|
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
|
||||||
save_name = "Archipelago"
|
|
||||||
|
|
||||||
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
|
||||||
|
|
||||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
||||||
|
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||||
options = Utils.get_options()
|
options = Utils.get_options()
|
||||||
executable = options["factorio_options"]["executable"]
|
executable = options["factorio_options"]["executable"]
|
||||||
bin_dir = os.path.dirname(executable)
|
bin_dir = os.path.dirname(executable)
|
||||||
@@ -35,10 +36,12 @@ if not os.path.exists(executable):
|
|||||||
else:
|
else:
|
||||||
raise FileNotFoundError(executable)
|
raise FileNotFoundError(executable)
|
||||||
|
|
||||||
threadpool = ThreadPoolExecutor(10)
|
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||||
|
ctx: FactorioContext
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_factorio(self, text: str) -> bool:
|
def _cmd_factorio(self, text: str) -> bool:
|
||||||
"""Send the following command to the bound Factorio Server."""
|
"""Send the following command to the bound Factorio Server."""
|
||||||
@@ -52,7 +55,10 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_connect(self, address: str = "") -> bool:
|
def _cmd_connect(self, address: str = "") -> bool:
|
||||||
"""Connect to a MultiWorld Server"""
|
"""Connect to a MultiWorld Server"""
|
||||||
if not self.ctx.auth:
|
if not self.ctx.auth:
|
||||||
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
if self.ctx.rcon_client:
|
||||||
|
get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code
|
||||||
|
else:
|
||||||
|
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
||||||
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
||||||
|
|
||||||
|
|
||||||
@@ -63,14 +69,16 @@ class FactorioContext(CommonContext):
|
|||||||
super(FactorioContext, self).__init__(*args, **kwargs)
|
super(FactorioContext, self).__init__(*args, **kwargs)
|
||||||
self.send_index = 0
|
self.send_index = 0
|
||||||
self.rcon_client = None
|
self.rcon_client = None
|
||||||
|
self.awaiting_bridge = False
|
||||||
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
||||||
|
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||||
|
|
||||||
async def server_auth(self, password_requested):
|
async def server_auth(self, password_requested):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
await super(FactorioContext, self).server_auth(password_requested)
|
await super(FactorioContext, self).server_auth(password_requested)
|
||||||
|
|
||||||
await self.send_msgs([{"cmd": 'Connect',
|
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'],
|
'tags': ['AP'],
|
||||||
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
||||||
}])
|
}])
|
||||||
@@ -79,60 +87,63 @@ class FactorioContext(CommonContext):
|
|||||||
logger.info(args["text"])
|
logger.info(args["text"])
|
||||||
if self.rcon_client:
|
if self.rcon_client:
|
||||||
cleaned_text = args['text'].replace('"', '')
|
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):
|
def on_print_json(self, args: dict):
|
||||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
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.
|
pass # don't want info on other player's local pickups.
|
||||||
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently
|
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
|
||||||
logger.info(self.jsontotextparser(args["data"]))
|
logger.info(text)
|
||||||
if self.rcon_client:
|
if self.rcon_client:
|
||||||
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '')
|
text = self.factorio_json_text_parser(args["data"])
|
||||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
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")
|
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||||
from worlds.factorio.Technologies import lookup_id_to_name
|
from worlds.factorio.Technologies import lookup_id_to_name
|
||||||
bridge_counter = 0
|
|
||||||
try:
|
try:
|
||||||
while 1:
|
while not ctx.exit_event.is_set():
|
||||||
if os.path.exists(bridge_file):
|
if ctx.awaiting_bridge and ctx.rcon_client:
|
||||||
bridge_logger.info("Found Factorio Bridge file.")
|
ctx.awaiting_bridge = False
|
||||||
while 1:
|
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
|
||||||
with open(bridge_file) as f:
|
if data["slot_name"] != ctx.auth:
|
||||||
data = json.load(f)
|
logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
|
||||||
research_data = data["research_done"]
|
elif data["seed_name"] != ctx.seed_name:
|
||||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
logger.warning(f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
||||||
victory = data["victory"]
|
else:
|
||||||
ctx.auth = data["slot_name"]
|
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:
|
if not ctx.finished_game and victory:
|
||||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
ctx.finished_game = True
|
ctx.finished_game = True
|
||||||
|
|
||||||
if ctx.locations_checked != research_data:
|
if ctx.locations_checked != research_data:
|
||||||
bridge_logger.info(f"New researches done: "
|
bridge_logger.info(
|
||||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
f"New researches done: "
|
||||||
|
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||||
ctx.locations_checked = research_data
|
ctx.locations_checked = research_data
|
||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
|
||||||
bridge_counter += 1
|
|
||||||
if bridge_counter >= 60:
|
|
||||||
bridge_logger.info(
|
|
||||||
"Did not find Factorio Bridge file, "
|
|
||||||
"waiting for mod to run, which requires the server to run, "
|
|
||||||
"which requires a player to be connected.")
|
|
||||||
bridge_counter = 0
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
|
|
||||||
|
|
||||||
def stream_factorio_output(pipe, queue):
|
def stream_factorio_output(pipe, queue, process):
|
||||||
def queuer():
|
def queuer():
|
||||||
while 1:
|
while process.poll() is None:
|
||||||
text = pipe.readline().strip()
|
text = pipe.readline().strip()
|
||||||
if text:
|
if text:
|
||||||
queue.put_nowait(text)
|
queue.put_nowait(text)
|
||||||
@@ -141,40 +152,42 @@ def stream_factorio_output(pipe, queue):
|
|||||||
|
|
||||||
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
|
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
async def factorio_server_watcher(ctx: FactorioContext):
|
async def factorio_server_watcher(ctx: FactorioContext):
|
||||||
import subprocess
|
savegame_name = os.path.abspath(ctx.savegame_name)
|
||||||
import factorio_rcon
|
if not os.path.exists(savegame_name):
|
||||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
logger.info(f"Creating savegame {savegame_name}")
|
||||||
factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)),
|
subprocess.run((
|
||||||
|
executable, "--create", savegame_name
|
||||||
|
))
|
||||||
|
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
|
||||||
|
*(str(elem) for elem in server_args)),
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
encoding="utf-8")
|
encoding="utf-8")
|
||||||
factorio_server_logger.info("Started Factorio Server")
|
factorio_server_logger.info("Started Factorio Server")
|
||||||
factorio_queue = Queue()
|
factorio_queue = Queue()
|
||||||
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
||||||
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
||||||
script_folder = None
|
|
||||||
try:
|
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():
|
while not factorio_queue.empty():
|
||||||
msg = factorio_queue.get()
|
msg = factorio_queue.get()
|
||||||
factorio_server_logger.info(msg)
|
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)
|
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||||
# trigger lua interface confirmation
|
# trigger lua interface confirmation
|
||||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||||
ctx.rcon_client.send_command("/ap-sync")
|
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||||
if not script_folder and "Write data path:" in msg:
|
ctx.awaiting_bridge = True
|
||||||
script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
|
|
||||||
bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json")
|
|
||||||
if os.path.exists(bridge_file):
|
|
||||||
os.remove(bridge_file)
|
|
||||||
logging.info(f"Bridge File Path: {bridge_file}")
|
|
||||||
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
|
||||||
if ctx.rcon_client:
|
if ctx.rcon_client:
|
||||||
while ctx.send_index < len(ctx.items_received):
|
while ctx.send_index < len(ctx.items_received):
|
||||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||||
@@ -185,42 +198,118 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
else:
|
else:
|
||||||
item_name = lookup_id_to_name[item_id]
|
item_name = lookup_id_to_name[item_id]
|
||||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
||||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
|
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
|
||||||
ctx.send_index += 1
|
ctx.send_index += 1
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
|
ctx.rcon_client = None
|
||||||
|
ctx.exit_event.set()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
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)
|
ctx = FactorioContext(None, None, True)
|
||||||
# testing shortcuts
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
# ctx.server_address = "localhost"
|
if ui:
|
||||||
# ctx.auth = "Nauvis"
|
input_task = None
|
||||||
if ctx.server_task is None:
|
ui_app = ui(ctx)
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
||||||
await asyncio.sleep(3)
|
else:
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
|
ui_task = None
|
||||||
|
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
|
||||||
|
await factorio_server_task
|
||||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||||
|
progression_watcher = asyncio.create_task(
|
||||||
|
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
ctx.server_address = None
|
ctx.server_address = None
|
||||||
ctx.snes_reconnect_address = None
|
|
||||||
|
|
||||||
await asyncio.gather(input_task, factorio_server_task)
|
await progression_watcher
|
||||||
|
await factorio_server_task
|
||||||
|
|
||||||
if ctx.server is not None and not ctx.server.socket.closed:
|
if ctx.server and not ctx.server.socket.closed:
|
||||||
await ctx.server.socket.close()
|
await ctx.server.socket.close()
|
||||||
if ctx.server_task is not None:
|
if ctx.server_task is not None:
|
||||||
await ctx.server_task
|
await ctx.server_task
|
||||||
await factorio_server_task
|
|
||||||
|
|
||||||
while ctx.input_requests > 0:
|
while ctx.input_requests > 0:
|
||||||
ctx.input_queue.put_nowait(None)
|
ctx.input_queue.put_nowait(None)
|
||||||
ctx.input_requests -= 1
|
ctx.input_requests -= 1
|
||||||
|
|
||||||
await input_task
|
if ui_task:
|
||||||
|
await ui_task
|
||||||
|
|
||||||
|
if input_task:
|
||||||
|
input_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
class FactorioJSONtoTextParser(JSONtoTextParser):
|
||||||
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
141
FactorioClientGUI.py
Normal file
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
143
Fill.py
@@ -3,7 +3,7 @@ import typing
|
|||||||
import collections
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from BaseClasses import CollectionState, PlandoItem, Location
|
from BaseClasses import CollectionState, PlandoItem, Location, MultiWorld
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
from worlds.alttp.Regions import key_drop_data
|
from worlds.alttp.Regions import key_drop_data
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ class FillError(RuntimeError):
|
|||||||
pass
|
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):
|
lock=False):
|
||||||
def sweep_from_pool():
|
def sweep_from_pool():
|
||||||
new_state = base_state.copy()
|
new_state = base_state.copy()
|
||||||
@@ -68,7 +68,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
|||||||
itempool.extend(unplaced_items)
|
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 passed in, then get a shuffled list of locations to fill in
|
||||||
if not fill_locations:
|
if not fill_locations:
|
||||||
fill_locations = world.get_unfilled_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
|
# fill in gtower locations with trash first
|
||||||
for player in world.alttp_player_ids:
|
for player in world.alttp_player_ids:
|
||||||
if not gftower_trash or not world.ganonstower_vanilla[player] or \
|
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
|
gtower_trash_count = 0
|
||||||
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
|
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,
|
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}')
|
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))
|
placing = min(len(item_pool), len(fill_locations))
|
||||||
for item, location in zip(item_pool, fill_locations):
|
for item, location in zip(item_pool, fill_locations):
|
||||||
world.push_item(location, item, False)
|
world.push_item(location, item, False)
|
||||||
return item_pool[placing:], fill_locations[placing:]
|
return item_pool[placing:], fill_locations[placing:]
|
||||||
|
|
||||||
|
|
||||||
def flood_items(world):
|
def flood_items(world: MultiWorld):
|
||||||
# get items to distribute
|
# get items to distribute
|
||||||
world.random.shuffle(world.itempool)
|
world.random.shuffle(world.itempool)
|
||||||
itempool = world.itempool
|
itempool = world.itempool
|
||||||
@@ -234,7 +234,7 @@ def flood_items(world):
|
|||||||
break
|
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]}
|
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
|
||||||
if not balanceable_players:
|
if not balanceable_players:
|
||||||
logging.info('Skipping multiworld progression balancing.')
|
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
|
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
|
world_name_lookup = world.world_name_lookup
|
||||||
|
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
placement: PlandoItem
|
try:
|
||||||
for placement in world.plando_items[player]:
|
placement: PlandoItem
|
||||||
if placement.location in key_drop_data:
|
for placement in world.plando_items[player]:
|
||||||
placement.warn(
|
if placement.location in key_drop_data:
|
||||||
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
placement.warn(
|
||||||
continue
|
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||||
item = ItemFactory(placement.item, player)
|
continue
|
||||||
target_world: int = placement.world
|
item = ItemFactory(placement.item, player)
|
||||||
if target_world is False or world.players == 1:
|
target_world: int = placement.world
|
||||||
target_world = player # in own world
|
if target_world is False or world.players == 1:
|
||||||
elif target_world is True: # in any other world
|
target_world = player # in own world
|
||||||
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
elif target_world is True: # in any other world
|
||||||
placement.location,
|
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
||||||
set(world.player_ids) - {player}) if location.item_rule(item)
|
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}",
|
if not unfilled:
|
||||||
FillError)
|
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
|
continue
|
||||||
|
|
||||||
target_world = world.random.choice(unfilled).player
|
if location.can_fill(world.state, item, False):
|
||||||
|
world.push_item(location, item, collect=False)
|
||||||
elif target_world is None: # any random world
|
location.event = True # flag location to be checked during fill
|
||||||
unfilled = list(location for location in world.get_unfilled_locations_for_players(
|
location.locked = True
|
||||||
placement.location,
|
logging.debug(f"Plando placed {item} at {location}")
|
||||||
set(world.player_ids)) if location.item_rule(item)
|
else:
|
||||||
)
|
placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
|
||||||
if not unfilled:
|
|
||||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
|
||||||
FillError)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target_world = world.random.choice(unfilled).player
|
if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
|
||||||
|
try:
|
||||||
elif type(target_world) == int: # target world by player id
|
world.itempool.remove(item)
|
||||||
if target_world not in range(1, world.players + 1):
|
except ValueError:
|
||||||
placement.failed(
|
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
|
||||||
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
except Exception as e:
|
||||||
ValueError)
|
raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e
|
||||||
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.")
|
|
||||||
|
|||||||
6
Gui.py
6
Gui.py
@@ -428,7 +428,6 @@ def guiMain(args=None):
|
|||||||
guiargs.fastmenu = rom_vars.fastMenuVar.get()
|
guiargs.fastmenu = rom_vars.fastMenuVar.get()
|
||||||
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
||||||
guiargs.skip_playthrough = not bool(createSpoilerVar.get())
|
guiargs.skip_playthrough = not bool(createSpoilerVar.get())
|
||||||
guiargs.suppress_rom = bool(suppressRomVar.get())
|
|
||||||
guiargs.open_pyramid = openpyramidVar.get()
|
guiargs.open_pyramid = openpyramidVar.get()
|
||||||
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
||||||
guiargs.compassshuffle = bool(compassshuffleVar.get())
|
guiargs.compassshuffle = bool(compassshuffleVar.get())
|
||||||
@@ -469,7 +468,7 @@ def guiMain(args=None):
|
|||||||
if shopWitchShuffleVar.get():
|
if shopWitchShuffleVar.get():
|
||||||
guiargs.shop_shuffle += "w"
|
guiargs.shop_shuffle += "w"
|
||||||
if shopPoolShuffleVar.get():
|
if shopPoolShuffleVar.get():
|
||||||
guiargs.shop_shuffle_slots = 30
|
guiargs.shop_item_slots = 30
|
||||||
guiargs.shuffle_prizes = {"none": "",
|
guiargs.shuffle_prizes = {"none": "",
|
||||||
"bonk": "b",
|
"bonk": "b",
|
||||||
"general": "g",
|
"general": "g",
|
||||||
@@ -513,7 +512,7 @@ def guiMain(args=None):
|
|||||||
elif type(v) is dict: # use same settings for every player
|
elif type(v) is dict: # use same settings for every player
|
||||||
setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)})
|
setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)})
|
||||||
try:
|
try:
|
||||||
if not guiargs.suppress_rom and not os.path.exists(guiargs.rom):
|
if not os.path.exists(guiargs.rom):
|
||||||
raise FileNotFoundError(f"Could not find specified rom file {guiargs.rom}")
|
raise FileNotFoundError(f"Could not find specified rom file {guiargs.rom}")
|
||||||
if guiargs.count is not None:
|
if guiargs.count is not None:
|
||||||
seed = guiargs.seed
|
seed = guiargs.seed
|
||||||
@@ -1204,7 +1203,6 @@ def guiMain(args=None):
|
|||||||
setattr(args, k, v[1]) # only get values for player 1 for now
|
setattr(args, k, v[1]) # only get values for player 1 for now
|
||||||
# load values from commandline args
|
# load values from commandline args
|
||||||
createSpoilerVar.set(int(args.create_spoiler))
|
createSpoilerVar.set(int(args.create_spoiler))
|
||||||
suppressRomVar.set(int(args.suppress_rom))
|
|
||||||
mapshuffleVar.set(args.mapshuffle)
|
mapshuffleVar.set(args.mapshuffle)
|
||||||
compassshuffleVar.set(args.compassshuffle)
|
compassshuffleVar.set(args.compassshuffle)
|
||||||
keyshuffleVar.set(args.keyshuffle)
|
keyshuffleVar.set(args.keyshuffle)
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import tkinter as tk
|
|||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
|
|
||||||
def set_icon(window):
|
def set_icon(window):
|
||||||
er16 = tk.PhotoImage(file=local_path('data', 'ER16.gif'))
|
logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
|
||||||
er32 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
|
window.tk.call('wm', 'iconphoto', window._w, logo)
|
||||||
er48 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
|
|
||||||
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
|
|
||||||
|
|
||||||
# Although tkinter is intended to be thread safe, there are many reports of issues
|
# 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
|
# 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):
|
def __init__(self, sprite_pool):
|
||||||
import random
|
import random
|
||||||
self.sprite_pool = {1: sprite_pool}
|
self.sprite_pool = {1: sprite_pool}
|
||||||
self.rom_seeds = {1: random}
|
self.slot_seeds = {1: random}
|
||||||
|
|
||||||
|
|
||||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||||
@@ -153,8 +153,8 @@ def adjust(args):
|
|||||||
|
|
||||||
|
|
||||||
def adjustGUI():
|
def adjustGUI():
|
||||||
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \
|
from tkinter import Tk, LEFT, BOTTOM, TOP, \
|
||||||
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
|
StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
|
||||||
from Gui import get_rom_options_frame, get_rom_frame
|
from Gui import get_rom_options_frame, get_rom_frame
|
||||||
from GuiUtils import set_icon
|
from GuiUtils import set_icon
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
|||||||
213
LttPClient.py
213
LttPClient.py
@@ -1,18 +1,13 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import atexit
|
import atexit
|
||||||
import time
|
import time
|
||||||
import functools
|
|
||||||
import webbrowser
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import socket
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import base64
|
import base64
|
||||||
import shutil
|
import shutil
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
|
|
||||||
from random import randrange
|
|
||||||
|
|
||||||
from Utils import get_item_name_from_id
|
from Utils import get_item_name_from_id
|
||||||
|
|
||||||
exit_func = atexit.register(input, "Press enter to close.")
|
exit_func = atexit.register(input, "Press enter to close.")
|
||||||
@@ -24,7 +19,6 @@ ModuleUpdate.update()
|
|||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
from NetUtils import *
|
from NetUtils import *
|
||||||
import WebUI
|
|
||||||
|
|
||||||
from worlds.alttp import Regions, Shops
|
from worlds.alttp import Regions, Shops
|
||||||
from worlds.alttp import Items
|
from worlds.alttp import Items
|
||||||
@@ -45,12 +39,6 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
|||||||
|
|
||||||
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
||||||
|
|
||||||
def _cmd_web(self):
|
|
||||||
if self.ctx.webui_socket_port:
|
|
||||||
webbrowser.open(f'http://localhost:5050?port={self.ctx.webui_socket_port}')
|
|
||||||
else:
|
|
||||||
self.output("Web UI was never started.")
|
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_snes(self, snes_address: str = "") -> bool:
|
def _cmd_snes(self, snes_address: str = "") -> bool:
|
||||||
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices"""
|
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices"""
|
||||||
@@ -69,21 +57,9 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
|||||||
|
|
||||||
class Context(CommonContext):
|
class Context(CommonContext):
|
||||||
command_processor = LttPCommandProcessor
|
command_processor = LttPCommandProcessor
|
||||||
def __init__(self, snes_address, server_address, password, found_items, port: int):
|
def __init__(self, snes_address, server_address, password, found_items):
|
||||||
super(Context, self).__init__(server_address, password, found_items)
|
super(Context, self).__init__(server_address, password, found_items)
|
||||||
|
|
||||||
# WebUI Stuff
|
|
||||||
self.ui_node = WebUI.WebUiClient()
|
|
||||||
logger.addHandler(self.ui_node)
|
|
||||||
|
|
||||||
self.webui_socket_port: typing.Optional[int] = port
|
|
||||||
self.hint_cost = 0
|
|
||||||
self.check_points = 0
|
|
||||||
self.forfeit_mode = ''
|
|
||||||
self.remaining_mode = ''
|
|
||||||
self.hint_points = 0
|
|
||||||
# End of WebUI Stuff
|
|
||||||
|
|
||||||
# snes stuff
|
# snes stuff
|
||||||
self.snes_address = snes_address
|
self.snes_address = snes_address
|
||||||
self.snes_socket = None
|
self.snes_socket = None
|
||||||
@@ -92,7 +68,6 @@ class Context(CommonContext):
|
|||||||
self.snes_reconnect_address = None
|
self.snes_reconnect_address = None
|
||||||
self.snes_recv_queue = asyncio.Queue()
|
self.snes_recv_queue = asyncio.Queue()
|
||||||
self.snes_request_lock = asyncio.Lock()
|
self.snes_request_lock = asyncio.Lock()
|
||||||
self.is_sd2snes = False
|
|
||||||
self.snes_write_buffer = []
|
self.snes_write_buffer = []
|
||||||
|
|
||||||
self.awaiting_rom = False
|
self.awaiting_rom = False
|
||||||
@@ -121,7 +96,7 @@ class Context(CommonContext):
|
|||||||
self.auth = self.rom
|
self.auth = self.rom
|
||||||
auth = base64.b64encode(self.rom).decode()
|
auth = base64.b64encode(self.rom).decode()
|
||||||
await self.send_msgs([{"cmd": 'Connect',
|
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),
|
'tags': get_tags(self),
|
||||||
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
|
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
|
||||||
}])
|
}])
|
||||||
@@ -162,8 +137,6 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
|||||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||||
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
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_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
||||||
|
|
||||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
@@ -434,26 +407,30 @@ class SNESState(enum.IntEnum):
|
|||||||
SNES_ATTACHED = 3
|
SNES_ATTACHED = 3
|
||||||
|
|
||||||
|
|
||||||
def launch_qusb2snes(ctx: Context):
|
def launch_sni(ctx: Context):
|
||||||
qusb2snes_path = Utils.get_options()["lttp_options"]["qusb2snes"]
|
sni_path = Utils.get_options()["lttp_options"]["sni"]
|
||||||
|
|
||||||
if not os.path.isfile(qusb2snes_path):
|
if not os.path.isdir(sni_path):
|
||||||
qusb2snes_path = Utils.local_path(qusb2snes_path)
|
sni_path = Utils.local_path(sni_path)
|
||||||
|
if os.path.isdir(sni_path):
|
||||||
|
for file in os.listdir(sni_path):
|
||||||
|
if file.startswith("sni.") and not file.endswith(".proto"):
|
||||||
|
sni_path = os.path.join(sni_path, file)
|
||||||
|
|
||||||
if os.path.isfile(qusb2snes_path):
|
if os.path.isfile(sni_path):
|
||||||
logger.info(f"Attempting to start {qusb2snes_path}")
|
logger.info(f"Attempting to start {sni_path}")
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path))
|
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, "
|
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
|
||||||
f"please start it yourself if it is not running")
|
f"please start it yourself if it is not running")
|
||||||
|
|
||||||
|
|
||||||
async def _snes_connect(ctx: Context, address: str):
|
async def _snes_connect(ctx: Context, address: str):
|
||||||
address = f"ws://{address}" if "://" not in address else address
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
|
|
||||||
logger.info("Connecting to QUsb2snes at %s ..." % address)
|
logger.info("Connecting to SNI at %s ..." % address)
|
||||||
seen_problems = set()
|
seen_problems = set()
|
||||||
succesful = False
|
succesful = False
|
||||||
while not succesful:
|
while not succesful:
|
||||||
@@ -465,11 +442,11 @@ async def _snes_connect(ctx: Context, address: str):
|
|||||||
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
||||||
if problem not in seen_problems:
|
if problem not in seen_problems:
|
||||||
seen_problems.add(problem)
|
seen_problems.add(problem)
|
||||||
logger.error(f"Error connecting to QUsb2snes ({problem})")
|
logger.error(f"Error connecting to SNI ({problem})")
|
||||||
|
|
||||||
if len(seen_problems) == 1:
|
if len(seen_problems) == 1:
|
||||||
# this is the first problem. Let's try launching QUsb2snes if it isn't already running
|
# this is the first problem. Let's try launching SNI if it isn't already running
|
||||||
launch_qusb2snes(ctx)
|
launch_sni(ctx)
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
@@ -488,14 +465,14 @@ async def get_snes_devices(ctx: Context):
|
|||||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
logger.info('No SNES device found. Ensure QUsb2Snes is running and connect it to the multibridge.')
|
logger.info('No SNES device found. Please connect a SNES device to SNI.')
|
||||||
while not devices:
|
while not devices:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await socket.send(dumps(DeviceList_Request))
|
await socket.send(dumps(DeviceList_Request))
|
||||||
reply = loads(await socket.recv())
|
reply = loads(await socket.recv())
|
||||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||||
|
|
||||||
ctx.ui_node.send_device_list(devices)
|
|
||||||
await socket.close()
|
await socket.close()
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
@@ -517,8 +494,6 @@ async def snes_connect(ctx: Context, address):
|
|||||||
|
|
||||||
if len(devices) == 1:
|
if len(devices) == 1:
|
||||||
device = devices[0]
|
device = devices[0]
|
||||||
elif ctx.ui_node.manual_snes and ctx.ui_node.manual_snes in devices:
|
|
||||||
device = ctx.ui_node.manual_snes
|
|
||||||
elif ctx.snes_reconnect_address:
|
elif ctx.snes_reconnect_address:
|
||||||
if ctx.snes_attached_device[1] in devices:
|
if ctx.snes_attached_device[1] in devices:
|
||||||
device = ctx.snes_attached_device[1]
|
device = ctx.snes_attached_device[1]
|
||||||
@@ -538,18 +513,6 @@ async def snes_connect(ctx: Context, address):
|
|||||||
await ctx.snes_socket.send(dumps(Attach_Request))
|
await ctx.snes_socket.send(dumps(Attach_Request))
|
||||||
ctx.snes_state = SNESState.SNES_ATTACHED
|
ctx.snes_state = SNESState.SNES_ATTACHED
|
||||||
ctx.snes_attached_device = (devices.index(device), device)
|
ctx.snes_attached_device = (devices.index(device), device)
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
|
||||||
|
|
||||||
if 'sd2snes' in device.lower() or 'COM' in device:
|
|
||||||
logger.info("SD2SNES/FXPAK Detected")
|
|
||||||
ctx.is_sd2snes = True
|
|
||||||
await ctx.snes_socket.send(dumps({"Opcode": "Info", "Space": "SNES"}))
|
|
||||||
reply = loads(await ctx.snes_socket.recv())
|
|
||||||
if reply and 'Results' in reply:
|
|
||||||
logger.info(reply['Results'])
|
|
||||||
else:
|
|
||||||
ctx.is_sd2snes = False
|
|
||||||
|
|
||||||
ctx.snes_reconnect_address = address
|
ctx.snes_reconnect_address = address
|
||||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||||
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
|
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
|
||||||
@@ -607,7 +570,6 @@ async def snes_recv_loop(ctx: Context):
|
|||||||
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
||||||
ctx.snes_recv_queue = asyncio.Queue()
|
ctx.snes_recv_queue = asyncio.Queue()
|
||||||
ctx.hud_message_queue = []
|
ctx.hud_message_queue = []
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
|
||||||
|
|
||||||
ctx.rom = None
|
ctx.rom = None
|
||||||
|
|
||||||
@@ -644,8 +606,7 @@ async def snes_read(ctx: Context, address, size):
|
|||||||
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||||
if len(data):
|
if len(data):
|
||||||
logger.error(str(data))
|
logger.error(str(data))
|
||||||
logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
|
logger.warning('Communication Failure with SNI')
|
||||||
'Try un-selecting and re-selecting the SNES Device.')
|
|
||||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
return None
|
return None
|
||||||
@@ -664,45 +625,16 @@ async def snes_write(ctx: Context, write_list):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||||
|
try:
|
||||||
if ctx.is_sd2snes:
|
|
||||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
|
||||||
|
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||||
logger.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
|
||||||
return False
|
|
||||||
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
|
||||||
cmd += b'\xA9' # LDA
|
|
||||||
cmd += bytes([byte])
|
|
||||||
cmd += b'\x8F' # STA.l
|
|
||||||
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
|
|
||||||
|
|
||||||
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
|
|
||||||
|
|
||||||
PutAddress_Request['Space'] = 'CMD'
|
|
||||||
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd) - 1)[2:], "2C00", "1"]
|
|
||||||
try:
|
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
await ctx.snes_socket.send(cmd)
|
await ctx.snes_socket.send(data)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Could not send data to SNES: {cmd}")
|
logger.warning(f"Could not send data to SNES: {data}")
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
PutAddress_Request['Space'] = 'SNES'
|
|
||||||
try:
|
|
||||||
# will pack those requests as soon as qusb2snes actually supports that for real
|
|
||||||
for address, data in write_list:
|
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
|
||||||
if ctx.snes_socket is not None:
|
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
|
||||||
await ctx.snes_socket.send(data)
|
|
||||||
else:
|
|
||||||
logger.warning(f"Could not send data to SNES: {data}")
|
|
||||||
except websockets.ConnectionClosed:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
@@ -732,9 +664,6 @@ def get_tags(ctx: Context):
|
|||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def track_locations(ctx: Context, roomid, roomdata):
|
async def track_locations(ctx: Context, roomid, roomdata):
|
||||||
new_locations = []
|
new_locations = []
|
||||||
|
|
||||||
@@ -743,12 +672,10 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
|||||||
ctx.locations_checked.add(location_id)
|
ctx.locations_checked.add(location_id)
|
||||||
location = ctx.location_name_getter(location_id)
|
location = ctx.location_name_getter(location_id)
|
||||||
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||||
ctx.ui_node.send_location_check(ctx, location)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if roomid in location_shop_ids:
|
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):
|
for cnt, b in enumerate(misc_data):
|
||||||
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
|
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
|
||||||
new_check(Shops.SHOP_ID_START + cnt)
|
new_check(Shops.SHOP_ID_START + cnt)
|
||||||
@@ -887,14 +814,11 @@ async def game_watcher(ctx: Context):
|
|||||||
|
|
||||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
ctx.ui_node.notify_item_received(ctx.player_names[item.player], ctx.item_name_getter(item.item),
|
recv_index += 1
|
||||||
ctx.location_name_getter(item.location), recv_index + 1,
|
|
||||||
len(ctx.items_received),
|
|
||||||
ctx.item_name_getter(item.item) in Items.progression_items)
|
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received)))
|
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
|
||||||
recv_index += 1
|
|
||||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
||||||
@@ -920,70 +844,17 @@ async def run_game(romfile):
|
|||||||
subprocess.Popen([auto_start, romfile],
|
subprocess.Popen([auto_start, romfile],
|
||||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
async def websocket_server(websocket: websockets.WebSocketServerProtocol, path, ctx: Context):
|
|
||||||
endpoint = Endpoint(websocket)
|
|
||||||
ctx.ui_node.endpoints.append(endpoint)
|
|
||||||
process_command = LttPCommandProcessor(ctx)
|
|
||||||
try:
|
|
||||||
async for incoming_data in websocket:
|
|
||||||
data = loads(incoming_data)
|
|
||||||
logging.debug(f"WebUIData:{data}")
|
|
||||||
if ('type' not in data) or ('content' not in data):
|
|
||||||
raise Exception('Invalid data received in websocket')
|
|
||||||
|
|
||||||
elif data['type'] == 'webStatus':
|
|
||||||
if data['content'] == 'connections':
|
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
|
||||||
elif data['content'] == 'devices':
|
|
||||||
await get_snes_devices(ctx)
|
|
||||||
elif data['content'] == 'gameInfo':
|
|
||||||
ctx.ui_node.send_game_info(ctx)
|
|
||||||
elif data['content'] == 'checkData':
|
|
||||||
ctx.ui_node.send_location_check(ctx, 'Waiting for check...')
|
|
||||||
|
|
||||||
elif data['type'] == 'webConfig':
|
|
||||||
if 'serverAddress' in data['content']:
|
|
||||||
ctx.server_address = data['content']['serverAddress']
|
|
||||||
await ctx.connect(data['content']['serverAddress'])
|
|
||||||
elif 'deviceId' in data['content']:
|
|
||||||
# Allow a SNES disconnect via UI sending -1 as new device
|
|
||||||
if data['content']['deviceId'] == "-1":
|
|
||||||
ctx.ui_node.manual_snes = None
|
|
||||||
ctx.snes_reconnect_address = None
|
|
||||||
await snes_disconnect(ctx)
|
|
||||||
else:
|
|
||||||
await snes_disconnect(ctx)
|
|
||||||
ctx.ui_node.manual_snes = data['content']['deviceId']
|
|
||||||
await snes_connect(ctx, ctx.snes_address)
|
|
||||||
|
|
||||||
elif data['type'] == 'webControl':
|
|
||||||
if 'disconnect' in data['content']:
|
|
||||||
await ctx.disconnect()
|
|
||||||
|
|
||||||
elif data['type'] == 'webCommand':
|
|
||||||
process_command(data['content'])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
|
||||||
logging.exception(e)
|
|
||||||
finally:
|
|
||||||
await ctx.ui_node.disconnect(endpoint)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
help='Path to a Archipelago Binary Patch file')
|
help='Path to a Archipelago Binary Patch file')
|
||||||
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
|
||||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||||
parser.add_argument('--founditems', default=False, action='store_true',
|
parser.add_argument('--founditems', default=False, action='store_true',
|
||||||
help='Show items found by other players for themselves.')
|
help='Show items found by other players for themselves.')
|
||||||
parser.add_argument('--web_ui', default=False, action='store_true',
|
|
||||||
help="Emit a webserver for the webbrowser based user interface.")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||||
if args.diff_file:
|
if args.diff_file:
|
||||||
@@ -1001,28 +872,12 @@ async def main():
|
|||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||||
|
|
||||||
port = None
|
ctx = Context(args.snes, args.connect, args.password, args.founditems)
|
||||||
if args.web_ui:
|
|
||||||
# Find an available port on the host system to use for hosting the websocket server
|
|
||||||
while True:
|
|
||||||
port = randrange(49152, 65535)
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
||||||
if not sock.connect_ex(('localhost', port)) == 0:
|
|
||||||
break
|
|
||||||
import threading
|
|
||||||
WebUI.start_server(
|
|
||||||
port, on_start=threading.Timer(1, webbrowser.open, (f'http://localhost:5050?port={port}',)).start)
|
|
||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password, args.founditems, port)
|
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
if args.web_ui:
|
|
||||||
ui_socket = websockets.serve(functools.partial(websocket_server, ctx=ctx),
|
|
||||||
'localhost', port, ping_timeout=None, ping_interval=None)
|
|
||||||
await ui_socket
|
|
||||||
|
|
||||||
if ctx.server_task is None:
|
if ctx.server_task is None:
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
asyncio.create_task(snes_connect(ctx, ctx.snes_address))
|
||||||
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
|
|||||||
451
Main.py
451
Main.py
@@ -1,4 +1,3 @@
|
|||||||
import copy
|
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -7,7 +6,7 @@ import time
|
|||||||
import zlib
|
import zlib
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import pickle
|
import pickle
|
||||||
from typing import Dict
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
||||||
from worlds.alttp.Items import ItemFactory, item_name_groups
|
from worlds.alttp.Items import ItemFactory, item_name_groups
|
||||||
@@ -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 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.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||||
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
||||||
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
|
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
|
||||||
from worlds.hk import gen_hollow
|
|
||||||
from worlds.hk import create_regions as hk_create_regions
|
|
||||||
from worlds.factorio import gen_factorio, factorio_create_regions
|
|
||||||
from worlds.factorio.Mod import generate_mod
|
|
||||||
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
|
|
||||||
from worlds.minecraft.Regions import minecraft_create_regions
|
|
||||||
from worlds.generic.Rules import locality_rules
|
from worlds.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
|
import Patch
|
||||||
|
|
||||||
seeddigits = 20
|
seeddigits = 20
|
||||||
@@ -67,6 +60,7 @@ def main(args, seed=None):
|
|||||||
world.secure()
|
world.secure()
|
||||||
else:
|
else:
|
||||||
world.random.seed(world.seed)
|
world.random.seed(world.seed)
|
||||||
|
world.seed_name = str(args.outputname if args.outputname else world.seed)
|
||||||
|
|
||||||
world.shuffle = args.shuffle.copy()
|
world.shuffle = args.shuffle.copy()
|
||||||
world.logic = args.logic.copy()
|
world.logic = args.logic.copy()
|
||||||
@@ -78,7 +72,7 @@ def main(args, seed=None):
|
|||||||
world.progressive = args.progressive.copy()
|
world.progressive = args.progressive.copy()
|
||||||
world.goal = args.goal.copy()
|
world.goal = args.goal.copy()
|
||||||
world.local_items = args.local_items.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.algorithm = args.algorithm
|
||||||
world.shuffleganon = args.shuffleganon
|
world.shuffleganon = args.shuffleganon
|
||||||
world.custom = args.custom
|
world.custom = args.custom
|
||||||
@@ -93,12 +87,6 @@ def main(args, seed=None):
|
|||||||
world.compassshuffle = args.compassshuffle.copy()
|
world.compassshuffle = args.compassshuffle.copy()
|
||||||
world.keyshuffle = args.keyshuffle.copy()
|
world.keyshuffle = args.keyshuffle.copy()
|
||||||
world.bigkeyshuffle = args.bigkeyshuffle.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.open_pyramid = args.open_pyramid.copy()
|
||||||
world.boss_shuffle = args.shufflebosses.copy()
|
world.boss_shuffle = args.shufflebosses.copy()
|
||||||
world.enemy_shuffle = args.enemy_shuffle.copy()
|
world.enemy_shuffle = args.enemy_shuffle.copy()
|
||||||
@@ -120,7 +108,6 @@ def main(args, seed=None):
|
|||||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||||
world.shop_shuffle = args.shop_shuffle.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.progression_balancing = args.progression_balancing.copy()
|
||||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||||
world.sprite_pool = args.sprite_pool.copy()
|
world.sprite_pool = args.sprite_pool.copy()
|
||||||
@@ -132,18 +119,13 @@ def main(args, seed=None):
|
|||||||
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
|
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
|
||||||
world.required_medallions = args.required_medallions.copy()
|
world.required_medallions = args.required_medallions.copy()
|
||||||
world.game = args.game.copy()
|
world.game = args.game.copy()
|
||||||
import Options
|
world.set_options(args)
|
||||||
for hk_option in Options.hollow_knight_options:
|
|
||||||
setattr(world, hk_option, getattr(args, hk_option, {}))
|
|
||||||
for factorio_option in Options.factorio_options:
|
|
||||||
setattr(world, factorio_option, getattr(args, factorio_option, {}))
|
|
||||||
for minecraft_option in Options.minecraft_options:
|
|
||||||
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
|
|
||||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
world.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))
|
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||||
|
|
||||||
if "-" in world.shuffle[player]:
|
if "-" in world.shuffle[player]:
|
||||||
@@ -153,7 +135,8 @@ def main(args, seed=None):
|
|||||||
world.er_seeds[player] = "vanilla"
|
world.er_seeds[player] = "vanilla"
|
||||||
elif seed.startswith("group-") or args.race:
|
elif seed.startswith("group-") or args.race:
|
||||||
# renamed from team to group to not confuse with existing team name use
|
# 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.
|
else: # not a race or group seed, use set seed as is.
|
||||||
world.er_seeds[player] = seed
|
world.er_seeds[player] = seed
|
||||||
elif world.shuffle[player] == "vanilla":
|
elif world.shuffle[player] == "vanilla":
|
||||||
@@ -161,6 +144,10 @@ def main(args, seed=None):
|
|||||||
|
|
||||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
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)
|
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||||
world.teams = len(parsed_names)
|
world.teams = len(parsed_names)
|
||||||
for i, team in enumerate(parsed_names, 1):
|
for i, team in enumerate(parsed_names, 1):
|
||||||
@@ -170,14 +157,15 @@ def main(args, seed=None):
|
|||||||
world.player_names[player].append(name)
|
world.player_names[player].append(name)
|
||||||
|
|
||||||
logger.info('')
|
logger.info('')
|
||||||
|
for player in world.alttp_player_ids:
|
||||||
|
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||||
|
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
for item_name in args.startinventory[player]:
|
for item_name in args.startinventory[player]:
|
||||||
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
|
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
|
||||||
|
item.game = world.game[player]
|
||||||
world.push_precollected(item)
|
world.push_precollected(item)
|
||||||
|
|
||||||
for player in world.alttp_player_ids:
|
|
||||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
|
||||||
|
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
|
|
||||||
# enforce pre-defined local items.
|
# enforce pre-defined local items.
|
||||||
@@ -204,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['Pendants']
|
||||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||||
|
|
||||||
for player in world.hk_player_ids:
|
AutoWorld.call_all(world, "create_regions")
|
||||||
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)
|
|
||||||
|
|
||||||
for player in world.alttp_player_ids:
|
for player in world.alttp_player_ids:
|
||||||
if world.open_pyramid[player] == 'goal':
|
if world.open_pyramid[player] == 'goal':
|
||||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
|
||||||
|
'localganontriforcehunt', 'ganonpedestal'}
|
||||||
elif world.open_pyramid[player] == 'auto':
|
elif world.open_pyramid[player] == 'auto':
|
||||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
|
||||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
|
'localganontriforcehunt', 'ganonpedestal'} and \
|
||||||
|
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull',
|
||||||
|
'dungeonscrossed'} or not world.shuffle_ganon)
|
||||||
else:
|
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_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
world.triforce_pieces_required[player])
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.mode[player] != 'inverted':
|
||||||
create_regions(world, player)
|
create_regions(world, player)
|
||||||
@@ -263,17 +248,12 @@ def main(args, seed=None):
|
|||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
locality_rules(world, player)
|
locality_rules(world, player)
|
||||||
|
|
||||||
|
AutoWorld.call_all(world, "set_rules")
|
||||||
|
|
||||||
for player in world.alttp_player_ids:
|
for player in world.alttp_player_ids:
|
||||||
set_rules(world, player)
|
set_rules(world, player)
|
||||||
|
|
||||||
for player in world.hk_player_ids:
|
AutoWorld.call_all(world, "generate_basic")
|
||||||
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)
|
|
||||||
|
|
||||||
logger.info("Running Item Plando")
|
logger.info("Running Item Plando")
|
||||||
|
|
||||||
@@ -314,7 +294,7 @@ def main(args, seed=None):
|
|||||||
balance_multiworld_progression(world)
|
balance_multiworld_progression(world)
|
||||||
|
|
||||||
logger.info('Generating output files.')
|
logger.info('Generating output files.')
|
||||||
outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed)
|
outfilebase = 'AP_' + world.seed_name
|
||||||
rom_names = []
|
rom_names = []
|
||||||
|
|
||||||
def _gen_rom(team: int, player: int):
|
def _gen_rom(team: int, player: int):
|
||||||
@@ -335,13 +315,13 @@ def main(args, seed=None):
|
|||||||
|
|
||||||
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||||
|
|
||||||
palettes_options={}
|
palettes_options = {}
|
||||||
palettes_options['dungeon']=args.uw_palettes[player]
|
palettes_options['dungeon'] = args.uw_palettes[player]
|
||||||
palettes_options['overworld']=args.ow_palettes[player]
|
palettes_options['overworld'] = args.ow_palettes[player]
|
||||||
palettes_options['hud']=args.hud_palettes[player]
|
palettes_options['hud'] = args.hud_palettes[player]
|
||||||
palettes_options['sword']=args.sword_palettes[player]
|
palettes_options['sword'] = args.sword_palettes[player]
|
||||||
palettes_options['shield']=args.shield_palettes[player]
|
palettes_options['shield'] = args.shield_palettes[player]
|
||||||
palettes_options['link']=args.link_palettes[player]
|
palettes_options['link'] = args.link_palettes[player]
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
||||||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||||
@@ -357,8 +337,8 @@ def main(args, seed=None):
|
|||||||
world.bigkeyshuffle[player]].count(True) == 1:
|
world.bigkeyshuffle[player]].count(True) == 1:
|
||||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
|
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
|
||||||
'-compassshuffle' if world.compassshuffle[player] else \
|
'-compassshuffle' if world.compassshuffle[player] else \
|
||||||
'-universal_keys' if world.keyshuffle[player] == "universal" else \
|
'-universal_keys' if world.keyshuffle[player] == "universal" else \
|
||||||
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||||
world.bigkeyshuffle[player]]):
|
world.bigkeyshuffle[player]]):
|
||||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||||
@@ -366,189 +346,207 @@ def main(args, seed=None):
|
|||||||
'U' if world.keyshuffle[player] == "universal" else 'S' if world.keyshuffle[player] else '',
|
'U' if world.keyshuffle[player] == "universal" else 'S' if world.keyshuffle[player] else '',
|
||||||
'B' if world.bigkeyshuffle[player] else '')
|
'B' if world.bigkeyshuffle[player] else '')
|
||||||
|
|
||||||
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
|
outfilepname = f'_P{player}'
|
||||||
outfilepname += f'_P{player}'
|
|
||||||
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
|
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
|
||||||
if world.player_names[player][team] != 'Player%d' % player else ''
|
if world.player_names[player][team] != 'Player%d' % player else ''
|
||||||
outfilestuffs = {
|
outfilestuffs = {
|
||||||
"logic": world.logic[player], # 0
|
"logic": world.logic[player], # 0
|
||||||
"difficulty": world.difficulty[player], # 1
|
"difficulty": world.difficulty[player], # 1
|
||||||
"item_functionality": world.item_functionality[player], # 2
|
"item_functionality": world.item_functionality[player], # 2
|
||||||
"mode": world.mode[player], # 3
|
"mode": world.mode[player], # 3
|
||||||
"goal": world.goal[player], # 4
|
"goal": world.goal[player], # 4
|
||||||
"timer": str(world.timer[player]), # 5
|
"timer": str(world.timer[player]), # 5
|
||||||
"shuffle": world.shuffle[player], # 6
|
"shuffle": world.shuffle[player], # 6
|
||||||
"algorithm": world.algorithm, # 7
|
"algorithm": world.algorithm, # 7
|
||||||
"mscb": mcsb_name, # 8
|
"mscb": mcsb_name, # 8
|
||||||
"retro": world.retro[player], # 9
|
"retro": world.retro[player], # 9
|
||||||
"progressive": world.progressive, # A
|
"progressive": world.progressive, # A
|
||||||
"hints": 'True' if world.hints[player] else 'False' # B
|
"hints": 'True' if world.hints[player] else 'False' # B
|
||||||
}
|
}
|
||||||
# 0 1 2 3 4 5 6 7 8 9 A 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' % (
|
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
|
# 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-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-retro
|
||||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
|
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
|
||||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
|
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
|
||||||
outfilestuffs["logic"], # 0
|
outfilestuffs["logic"], # 0
|
||||||
|
|
||||||
outfilestuffs["difficulty"], # 1
|
outfilestuffs["difficulty"], # 1
|
||||||
outfilestuffs["item_functionality"], # 2
|
outfilestuffs["item_functionality"], # 2
|
||||||
outfilestuffs["mode"], # 3
|
outfilestuffs["mode"], # 3
|
||||||
outfilestuffs["goal"], # 4
|
outfilestuffs["goal"], # 4
|
||||||
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
|
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
|
||||||
|
|
||||||
outfilestuffs["shuffle"], # 6
|
outfilestuffs["shuffle"], # 6
|
||||||
outfilestuffs["algorithm"], # 7
|
outfilestuffs["algorithm"], # 7
|
||||||
outfilestuffs["mscb"], # 8
|
outfilestuffs["mscb"], # 8
|
||||||
|
|
||||||
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
|
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
|
||||||
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
|
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
|
||||||
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
|
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
|
||||||
) if not args.outputname else ''
|
) if not args.outputname else ''
|
||||||
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||||
rom.write_to_file(rompath, hide_enemizer=True)
|
rom.write_to_file(rompath, hide_enemizer=True)
|
||||||
if args.create_diff:
|
if args.create_diff:
|
||||||
Patch.create_patch_file(rompath)
|
Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
|
||||||
return player, team, bytes(rom.name)
|
return player, team, bytes(rom.name)
|
||||||
|
|
||||||
pool = concurrent.futures.ThreadPoolExecutor()
|
pool = concurrent.futures.ThreadPoolExecutor()
|
||||||
multidata_task = None
|
|
||||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||||
if not args.suppress_rom:
|
|
||||||
|
|
||||||
rom_futures = []
|
rom_futures = []
|
||||||
mod_futures = []
|
output_file_futures = []
|
||||||
for team in range(world.teams):
|
for team in range(world.teams):
|
||||||
for player in world.alttp_player_ids:
|
for player in world.alttp_player_ids:
|
||||||
rom_futures.append(pool.submit(_gen_rom, team, player))
|
rom_futures.append(pool.submit(_gen_rom, team, player))
|
||||||
for player in world.factorio_player_ids:
|
for player in world.player_ids:
|
||||||
mod_futures.append(pool.submit(generate_mod, world, player,
|
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player))
|
||||||
str(args.outputname if args.outputname else world.seed)))
|
|
||||||
|
|
||||||
def get_entrance_to_region(region: Region):
|
def get_entrance_to_region(region: Region):
|
||||||
for entrance in region.entrances:
|
for entrance in region.entrances:
|
||||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld):
|
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld):
|
||||||
return entrance
|
return entrance
|
||||||
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
||||||
return get_entrance_to_region(entrance.parent_region)
|
return get_entrance_to_region(entrance.parent_region)
|
||||||
|
|
||||||
# collect ER hint info
|
# 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
|
||||||
from worlds.alttp.Regions import RegionType
|
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||||
for region in world.regions:
|
from worlds.alttp.Regions import RegionType
|
||||||
if region.player in er_hint_data and region.locations:
|
for region in world.regions:
|
||||||
main_entrance = get_entrance_to_region(region)
|
if region.player in er_hint_data and region.locations:
|
||||||
for location in region.locations:
|
main_entrance = get_entrance_to_region(region)
|
||||||
if type(location.address) == int: # skips events and crystals
|
for location in region.locations:
|
||||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
if type(location.address) == int: # skips events and crystals
|
||||||
er_hint_data[region.player][location.address] = main_entrance.name
|
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||||
|
er_hint_data[region.player][location.address] = main_entrance.name
|
||||||
|
|
||||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||||
|
|
||||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||||
for player in range(1, world.players + 1)}
|
for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
checks_in_area[player]["Total"] = 0
|
checks_in_area[player]["Total"] = 0
|
||||||
|
|
||||||
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
|
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
|
||||||
main_entrance = get_entrance_to_region(location.parent_region)
|
main_entrance = get_entrance_to_region(location.parent_region)
|
||||||
if location.game != Games.LTTP:
|
if location.game != Games.LTTP:
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
checks_in_area[location.player]["Light World"].append(location.address)
|
||||||
elif location.parent_region.dungeon:
|
elif location.parent_region.dungeon:
|
||||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
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)
|
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||||
checks_in_area[location.player][dungeonname].append(location.address)
|
checks_in_area[location.player][dungeonname].append(location.address)
|
||||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
checks_in_area[location.player]["Light World"].append(location.address)
|
||||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||||
checks_in_area[location.player]["Total"] += 1
|
checks_in_area[location.player]["Total"] += 1
|
||||||
|
|
||||||
oldmancaves = []
|
oldmancaves = []
|
||||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
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 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]]:
|
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
|
||||||
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
world.retro[player]]:
|
||||||
player = region.player
|
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||||
location_id = SHOP_ID_START + total_shop_slots + index
|
region.player)
|
||||||
|
player = region.player
|
||||||
|
location_id = SHOP_ID_START + total_shop_slots + index
|
||||||
|
|
||||||
main_entrance = get_entrance_to_region(region)
|
main_entrance = get_entrance_to_region(region)
|
||||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||||
checks_in_area[player]["Light World"].append(location_id)
|
checks_in_area[player]["Light World"].append(location_id)
|
||||||
else:
|
else:
|
||||||
checks_in_area[player]["Dark World"].append(location_id)
|
checks_in_area[player]["Dark World"].append(location_id)
|
||||||
checks_in_area[player]["Total"] += 1
|
checks_in_area[player]["Total"] += 1
|
||||||
|
|
||||||
er_hint_data[player][location_id] = main_entrance.name
|
er_hint_data[player][location_id] = main_entrance.name
|
||||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||||
|
|
||||||
precollected_items = {player: [] for player in range(1, world.players+1)}
|
FillDisabledShopSlots(world)
|
||||||
|
|
||||||
|
def write_multidata(roms, outputs):
|
||||||
|
import base64
|
||||||
|
import NetUtils
|
||||||
|
for future in roms:
|
||||||
|
rom_name = future.result()
|
||||||
|
rom_names.append(rom_name)
|
||||||
|
slot_data = {}
|
||||||
|
client_versions = {}
|
||||||
|
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||||
|
games = {}
|
||||||
|
for slot in world.player_ids:
|
||||||
|
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)}
|
||||||
for item in world.precollected_items:
|
for item in world.precollected_items:
|
||||||
precollected_items[item.player].append(item.code)
|
precollected_items[item.player].append(item.code)
|
||||||
|
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.tech_tree_information[player].value == 2:
|
||||||
|
sending_visible_players.add(player)
|
||||||
|
|
||||||
FillDisabledShopSlots(world)
|
for i, team in enumerate(parsed_names):
|
||||||
|
for player, name in enumerate(team, 1):
|
||||||
def write_multidata(roms, mods):
|
if player not in world.alttp_player_ids:
|
||||||
import base64
|
connect_names[name] = (i, player)
|
||||||
for future in roms:
|
if world.hk_player_ids:
|
||||||
rom_name = future.result()
|
|
||||||
rom_names.append(rom_name)
|
|
||||||
slot_data = {}
|
|
||||||
client_versions = {}
|
|
||||||
minimum_versions = {"server": (0, 0, 4), "clients": client_versions}
|
|
||||||
games = {}
|
|
||||||
for slot in world.player_ids:
|
|
||||||
client_versions[slot] = (0, 0, 3)
|
|
||||||
games[slot] = world.game[slot]
|
|
||||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
|
||||||
slot, team, rom_name in rom_names}
|
|
||||||
|
|
||||||
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:
|
for slot in world.hk_player_ids:
|
||||||
slots_data = slot_data[slot] = {}
|
slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot)
|
||||||
for option_name in Options.hollow_knight_options:
|
for slot in world.minecraft_player_ids:
|
||||||
option = getattr(world, option_name)[slot]
|
slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot)
|
||||||
slots_data[option_name] = int(option.value)
|
|
||||||
for slot in world.minecraft_player_ids:
|
|
||||||
slot_data[slot] = fill_minecraft_slot_data(world, slot)
|
|
||||||
multidata = zlib.compress(pickle.dumps({
|
|
||||||
"slot_data" : slot_data,
|
|
||||||
"games": games,
|
|
||||||
"names": parsed_names,
|
|
||||||
"connect_names": connect_names,
|
|
||||||
"remote_items": {player for player in range(1, world.players + 1) if
|
|
||||||
world.remote_items[player]},
|
|
||||||
"locations": {
|
|
||||||
(location.address, location.player):
|
|
||||||
(location.item.code, location.item.player)
|
|
||||||
for location in world.get_filled_locations() if
|
|
||||||
type(location.address) is int},
|
|
||||||
"checks_in_area": checks_in_area,
|
|
||||||
"server_options": get_options()["server_options"],
|
|
||||||
"er_hint_data": er_hint_data,
|
|
||||||
"precollected_items": precollected_items,
|
|
||||||
"version": tuple(_version_tuple),
|
|
||||||
"tags": ["AP"],
|
|
||||||
"minimum_versions": minimum_versions,
|
|
||||||
"seed_name": str(args.outputname if args.outputname else world.seed)
|
|
||||||
}), 9)
|
|
||||||
|
|
||||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||||
f.write(bytes([1])) # version of format
|
for location in world.get_filled_locations():
|
||||||
f.write(multidata)
|
if type(location.address) == int:
|
||||||
for future in mods:
|
locations_data[location.player][location.address] = (location.item.code, location.item.player)
|
||||||
future.result() # collect errors if they occured
|
if location.player in sending_visible_players and location.item.player != location.player:
|
||||||
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
|
location.item.code, False)
|
||||||
|
precollected_hints[location.player].add(hint)
|
||||||
|
precollected_hints[location.item.player].add(hint)
|
||||||
|
elif location.item.name in args.start_hints[location.item.player]:
|
||||||
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
|
location.item.code, False,
|
||||||
|
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||||
|
precollected_hints[location.player].add(hint)
|
||||||
|
precollected_hints[location.item.player].add(hint)
|
||||||
|
|
||||||
multidata_task = pool.submit(write_multidata, rom_futures, mod_futures)
|
multidata = zlib.compress(pickle.dumps({
|
||||||
|
"slot_data": slot_data,
|
||||||
|
"games": games,
|
||||||
|
"names": parsed_names,
|
||||||
|
"connect_names": connect_names,
|
||||||
|
"remote_items": {player for player in range(1, world.players + 1) if
|
||||||
|
world.remote_items[player]},
|
||||||
|
"locations": locations_data,
|
||||||
|
"checks_in_area": checks_in_area,
|
||||||
|
"server_options": get_options()["server_options"],
|
||||||
|
"er_hint_data": er_hint_data,
|
||||||
|
"precollected_items": precollected_items,
|
||||||
|
"precollected_hints": precollected_hints,
|
||||||
|
"version": tuple(version_tuple),
|
||||||
|
"tags": ["AP"],
|
||||||
|
"minimum_versions": minimum_versions,
|
||||||
|
"seed_name": world.seed_name
|
||||||
|
}), 9)
|
||||||
|
|
||||||
|
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||||
|
f.write(bytes([1])) # version of format
|
||||||
|
f.write(multidata)
|
||||||
|
for future in outputs:
|
||||||
|
future.result() # collect errors if they occured
|
||||||
|
|
||||||
|
multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
|
||||||
if not check_accessibility_task.result():
|
if not check_accessibility_task.result():
|
||||||
if not world.can_beat_game():
|
if not world.can_beat_game():
|
||||||
raise Exception("Game appears as unbeatable. Aborting.")
|
raise Exception("Game appears as unbeatable. Aborting.")
|
||||||
@@ -557,8 +555,6 @@ def main(args, seed=None):
|
|||||||
if multidata_task:
|
if multidata_task:
|
||||||
multidata_task.result() # retrieve exception if one exists
|
multidata_task.result() # retrieve exception if one exists
|
||||||
pool.shutdown() # wait for all queued tasks to complete
|
pool.shutdown() # wait for all queued tasks to complete
|
||||||
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
|
|
||||||
generate_mc_data(world, player, str(args.outputname if args.outputname else world.seed))
|
|
||||||
if not args.skip_playthrough:
|
if not args.skip_playthrough:
|
||||||
logger.info('Calculating playthrough.')
|
logger.info('Calculating playthrough.')
|
||||||
create_playthrough(world)
|
create_playthrough(world)
|
||||||
@@ -612,7 +608,8 @@ def create_playthrough(world):
|
|||||||
to_delete = set()
|
to_delete = set()
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
# we remove the item at location and check if game is still beatable
|
# 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
|
old_item = location.item
|
||||||
location.item = None
|
location.item = None
|
||||||
if world.can_beat_game(state_cache[num]):
|
if world.can_beat_game(state_cache[num]):
|
||||||
@@ -657,7 +654,8 @@ def create_playthrough(world):
|
|||||||
|
|
||||||
collection_spheres.append(sphere)
|
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:
|
if not sphere:
|
||||||
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
||||||
|
|
||||||
@@ -674,16 +672,25 @@ def create_playthrough(world):
|
|||||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||||
return list(pathpairs)
|
return list(pathpairs)
|
||||||
|
|
||||||
world.spoiler.paths = dict()
|
world.spoiler.paths = {}
|
||||||
for player in range(1, world.players + 1):
|
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
|
||||||
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})
|
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:
|
if player in world.alttp_player_ids:
|
||||||
for path in dict(world.spoiler.paths).values():
|
for path in dict(world.spoiler.paths).values():
|
||||||
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
||||||
if world.mode[player] != 'inverted':
|
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:
|
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
|
# we can finally output our playthrough
|
||||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||||
|
|||||||
@@ -1,55 +1,48 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import importlib
|
import pkg_resources
|
||||||
|
|
||||||
|
requirements_files = {'requirements.txt'}
|
||||||
|
|
||||||
if sys.version_info < (3, 8, 6):
|
if sys.version_info < (3, 8, 6):
|
||||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
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():
|
def update_command():
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade'])
|
for file in requirements_files:
|
||||||
|
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
|
||||||
|
|
||||||
naming_specialties = {"PyYAML": "yaml", # PyYAML is imported as the name yaml
|
|
||||||
"maseya-z3pr": "maseya",
|
|
||||||
"factorio-rcon-py": "factorio_rcon"}
|
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
global update_ran
|
global update_ran
|
||||||
if not update_ran:
|
if not update_ran:
|
||||||
update_ran = True
|
update_ran = True
|
||||||
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt')
|
for req_file in requirements_files:
|
||||||
if not os.path.exists(path):
|
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||||
path = os.path.join(os.path.dirname(__file__), 'requirements.txt')
|
if not os.path.exists(path):
|
||||||
with open(path) as requirementsfile:
|
path = os.path.join(os.path.dirname(__file__), req_file)
|
||||||
for line in requirementsfile.readlines():
|
with open(path) as requirementsfile:
|
||||||
module, remote_version = line.split(">=")
|
requirements = pkg_resources.parse_requirements(requirementsfile)
|
||||||
module = naming_specialties.get(module, module)
|
for requirement in requirements:
|
||||||
try:
|
requirement = str(requirement)
|
||||||
module = importlib.import_module(module)
|
try:
|
||||||
except:
|
pkg_resources.require(requirement)
|
||||||
import traceback
|
except pkg_resources.ResolutionError:
|
||||||
traceback.print_exc()
|
import traceback
|
||||||
input(f'Required python module {module} not found, press enter to install it')
|
traceback.print_exc()
|
||||||
update_command()
|
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||||
return
|
update_command()
|
||||||
else:
|
return
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import concurrent.futures
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
|
|
||||||
def feedback(text: str):
|
def feedback(text: str):
|
||||||
@@ -26,7 +27,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
from Utils import get_public_ipv4, get_options
|
from Utils import get_public_ipv4, get_options
|
||||||
from Mystery import get_seed_name
|
from Mystery import get_seed_name
|
||||||
from Patch import create_patch_file
|
|
||||||
|
|
||||||
options = get_options()
|
options = get_options()
|
||||||
|
|
||||||
@@ -41,11 +41,11 @@ if __name__ == "__main__":
|
|||||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||||
zip_roms = multi_mystery_options["zip_roms"]
|
zip_roms = multi_mystery_options["zip_roms"]
|
||||||
zip_diffs = multi_mystery_options["zip_diffs"]
|
zip_diffs = multi_mystery_options["zip_diffs"]
|
||||||
|
zip_apmcs = multi_mystery_options["zip_apmcs"]
|
||||||
zip_spoiler = multi_mystery_options["zip_spoiler"]
|
zip_spoiler = multi_mystery_options["zip_spoiler"]
|
||||||
zip_multidata = multi_mystery_options["zip_multidata"]
|
zip_multidata = multi_mystery_options["zip_multidata"]
|
||||||
zip_format = multi_mystery_options["zip_format"]
|
zip_format = multi_mystery_options["zip_format"]
|
||||||
# zip_password = multi_mystery_options["zip_password"] not at this time
|
# 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"]
|
meta_file_path = multi_mystery_options["meta_file_path"]
|
||||||
weights_file_path = multi_mystery_options["weights_file_path"]
|
weights_file_path = multi_mystery_options["weights_file_path"]
|
||||||
pre_roll = multi_mystery_options["pre_roll"]
|
pre_roll = multi_mystery_options["pre_roll"]
|
||||||
@@ -76,9 +76,11 @@ if __name__ == "__main__":
|
|||||||
if os.path.exists("ArchipelagoMystery.exe"):
|
if os.path.exists("ArchipelagoMystery.exe"):
|
||||||
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
|
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
|
||||||
elif os.path.exists("ArchipelagoMystery"):
|
elif os.path.exists("ArchipelagoMystery"):
|
||||||
basemysterycommand = "ArchipelagoMystery" # compiled linux
|
basemysterycommand = "./ArchipelagoMystery" # compiled linux
|
||||||
|
elif which('py'):
|
||||||
|
basemysterycommand = f"py -{py_version} Mystery.py" # source windows
|
||||||
else:
|
else:
|
||||||
basemysterycommand = f"py -{py_version} Mystery.py" # source
|
basemysterycommand = f"python3 Mystery.py" # source others
|
||||||
|
|
||||||
weights_file_path = os.path.join(player_files_path, weights_file_path)
|
weights_file_path = os.path.join(player_files_path, weights_file_path)
|
||||||
if os.path.exists(weights_file_path):
|
if os.path.exists(weights_file_path):
|
||||||
@@ -124,16 +126,7 @@ if __name__ == "__main__":
|
|||||||
spoilername = f"AP_{seed_name}_Spoiler.txt"
|
spoilername = f"AP_{seed_name}_Spoiler.txt"
|
||||||
romfilename = ""
|
romfilename = ""
|
||||||
|
|
||||||
if player_name:
|
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)):
|
||||||
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)):
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
compression = {1: zipfile.ZIP_DEFLATED,
|
compression = {1: zipfile.ZIP_DEFLATED,
|
||||||
@@ -167,7 +160,7 @@ if __name__ == "__main__":
|
|||||||
def _handle_sfc_file(file: str):
|
def _handle_sfc_file(file: str):
|
||||||
if zip_roms:
|
if zip_roms:
|
||||||
pack_file(file)
|
pack_file(file)
|
||||||
if zip_roms == 2 and player_name.lower() not in file.lower():
|
if zip_roms == 2:
|
||||||
remove_zipped_file(file)
|
remove_zipped_file(file)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,15 +171,28 @@ if __name__ == "__main__":
|
|||||||
remove_zipped_file(file)
|
remove_zipped_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_apmc_file(file: str):
|
||||||
|
if zip_apmcs:
|
||||||
|
pack_file(file)
|
||||||
|
if zip_apmcs == 2:
|
||||||
|
remove_zipped_file(file)
|
||||||
|
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||||
futures = []
|
futures = []
|
||||||
|
files = os.listdir(output_path)
|
||||||
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
|
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
|
||||||
for file in os.listdir(output_path):
|
for file in files:
|
||||||
if seed_name in file:
|
if seed_name in file:
|
||||||
if file.endswith(".sfc"):
|
if file.endswith(".sfc"):
|
||||||
futures.append(pool.submit(_handle_sfc_file, file))
|
futures.append(pool.submit(_handle_sfc_file, file))
|
||||||
elif file.endswith(".apbp"):
|
elif file.endswith(".apbp"):
|
||||||
futures.append(pool.submit(_handle_diff_file, file))
|
futures.append(pool.submit(_handle_diff_file, file))
|
||||||
|
elif file.endswith(".apmc"):
|
||||||
|
futures.append(pool.submit(_handle_apmc_file, file))
|
||||||
|
# just handle like a diff file for now
|
||||||
|
elif file.endswith(".zip"):
|
||||||
|
futures.append(pool.submit(_handle_diff_file, file))
|
||||||
|
|
||||||
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
|
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
|
||||||
pack_file(multidataname)
|
pack_file(multidataname)
|
||||||
@@ -204,14 +210,15 @@ if __name__ == "__main__":
|
|||||||
if not args.disable_autohost:
|
if not args.disable_autohost:
|
||||||
if os.path.exists(os.path.join(output_path, multidataname)):
|
if os.path.exists(os.path.join(output_path, multidataname)):
|
||||||
if os.path.exists("ArchipelagoServer.exe"):
|
if os.path.exists("ArchipelagoServer.exe"):
|
||||||
baseservercommand = "ArchipelagoServer.exe" # compiled windows
|
baseservercommand = ["ArchipelagoServer.exe"] # compiled windows
|
||||||
elif os.path.exists("ArchipelagoServer"):
|
elif os.path.exists("ArchipelagoServer"):
|
||||||
baseservercommand = "ArchipelagoServer" # compiled linux
|
baseservercommand = ["./ArchipelagoServer"] # compiled linux
|
||||||
|
elif which('py'):
|
||||||
|
baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source windows
|
||||||
else:
|
else:
|
||||||
baseservercommand = 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.
|
# don't have a mac to test that. If you try to run compiled on mac, good luck.
|
||||||
|
subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)])
|
||||||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|||||||
234
MultiServer.py
234
MultiServer.py
@@ -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
|
lookup_any_location_id_to_name, lookup_any_location_name_to_id
|
||||||
import Utils
|
import Utils
|
||||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
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
|
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
@@ -39,11 +39,12 @@ all_items = frozenset(lookup_any_item_name_to_id)
|
|||||||
all_locations = frozenset(lookup_any_location_name_to_id)
|
all_locations = frozenset(lookup_any_location_name_to_id)
|
||||||
all_console_names = frozenset(all_items | all_locations)
|
all_console_names = frozenset(all_items | all_locations)
|
||||||
|
|
||||||
|
|
||||||
class Client(Endpoint):
|
class Client(Endpoint):
|
||||||
version = Version(0, 0, 0)
|
version = Version(0, 0, 0)
|
||||||
tags: typing.List[str] = []
|
tags: typing.List[str] = []
|
||||||
|
|
||||||
def __init__(self, socket: websockets.server.WebSocketServerProtocol, ctx: Context):
|
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
|
||||||
super().__init__(socket)
|
super().__init__(socket)
|
||||||
self.auth = False
|
self.auth = False
|
||||||
self.name = None
|
self.name = None
|
||||||
@@ -75,10 +76,10 @@ class Context(Node):
|
|||||||
self.save_filename = None
|
self.save_filename = None
|
||||||
self.saving = False
|
self.saving = False
|
||||||
self.player_names = {}
|
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.allow_forfeits = {}
|
||||||
self.remote_items = set()
|
self.remote_items = set()
|
||||||
self.locations = {}
|
self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.server_password = server_password
|
self.server_password = server_password
|
||||||
@@ -114,6 +115,11 @@ class Context(Node):
|
|||||||
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
||||||
self.seed_name = ""
|
self.seed_name = ""
|
||||||
|
|
||||||
|
def get_hint_cost(self, slot):
|
||||||
|
if self.hint_cost:
|
||||||
|
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||||
|
return 0
|
||||||
|
|
||||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||||
with open(multidatapath, 'rb') as f:
|
with open(multidatapath, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
@@ -131,9 +137,9 @@ class Context(Node):
|
|||||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||||
|
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > Utils._version_tuple:
|
if mdata_ver > Utils.version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
|
raise RuntimeError(f"Supplied Multidata (.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", {})
|
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||||
self.minimum_client_versions = {}
|
self.minimum_client_versions = {}
|
||||||
for player, version in clients_ver.items():
|
for player, version in clients_ver.items():
|
||||||
@@ -155,12 +161,12 @@ class Context(Node):
|
|||||||
for slot, item_codes in decoded_obj["precollected_items"].items():
|
for slot, item_codes in decoded_obj["precollected_items"].items():
|
||||||
if slot in self.remote_items:
|
if slot in self.remote_items:
|
||||||
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||||
|
for slot, hints in decoded_obj["precollected_hints"].items():
|
||||||
|
self.hints[team, slot].update(hints)
|
||||||
if use_embedded_server_options:
|
if use_embedded_server_options:
|
||||||
server_options = decoded_obj.get("server_options", {})
|
server_options = decoded_obj.get("server_options", {})
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
||||||
|
|
||||||
def get_players_package(self):
|
def get_players_package(self):
|
||||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||||
|
|
||||||
@@ -168,7 +174,7 @@ class Context(Node):
|
|||||||
for key, value in server_options.items():
|
for key, value in server_options.items():
|
||||||
data_type = self.simple_options.get(key, None)
|
data_type = self.simple_options.get(key, None)
|
||||||
if data_type is not 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:
|
try:
|
||||||
value = data_type(value)
|
value = data_type(value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -194,7 +200,7 @@ class Context(Node):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _save(self, exit_save:bool=False) -> bool:
|
def _save(self, exit_save: bool = False) -> bool:
|
||||||
try:
|
try:
|
||||||
encoded_save = pickle.dumps(self.get_save())
|
encoded_save = pickle.dumps(self.get_save())
|
||||||
with open(self.save_filename, "wb") as f:
|
with open(self.save_filename, "wb") as f:
|
||||||
@@ -238,44 +244,50 @@ class Context(Node):
|
|||||||
import atexit
|
import atexit
|
||||||
atexit.register(self._save, True) # make sure we save on exit too
|
atexit.register(self._save, True) # make sure we save on exit too
|
||||||
|
|
||||||
|
def recheck_hints(self):
|
||||||
|
for team, slot in self.hints:
|
||||||
|
self.hints[team, slot] = {
|
||||||
|
hint.re_check(self, team) for hint in
|
||||||
|
self.hints[team, slot]
|
||||||
|
}
|
||||||
|
|
||||||
def get_save(self) -> dict:
|
def get_save(self) -> dict:
|
||||||
|
self.recheck_hints()
|
||||||
d = {
|
d = {
|
||||||
"rom_names": list(self.connect_names.items()),
|
"connect_names": self.connect_names,
|
||||||
"received_items": tuple((k, v) for k, v in self.received_items.items()),
|
"received_items": self.received_items,
|
||||||
"hints_used": tuple((key, value) for key, value in self.hints_used.items()),
|
"hints_used": dict(self.hints_used),
|
||||||
"hints": tuple(
|
"hints": dict(self.hints),
|
||||||
(key, list(hint.re_check(self, key[0]) for hint in value)) for key, value in self.hints.items()),
|
"location_checks": dict(self.location_checks),
|
||||||
"location_checks": tuple((key, tuple(value)) for key, value in self.location_checks.items()),
|
"name_aliases": self.name_aliases,
|
||||||
"name_aliases": tuple((key, value) for key, value in self.name_aliases.items()),
|
"client_game_state": dict(self.client_game_state),
|
||||||
"client_game_state": tuple((key, value) for key, value in self.client_game_state.items()),
|
|
||||||
"client_activity_timers": tuple(
|
"client_activity_timers": tuple(
|
||||||
(key, value.timestamp()) for key, value in self.client_activity_timers.items()),
|
(key, value.timestamp()) for key, value in self.client_activity_timers.items()),
|
||||||
"client_connection_timers": tuple(
|
"client_connection_timers": tuple(
|
||||||
(key, value.timestamp()) for key, value in self.client_connection_timers.items()),
|
(key, value.timestamp()) for key, value in self.client_connection_timers.items()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def set_save(self, savedata: dict):
|
def set_save(self, savedata: dict):
|
||||||
|
if self.connect_names != savedata["connect_names"]:
|
||||||
|
raise Exception("This savegame does not appear to match the loaded multiworld.")
|
||||||
|
self.received_items = savedata["received_items"]
|
||||||
|
self.hints_used.update(savedata["hints_used"])
|
||||||
|
self.hints.update(savedata["hints"])
|
||||||
|
|
||||||
received_items = {tuple(k): [NetworkItem(*i) for i in v] for k, v in savedata["received_items"]}
|
self.name_aliases.update(savedata["name_aliases"])
|
||||||
|
self.client_game_state.update(savedata["client_game_state"])
|
||||||
self.received_items = received_items
|
|
||||||
self.hints_used.update({tuple(key): value for key, value in savedata["hints_used"]})
|
|
||||||
self.hints.update(
|
|
||||||
{tuple(key): set(NetUtils.Hint(*hint) for hint in value) for key, value in savedata["hints"]})
|
|
||||||
|
|
||||||
self.name_aliases.update({tuple(key): value for key, value in savedata["name_aliases"]})
|
|
||||||
self.client_game_state.update({tuple(key): value for key, value in savedata["client_game_state"]})
|
|
||||||
self.client_connection_timers.update(
|
self.client_connection_timers.update(
|
||||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||||
in savedata["client_connection_timers"]})
|
in savedata["client_connection_timers"]})
|
||||||
self.client_activity_timers.update(
|
self.client_activity_timers.update(
|
||||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||||
in savedata["client_activity_timers"]})
|
in savedata["client_activity_timers"]})
|
||||||
self.location_checks.update({tuple(key): set(value) for key, value in savedata["location_checks"]})
|
self.location_checks.update(savedata["location_checks"])
|
||||||
|
|
||||||
logging.info(f'Loaded save file with {sum([len(p) for p in received_items.values()])} received items '
|
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
|
||||||
f'for {len(received_items)} players')
|
f'for {len(self.received_items)} players')
|
||||||
|
|
||||||
def get_aliased_name(self, team: int, slot: int):
|
def get_aliased_name(self, team: int, slot: int):
|
||||||
if (team, slot) in self.name_aliases:
|
if (team, slot) in self.name_aliases:
|
||||||
@@ -316,16 +328,20 @@ class Context(Node):
|
|||||||
|
|
||||||
|
|
||||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||||
cmd = ctx.dumper([{"cmd": "Hint", "hints" : hints}])
|
concerns = collections.defaultdict(list)
|
||||||
commands = ctx.dumper([hint.as_network_message() for hint in hints])
|
for hint in hints:
|
||||||
|
net_msg = hint.as_network_message()
|
||||||
|
concerns[hint.receiving_player].append(net_msg)
|
||||||
|
if not hint.local:
|
||||||
|
concerns[hint.finding_player].append(net_msg)
|
||||||
for text in (format_hint(ctx, team, hint) for hint in hints):
|
for text in (format_hint(ctx, team, hint) for hint in hints):
|
||||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||||
|
|
||||||
for client in ctx.endpoints:
|
for client in ctx.endpoints:
|
||||||
if client.auth and client.team == team:
|
if client.auth and client.team == team:
|
||||||
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
client_hints = concerns[client.slot]
|
||||||
asyncio.create_task(ctx.send_encoded_msgs(client, commands))
|
if client_hints:
|
||||||
|
asyncio.create_task(ctx.send_msgs(client, client_hints))
|
||||||
|
|
||||||
|
|
||||||
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
|
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
|
||||||
@@ -358,7 +374,8 @@ async def server(websocket, path, ctx: Context):
|
|||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
finally:
|
finally:
|
||||||
logging.info("Disconnected")
|
if ctx.log_network:
|
||||||
|
logging.info("Disconnected")
|
||||||
await ctx.disconnect(client)
|
await ctx.disconnect(client)
|
||||||
|
|
||||||
|
|
||||||
@@ -366,12 +383,14 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
'cmd': 'RoomInfo',
|
'cmd': 'RoomInfo',
|
||||||
'password': ctx.password is not None,
|
'password': ctx.password is not None,
|
||||||
'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client
|
'players': [
|
||||||
in ctx.endpoints if client.auth],
|
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.
|
# tags are for additional features in the communication.
|
||||||
# Name them by feature or fork, as you feel is appropriate.
|
# Name them by feature or fork, as you feel is appropriate.
|
||||||
'tags': ctx.tags,
|
'tags': ctx.tags,
|
||||||
'version': Utils._version_tuple,
|
'version': Utils.version_tuple,
|
||||||
'forfeit_mode': ctx.forfeit_mode,
|
'forfeit_mode': ctx.forfeit_mode,
|
||||||
'remaining_mode': ctx.remaining_mode,
|
'remaining_mode': ctx.remaining_mode,
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
@@ -395,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)
|
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
async def on_client_left(ctx: Context, client: Client):
|
async def on_client_left(ctx: Context, client: Client):
|
||||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
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)
|
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@@ -437,34 +458,30 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
|
|||||||
return ctx.received_items.setdefault((team, player), [])
|
return ctx.received_items.setdefault((team, player), [])
|
||||||
|
|
||||||
|
|
||||||
def tuplize_received_items(items):
|
|
||||||
return [NetworkItem(item.item, item.location, item.player) for item in items]
|
|
||||||
|
|
||||||
|
|
||||||
def send_new_items(ctx: Context):
|
def send_new_items(ctx: Context):
|
||||||
for client in ctx.endpoints:
|
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)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if len(items) > client.send_index:
|
if len(items) > client.send_index:
|
||||||
asyncio.create_task(ctx.send_msgs(client, [{
|
asyncio.create_task(ctx.send_msgs(client, [{
|
||||||
"cmd": "ReceivedItems",
|
"cmd": "ReceivedItems",
|
||||||
"index": client.send_index,
|
"index": client.send_index,
|
||||||
"items": tuplize_received_items(items)[client.send_index:]}]))
|
"items": items[client.send_index:]}]))
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
|
|
||||||
|
|
||||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||||
# register any locations that are in the multidata
|
# register any locations that are in the multidata
|
||||||
all_locations = {location_id for location_id, location_slot in ctx.locations if location_slot == slot}
|
all_locations = set(ctx.locations[slot])
|
||||||
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||||
register_location_checks(ctx, team, slot, all_locations)
|
register_location_checks(ctx, team, slot, all_locations)
|
||||||
|
|
||||||
|
|
||||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
items = []
|
items = []
|
||||||
for (location, location_slot) in ctx.locations:
|
for location_id in ctx.locations[slot]:
|
||||||
if location_slot == slot and location not in ctx.location_checks[team, slot]:
|
if location_id not in ctx.location_checks[team, slot]:
|
||||||
items.append(ctx.locations[location, slot][0]) # item ID
|
items.append(ctx.locations[slot][location_id][0]) # item ID
|
||||||
return sorted(items)
|
return sorted(items)
|
||||||
|
|
||||||
|
|
||||||
@@ -473,8 +490,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
|||||||
if new_locations:
|
if new_locations:
|
||||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
for location in new_locations:
|
for location in new_locations:
|
||||||
if (location, slot) in ctx.locations:
|
if location in ctx.locations[slot]:
|
||||||
item_id, target_player = ctx.locations[(location, slot)]
|
item_id, target_player = ctx.locations[slot][location]
|
||||||
new_item = NetworkItem(item_id, location, slot)
|
new_item = NetworkItem(item_id, location, slot)
|
||||||
if target_player != slot or slot in ctx.remote_items:
|
if target_player != slot or slot in ctx.remote_items:
|
||||||
get_received_items(ctx, team, target_player).append(new_item)
|
get_received_items(ctx, team, target_player).append(new_item)
|
||||||
@@ -500,33 +517,28 @@ def notify_team(ctx: Context, team: int, text: str):
|
|||||||
ctx.broadcast_team(team, [['Print', {"text": text}]])
|
ctx.broadcast_team(team, [['Print', {"text": text}]])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
|
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
|
||||||
hints = []
|
hints = []
|
||||||
seeked_item_id = lookup_any_item_name_to_id[item]
|
seeked_item_id = lookup_any_item_name_to_id[item]
|
||||||
for check, result in ctx.locations.items():
|
for finding_player, check_data in ctx.locations.items():
|
||||||
item_id, receiving_player = result
|
for location_id, result in check_data.items():
|
||||||
if receiving_player == slot and item_id == seeked_item_id:
|
item_id, receiving_player = result
|
||||||
location_id, finding_player = check
|
if receiving_player == slot and item_id == seeked_item_id:
|
||||||
found = location_id in ctx.location_checks[team, finding_player]
|
found = location_id in ctx.location_checks[team, finding_player]
|
||||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
|
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
|
||||||
|
|
||||||
return hints
|
return hints
|
||||||
|
|
||||||
|
|
||||||
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||||
hints = []
|
seeked_location: int = Regions.lookup_name_to_id[location]
|
||||||
seeked_location = Regions.lookup_name_to_id[location]
|
item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
|
||||||
for check, result in ctx.locations.items():
|
if item_id:
|
||||||
location_id, finding_player = check
|
found = seeked_location in ctx.location_checks[team, slot]
|
||||||
if finding_player == slot and location_id == seeked_location:
|
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||||
item_id, receiving_player = result
|
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance)]
|
||||||
found = location_id in ctx.location_checks[team, finding_player]
|
return []
|
||||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
|
||||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
|
|
||||||
break # each location has 1 item
|
|
||||||
return hints
|
|
||||||
|
|
||||||
|
|
||||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||||
@@ -539,6 +551,7 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
|||||||
text += f" at {hint.entrance}"
|
text += f" at {hint.entrance}"
|
||||||
return text + (". (found)" if hint.found else ".")
|
return text + (". (found)" if hint.found else ".")
|
||||||
|
|
||||||
|
|
||||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||||
parts = []
|
parts = []
|
||||||
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
||||||
@@ -556,9 +569,12 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
|||||||
NetUtils.add_json_text(parts, ")")
|
NetUtils.add_json_text(parts, ")")
|
||||||
|
|
||||||
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
|
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
|
||||||
"receiving": receiving_player, "sending": net_item.player}
|
"receiving": receiving_player,
|
||||||
|
"item": net_item}
|
||||||
|
|
||||||
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str]= all_console_names) -> typing.Tuple[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)
|
picks = fuzzy_process.extract(input_text, possible_answers, limit=2)
|
||||||
if len(picks) > 1:
|
if len(picks) > 1:
|
||||||
dif = picks[0][1] - picks[1][1]
|
dif = picks[0][1] - picks[1][1]
|
||||||
@@ -683,11 +699,12 @@ class CommonCommandProcessor(CommandProcessor):
|
|||||||
"""List all current options. Warning: lists password."""
|
"""List all current options. Warning: lists password."""
|
||||||
self.output("Current options:")
|
self.output("Current options:")
|
||||||
for option in self.ctx.simple_options:
|
for option in self.ctx.simple_options:
|
||||||
if option == "server_password" and self.marker == "!": #Do not display the server password to the client.
|
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))}")
|
self.output(f"Option server_password is set to {('*' * random.randint(4, 16))}")
|
||||||
else:
|
else:
|
||||||
self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
|
self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
|
||||||
|
|
||||||
|
|
||||||
class ClientMessageProcessor(CommonCommandProcessor):
|
class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
marker = "!"
|
marker = "!"
|
||||||
|
|
||||||
@@ -714,11 +731,14 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
"""Allow remote administration of the multiworld server"""
|
"""Allow remote administration of the multiworld server"""
|
||||||
|
|
||||||
output = f"!admin {command}"
|
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))}"
|
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))}"
|
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:
|
if not self.ctx.server_password:
|
||||||
self.output("Sorry, Remote administration is disabled")
|
self.output("Sorry, Remote administration is disabled")
|
||||||
@@ -726,7 +746,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
if not command:
|
if not command:
|
||||||
if self.is_authenticated():
|
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:
|
else:
|
||||||
self.output("Usage: !admin login [password]")
|
self.output("Usage: !admin login [password]")
|
||||||
return True
|
return True
|
||||||
@@ -786,7 +807,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if self.ctx.remaining_mode == "enabled":
|
if self.ctx.remaining_mode == "enabled":
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item")
|
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
|
||||||
for item_id in remaining_item_ids))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
@@ -799,7 +820,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item")
|
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
|
||||||
for item_id in remaining_item_ids))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
@@ -809,7 +830,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
"Sorry, !remaining requires you to have beaten the game on this server")
|
"Sorry, !remaining requires you to have beaten the game on this server")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _cmd_missing(self) -> bool:
|
def _cmd_missing(self) -> bool:
|
||||||
"""List all missing location checks from the server's perspective"""
|
"""List all missing location checks from the server's perspective"""
|
||||||
|
|
||||||
@@ -849,7 +869,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if usable:
|
if usable:
|
||||||
new_item = NetworkItem(Items.item_table[item_name][2], -1, self.client.slot)
|
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)
|
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)
|
send_new_items(self.ctx)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -864,7 +886,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """
|
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """
|
||||||
points_available = get_client_points(self.ctx, self.client)
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
if not item_or_location:
|
if not item_or_location:
|
||||||
self.output(f"A hint costs {self.ctx.hint_cost} points. "
|
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||||
f"You have {points_available} points.")
|
f"You have {points_available} points.")
|
||||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||||
self.ctx.hints[self.client.team, self.client.slot]}
|
self.ctx.hints[self.client.team, self.client.slot]}
|
||||||
@@ -885,7 +907,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
||||||
|
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||||
if hints:
|
if hints:
|
||||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||||
old_hints = set(hints) - new_hints
|
old_hints = set(hints) - new_hints
|
||||||
@@ -899,8 +921,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
if not not_found_hints: # everything's been found, no need to pay
|
if not not_found_hints: # everything's been found, no need to pay
|
||||||
can_pay = 1000
|
can_pay = 1000
|
||||||
elif self.ctx.hint_cost:
|
elif cost:
|
||||||
can_pay = points_available // self.ctx.hint_cost
|
can_pay = points_available // cost
|
||||||
else:
|
else:
|
||||||
can_pay = 1000
|
can_pay = 1000
|
||||||
|
|
||||||
@@ -926,7 +948,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
else:
|
else:
|
||||||
self.output(f"You can't afford the hint. "
|
self.output(f"You can't afford the hint. "
|
||||||
f"You have {points_available} points and need at least "
|
f"You have {points_available} points and need at least "
|
||||||
f"{self.ctx.hint_cost}")
|
f"{self.ctx.get_hint_cost(self.client.slot)}")
|
||||||
notify_hints(self.ctx, self.client.team, hints)
|
notify_hints(self.ctx, self.client.team, hints)
|
||||||
self.ctx.save()
|
self.ctx.save()
|
||||||
return True
|
return True
|
||||||
@@ -941,26 +963,24 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||||
return [location_id for
|
return [location_id for
|
||||||
location_id, slot in ctx.locations if
|
location_id in ctx.locations[client.slot] if
|
||||||
slot == client.slot and
|
|
||||||
location_id in ctx.location_checks[client.team, client.slot]]
|
location_id in ctx.location_checks[client.team, client.slot]]
|
||||||
|
|
||||||
|
|
||||||
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
|
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||||
return [location_id for
|
return [location_id for
|
||||||
location_id, slot in ctx.locations if
|
location_id in ctx.locations[client.slot] if
|
||||||
slot == client.slot and
|
|
||||||
location_id not in ctx.location_checks[client.team, client.slot]]
|
location_id not in ctx.location_checks[client.team, client.slot]]
|
||||||
|
|
||||||
|
|
||||||
def get_client_points(ctx: Context, client: Client) -> int:
|
def get_client_points(ctx: Context, client: Client) -> int:
|
||||||
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
|
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
|
||||||
ctx.hint_cost * ctx.hints_used[client.team, client.slot])
|
ctx.get_hint_cost(client.slot) * ctx.hints_used[client.team, client.slot])
|
||||||
|
|
||||||
|
|
||||||
async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
try:
|
try:
|
||||||
cmd:str = args["cmd"]
|
cmd: str = args["cmd"]
|
||||||
except:
|
except:
|
||||||
logging.exception(f"Could not get command from {args}")
|
logging.exception(f"Could not get command from {args}")
|
||||||
raise
|
raise
|
||||||
@@ -1012,10 +1032,10 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
|
|
||||||
# only exact version match allowed
|
# 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')
|
errors.add('IncompatibleVersion')
|
||||||
if errors:
|
if errors:
|
||||||
logging.info(f"A client connection was refused due to: {errors}")
|
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
||||||
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
||||||
else:
|
else:
|
||||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||||
@@ -1032,7 +1052,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
}]
|
}]
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
|
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
|
|
||||||
await ctx.send_msgs(client, reply)
|
await ctx.send_msgs(client, reply)
|
||||||
@@ -1046,8 +1066,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
client.send_index = len(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": tuplize_received_items(items)}])
|
"items": items}])
|
||||||
|
|
||||||
elif cmd == 'LocationChecks':
|
elif cmd == 'LocationChecks':
|
||||||
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
||||||
@@ -1058,7 +1078,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||||
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
|
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
|
||||||
return
|
return
|
||||||
target_item, target_player = ctx.locations[location, client.slot]
|
target_item, target_player = ctx.locations[client.slot][location]
|
||||||
locs.append(NetworkItem(target_item, location, target_player))
|
locs.append(NetworkItem(target_item, location, target_player))
|
||||||
|
|
||||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||||
@@ -1068,11 +1088,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
|
|
||||||
if cmd == 'Say':
|
if cmd == 'Say':
|
||||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
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
|
return
|
||||||
|
|
||||||
client.messageprocessor(args["text"])
|
client.messageprocessor(args["text"])
|
||||||
|
|
||||||
|
|
||||||
def update_client_status(ctx: Context, client: Client, new_status: ClientStatus):
|
def update_client_status(ctx: Context, client: Client, new_status: ClientStatus):
|
||||||
current = ctx.client_game_state[client.team, client.slot]
|
current = ctx.client_game_state[client.team, client.slot]
|
||||||
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
|
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
|
||||||
@@ -1084,6 +1105,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
|||||||
|
|
||||||
ctx.client_game_state[client.team, client.slot] = new_status
|
ctx.client_game_state[client.team, client.slot] = new_status
|
||||||
|
|
||||||
|
|
||||||
class ServerCommandProcessor(CommonCommandProcessor):
|
class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
def __init__(self, ctx: Context):
|
def __init__(self, ctx: Context):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
@@ -1191,7 +1213,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
for (team, slot), name in self.ctx.player_names.items():
|
for (team, slot), name in self.ctx.player_names.items():
|
||||||
if name.lower() == seeked_player:
|
if name.lower() == seeked_player:
|
||||||
self.ctx.allow_forfeits[(team, slot)] = False
|
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
|
return True
|
||||||
|
|
||||||
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
|
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
|
||||||
@@ -1206,7 +1229,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
if usable:
|
if usable:
|
||||||
for client in self.ctx.endpoints:
|
for client in self.ctx.endpoints:
|
||||||
if client.name == seeked_player:
|
if client.name == seeked_player:
|
||||||
new_item = NetworkItem(lookup_any_item_name_to_id[item], -1, client.slot)
|
new_item = NetworkItem(lookup_any_item_name_to_id[item], -1, 0)
|
||||||
get_received_items(self.ctx, client.team, client.slot).append(new_item)
|
get_received_items(self.ctx, client.team, client.slot).append(new_item)
|
||||||
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
|
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
|
||||||
self.ctx.get_aliased_name(client.team, client.slot))
|
self.ctx.get_aliased_name(client.team, client.slot))
|
||||||
@@ -1271,6 +1294,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
f"{', '.join(known)}")
|
f"{', '.join(known)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def console(ctx: Context):
|
async def console(ctx: Context):
|
||||||
session = prompt_toolkit.PromptSession()
|
session = prompt_toolkit.PromptSession()
|
||||||
while ctx.running:
|
while ctx.running:
|
||||||
@@ -1357,7 +1381,7 @@ async def auto_shutdown(ctx, to_cancel=None):
|
|||||||
|
|
||||||
|
|
||||||
async def main(args: argparse.Namespace):
|
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))
|
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,
|
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||||
|
|||||||
251
Mystery.py
251
Mystery.py
@@ -9,11 +9,12 @@ from collections import Counter
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
from worlds.alttp import Options as LttPOptions
|
||||||
from worlds.generic import PlandoItem, PlandoConnection
|
from worlds.generic import PlandoItem, PlandoConnection
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
from Utils import parse_yaml
|
from Utils import parse_yaml, version_tuple, __version__, tuplize_version
|
||||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||||
from Main import main as ERmain
|
from Main import main as ERmain
|
||||||
from Main import get_seed, seeddigits
|
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 import Bosses
|
||||||
from worlds.alttp.Text import TextTable
|
from worlds.alttp.Text import TextTable
|
||||||
from worlds.alttp.Regions import location_table, key_drop_data
|
from worlds.alttp.Regions import location_table, key_drop_data
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
categories = set(AutoWorldRegister.world_types)
|
||||||
|
|
||||||
def mystery_argparse():
|
def mystery_argparse():
|
||||||
parser = argparse.ArgumentParser(add_help=False)
|
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(",")}
|
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
def get_seed_name(random):
|
def get_seed_name(random):
|
||||||
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
|
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
|
||||||
|
|
||||||
|
|
||||||
def main(args=None, callback=ERmain):
|
def main(args=None, callback=ERmain):
|
||||||
if not args:
|
if not args:
|
||||||
args = mystery_argparse()
|
args = mystery_argparse()
|
||||||
@@ -79,14 +84,14 @@ def main(args=None, callback=ERmain):
|
|||||||
weights_cache = {}
|
weights_cache = {}
|
||||||
if args.weights:
|
if args.weights:
|
||||||
try:
|
try:
|
||||||
weights_cache[args.weights] = get_weights(args.weights)
|
weights_cache[args.weights] = read_weights_yaml(args.weights)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
|
||||||
print(f"Weights: {args.weights} >> "
|
print(f"Weights: {args.weights} >> "
|
||||||
f"{get_choice('description', weights_cache[args.weights], 'No description specified')}")
|
f"{get_choice('description', weights_cache[args.weights], 'No description specified')}")
|
||||||
if args.meta:
|
if args.meta:
|
||||||
try:
|
try:
|
||||||
weights_cache[args.meta] = get_weights(args.meta)
|
weights_cache[args.meta] = read_weights_yaml(args.meta)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
|
||||||
meta_weights = weights_cache[args.meta]
|
meta_weights = weights_cache[args.meta]
|
||||||
@@ -99,7 +104,7 @@ def main(args=None, callback=ERmain):
|
|||||||
if path:
|
if path:
|
||||||
try:
|
try:
|
||||||
if path not in weights_cache:
|
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} >> "
|
print(f"P{player} Weights: {path} >> "
|
||||||
f"{get_choice('description', weights_cache[path], 'No description specified')}")
|
f"{get_choice('description', weights_cache[path], 'No description specified')}")
|
||||||
|
|
||||||
@@ -254,7 +259,7 @@ def main(args=None, callback=ERmain):
|
|||||||
callback(erargs, seed)
|
callback(erargs, seed)
|
||||||
|
|
||||||
|
|
||||||
def get_weights(path):
|
def read_weights_yaml(path):
|
||||||
try:
|
try:
|
||||||
if urllib.parse.urlparse(path).scheme:
|
if urllib.parse.urlparse(path).scheme:
|
||||||
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
|
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
|
||||||
@@ -303,7 +308,10 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
|||||||
name] > 1 else ''),
|
name] > 1 else ''),
|
||||||
player=player,
|
player=player,
|
||||||
PLAYER=(player if player > 1 else '')))
|
PLAYER=(player if player > 1 else '')))
|
||||||
return new_name.strip().replace(' ', '_')[:16]
|
new_name = new_name.strip().replace(' ', '_')[:16]
|
||||||
|
if new_name == "Archipelago":
|
||||||
|
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||||
|
return new_name
|
||||||
|
|
||||||
|
|
||||||
def prefer_int(input_data: str) -> typing.Union[str, int]:
|
def prefer_int(input_data: str) -> typing.Union[str, int]:
|
||||||
@@ -339,19 +347,6 @@ goals = {
|
|||||||
'ice_rod_hunt': 'icerodhunt',
|
'ice_rod_hunt': 'icerodhunt',
|
||||||
}
|
}
|
||||||
|
|
||||||
# remove sometime before 1.0.0, warn before
|
|
||||||
legacy_boss_shuffle_options = {
|
|
||||||
# legacy, will go away:
|
|
||||||
'simple': 'basic',
|
|
||||||
'random': 'full',
|
|
||||||
'normal': 'full'
|
|
||||||
}
|
|
||||||
|
|
||||||
legacy_goals = {
|
|
||||||
'dungeons': 'bosses',
|
|
||||||
'fast_ganon': 'crystals',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
||||||
"""Roll a percentage chance.
|
"""Roll a percentage chance.
|
||||||
@@ -379,13 +374,12 @@ def roll_linked_options(weights: dict) -> dict:
|
|||||||
try:
|
try:
|
||||||
if roll_percentage(option_set["percentage"]):
|
if roll_percentage(option_set["percentage"]):
|
||||||
logging.debug(f"Linked option {option_set['name']} triggered.")
|
logging.debug(f"Linked option {option_set['name']} triggered.")
|
||||||
if "options" in option_set:
|
new_options = option_set["options"]
|
||||||
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
|
for category_name, category_options in new_options.items():
|
||||||
if "rom_options" in option_set:
|
currently_targeted_weights = weights
|
||||||
rom_weights = weights.get("rom", dict())
|
if category_name:
|
||||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
|
currently_targeted_weights = currently_targeted_weights[category_name]
|
||||||
option_set["name"])
|
update_weights(currently_targeted_weights, category_options, "Linked", option_set["name"])
|
||||||
weights["rom"] = rom_weights
|
|
||||||
else:
|
else:
|
||||||
logging.debug(f"linked option {option_set['name']} skipped.")
|
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -399,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.
|
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"]):
|
for i, option_set in enumerate(weights["triggers"]):
|
||||||
try:
|
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)
|
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 '
|
logging.warning(f'Specified option name {option_set["option_name"]} did not '
|
||||||
f'match with a root option. '
|
f'match with a root option. '
|
||||||
f'This is probably in error.')
|
f'This is probably in error.')
|
||||||
trigger_result = get_choice("option_result", option_set)
|
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 result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
||||||
if "options" in option_set:
|
for category_name, category_options in option_set["options"].items():
|
||||||
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
|
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:
|
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
|
f"Please fix your triggers.") from e
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
||||||
if boss_shuffle in legacy_boss_shuffle_options:
|
|
||||||
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
|
||||||
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
|
|
||||||
f"please use {new_boss_shuffle} instead")
|
|
||||||
return new_boss_shuffle
|
|
||||||
if boss_shuffle in boss_shuffle_options:
|
if boss_shuffle in boss_shuffle_options:
|
||||||
return boss_shuffle_options[boss_shuffle]
|
return boss_shuffle_options[boss_shuffle]
|
||||||
elif "bosses" in plando_options:
|
elif "bosses" in plando_options:
|
||||||
@@ -435,10 +426,6 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
|||||||
remainder_shuffle = "none" # vanilla
|
remainder_shuffle = "none" # vanilla
|
||||||
bosses = []
|
bosses = []
|
||||||
for boss in options:
|
for boss in options:
|
||||||
if boss in legacy_boss_shuffle_options:
|
|
||||||
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
|
||||||
logging.warning(f"Boss shuffle {boss} is deprecated, "
|
|
||||||
f"please use {remainder_shuffle} instead")
|
|
||||||
if boss in boss_shuffle_options:
|
if boss in boss_shuffle_options:
|
||||||
remainder_shuffle = boss_shuffle_options[boss]
|
remainder_shuffle = boss_shuffle_options[boss]
|
||||||
elif "-" in boss:
|
elif "-" in boss:
|
||||||
@@ -504,14 +491,34 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
if "triggers" in weights:
|
if "triggers" in weights:
|
||||||
weights = roll_triggers(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 = argparse.Namespace()
|
||||||
ret.name = get_choice('name', weights)
|
ret.name = get_choice('name', weights)
|
||||||
ret.accessibility = get_choice('accessibility', weights)
|
ret.accessibility = get_choice('accessibility', weights)
|
||||||
ret.progression_balancing = get_choice('progression_balancing', weights, True)
|
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()
|
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})
|
items = item_name_groups.get(item_name, {item_name})
|
||||||
for item in items:
|
for item in items:
|
||||||
if item in lookup_any_item_name_to_id:
|
if item in lookup_any_item_name_to_id:
|
||||||
@@ -520,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.")
|
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
|
||||||
|
|
||||||
ret.non_local_items = set()
|
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})
|
items = item_name_groups.get(item_name, {item_name})
|
||||||
for item in items:
|
for item in items:
|
||||||
if item in lookup_any_item_name_to_id:
|
if item in lookup_any_item_name_to_id:
|
||||||
@@ -528,7 +535,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
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 = []
|
startitems = []
|
||||||
for item in inventoryweights.keys():
|
for item in inventoryweights.keys():
|
||||||
itemvalue = get_choice(item, inventoryweights)
|
itemvalue = get_choice(item, inventoryweights)
|
||||||
@@ -538,27 +545,34 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
elif itemvalue:
|
elif itemvalue:
|
||||||
startitems.append(item)
|
startitems.append(item)
|
||||||
ret.startinventory = startitems
|
ret.startinventory = startitems
|
||||||
|
ret.start_hints = set(game_weights.get('start_hints', []))
|
||||||
|
|
||||||
if ret.game == "A Link to the Past":
|
if ret.game in AutoWorldRegister.world_types:
|
||||||
roll_alttp_settings(ret, weights, plando_options)
|
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
|
||||||
elif ret.game == "Hollow Knight":
|
if option_name in game_weights:
|
||||||
for option_name, option in Options.hollow_knight_options.items():
|
try:
|
||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
|
if issubclass(option, Options.OptionDict):
|
||||||
elif ret.game == "Factorio":
|
setattr(ret, option_name, option.from_any(game_weights[option_name]))
|
||||||
for option_name, option in Options.factorio_options.items():
|
else:
|
||||||
if option_name in weights:
|
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
|
||||||
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
|
except Exception as e:
|
||||||
setattr(ret, option_name, option.from_any(weights[option_name]))
|
raise Exception(f"Error generating option {option_name} in {ret.game}")
|
||||||
else:
|
|
||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
|
||||||
else:
|
|
||||||
setattr(ret, option_name, option(option.default))
|
|
||||||
elif ret.game == "Minecraft":
|
|
||||||
for option_name, option in Options.minecraft_options.items():
|
|
||||||
if option_name in weights:
|
|
||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
|
||||||
else:
|
else:
|
||||||
setattr(ret, option_name, option(option.default))
|
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:
|
else:
|
||||||
raise Exception(f"Unsupported game {ret.game}")
|
raise Exception(f"Unsupported game {ret.game}")
|
||||||
return ret
|
return ret
|
||||||
@@ -566,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):
|
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||||
glitches_required = get_choice('glitches_required', weights)
|
glitches_required = get_choice('glitches_required', weights)
|
||||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
|
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
||||||
logging.warning("Only NMG, OWG and No Logic supported")
|
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
||||||
glitches_required = 'none'
|
glitches_required = 'none'
|
||||||
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
|
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
|
||||||
'minor_glitches': 'minorglitches'}[
|
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
|
||||||
glitches_required]
|
glitches_required]
|
||||||
|
|
||||||
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
|
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
|
||||||
@@ -607,23 +621,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
|
|
||||||
goal = get_choice('goals', weights, 'ganon')
|
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]
|
ret.goal = goals[goal]
|
||||||
|
|
||||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||||
# fast ganon + ganon at hole
|
# fast ganon + ganon at hole
|
||||||
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
|
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')
|
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 = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
||||||
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90)
|
|
||||||
|
|
||||||
# sum a percentage to required
|
# sum a percentage to required
|
||||||
if extra_pieces == 'percentage':
|
if extra_pieces == 'percentage':
|
||||||
@@ -631,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))
|
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||||
# vanilla mode (specify how many pieces are)
|
# vanilla mode (specify how many pieces are)
|
||||||
elif extra_pieces == 'available':
|
elif extra_pieces == 'available':
|
||||||
ret.triforce_pieces_available = 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
|
# required pieces + fixed extra
|
||||||
elif extra_pieces == 'extra':
|
elif extra_pieces == 'extra':
|
||||||
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
|
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
|
||||||
@@ -639,11 +646,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
|
|
||||||
# change minimum to required pieces to avoid problems
|
# change minimum to required pieces to avoid problems
|
||||||
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
|
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, '')
|
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
|
||||||
if not ret.shop_shuffle:
|
if not ret.shop_shuffle:
|
||||||
@@ -665,7 +667,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
|
|
||||||
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
||||||
|
|
||||||
|
|
||||||
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||||
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
||||||
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
||||||
@@ -777,49 +778,43 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||||||
get_choice("direction", placement, "both")
|
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_pool = weights.get('sprite_pool', [])
|
||||||
ret.sprite = get_choice('sprite', romweights, "Link")
|
ret.sprite = get_choice('sprite', weights, "Link")
|
||||||
if 'random_sprite_on_event' in romweights:
|
if 'random_sprite_on_event' in weights:
|
||||||
randomoneventweights = romweights['random_sprite_on_event']
|
randomoneventweights = weights['random_sprite_on_event']
|
||||||
if get_choice('enabled', randomoneventweights, False):
|
if get_choice('enabled', randomoneventweights, False):
|
||||||
ret.sprite = 'randomon'
|
ret.sprite = 'randomon'
|
||||||
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
||||||
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) 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 += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
|
||||||
ret.sprite += '-slash' if get_choice('on_slash', 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 += '-item' if get_choice('on_item', randomoneventweights, False) else ''
|
||||||
ret.sprite += '-bonk' if get_choice('on_bonk', 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 = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
|
||||||
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' 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)) \
|
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.
|
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
|
||||||
for key, value in romweights['sprite'].items():
|
for key, value in weights['sprite'].items():
|
||||||
if key.startswith('random'):
|
if key.startswith('random'):
|
||||||
ret.sprite_pool += ['random'] * int(value)
|
ret.sprite_pool += ['random'] * int(value)
|
||||||
else:
|
else:
|
||||||
ret.sprite_pool += [key] * int(value)
|
ret.sprite_pool += [key] * int(value)
|
||||||
|
|
||||||
ret.disablemusic = get_choice('disablemusic', romweights, False)
|
ret.disablemusic = get_choice('disablemusic', weights, False)
|
||||||
ret.triforcehud = get_choice('triforcehud', romweights, 'hide_goal')
|
ret.triforcehud = get_choice('triforcehud', weights, 'hide_goal')
|
||||||
ret.quickswap = get_choice('quickswap', romweights, True)
|
ret.quickswap = get_choice('quickswap', weights, True)
|
||||||
ret.fastmenu = get_choice('menuspeed', romweights, "normal")
|
ret.fastmenu = get_choice('menuspeed', weights, "normal")
|
||||||
ret.reduceflashing = get_choice('reduceflashing', romweights, False)
|
ret.reduceflashing = get_choice('reduceflashing', weights, False)
|
||||||
ret.heartcolor = get_choice('heartcolor', romweights, "red")
|
ret.heartcolor = get_choice('heartcolor', weights, "red")
|
||||||
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights, "normal"))
|
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', weights, "normal"))
|
||||||
ret.ow_palettes = get_choice('ow_palettes', romweights, "default")
|
ret.ow_palettes = get_choice('ow_palettes', weights, "default")
|
||||||
ret.uw_palettes = get_choice('uw_palettes', romweights, "default")
|
ret.uw_palettes = get_choice('uw_palettes', weights, "default")
|
||||||
ret.hud_palettes = get_choice('hud_palettes', romweights, "default")
|
ret.hud_palettes = get_choice('hud_palettes', weights, "default")
|
||||||
ret.sword_palettes = get_choice('sword_palettes', romweights, "default")
|
ret.sword_palettes = get_choice('sword_palettes', weights, "default")
|
||||||
ret.shield_palettes = get_choice('shield_palettes', romweights, "default")
|
ret.shield_palettes = get_choice('shield_palettes', weights, "default")
|
||||||
ret.link_palettes = get_choice('link_palettes', romweights, "default")
|
ret.link_palettes = get_choice('link_palettes', weights, "default")
|
||||||
|
|
||||||
else:
|
|
||||||
ret.quickswap = True
|
|
||||||
ret.sprite = "Link"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -307,4 +307,10 @@ class Hint(typing.NamedTuple):
|
|||||||
else:
|
else:
|
||||||
add_json_text(parts, ".")
|
add_json_text(parts, ".")
|
||||||
|
|
||||||
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
|
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
||||||
|
"receiving": self.receiving_player,
|
||||||
|
"item": NetworkItem(self.item, self.location, self.finding_player)}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def local(self):
|
||||||
|
return self.receiving_player == self.finding_player
|
||||||
|
|||||||
263
Options.py
263
Options.py
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import typing
|
import typing
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
class AssembleOptions(type):
|
class AssembleOptions(type):
|
||||||
@@ -7,8 +8,9 @@ class AssembleOptions(type):
|
|||||||
options = attrs["options"] = {}
|
options = attrs["options"] = {}
|
||||||
name_lookup = attrs["name_lookup"] = {}
|
name_lookup = attrs["name_lookup"] = {}
|
||||||
for base in bases:
|
for base in bases:
|
||||||
options.update(base.options)
|
if hasattr(base, "options"):
|
||||||
name_lookup.update(name_lookup)
|
options.update(base.options)
|
||||||
|
name_lookup.update(name_lookup)
|
||||||
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
||||||
name.startswith("option_")}
|
name.startswith("option_")}
|
||||||
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
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_")})
|
name.startswith("alias_")})
|
||||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
class Option(metaclass=AssembleOptions):
|
class Option(metaclass=AssembleOptions):
|
||||||
value: int
|
value: int
|
||||||
name_lookup: typing.Dict[int, str]
|
name_lookup: typing.Dict[int, str]
|
||||||
@@ -88,6 +89,8 @@ class Toggle(Option):
|
|||||||
def get_option_name(self):
|
def get_option_name(self):
|
||||||
return bool(self.value)
|
return bool(self.value)
|
||||||
|
|
||||||
|
class DefaultOnToggle(Toggle):
|
||||||
|
default = 1
|
||||||
|
|
||||||
class Choice(Option):
|
class Choice(Option):
|
||||||
def __init__(self, value: int):
|
def __init__(self, value: int):
|
||||||
@@ -109,6 +112,44 @@ class Choice(Option):
|
|||||||
return cls.from_text(str(data))
|
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):
|
class OptionNameSet(Option):
|
||||||
default = frozenset()
|
default = frozenset()
|
||||||
|
|
||||||
@@ -139,226 +180,28 @@ class OptionDict(Option):
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||||
|
|
||||||
|
def get_option_name(self):
|
||||||
class Logic(Choice):
|
return str(self.value)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice):
|
|
||||||
option_kill_ganon = 0
|
|
||||||
option_kill_ganon_and_gt_agahnim = 1
|
|
||||||
option_hand_in = 2
|
|
||||||
|
|
||||||
|
|
||||||
class Accessibility(Choice):
|
class Accessibility(Choice):
|
||||||
option_locations = 0
|
option_locations = 0
|
||||||
option_items = 1
|
option_items = 1
|
||||||
option_beatable = 2
|
option_beatable = 2
|
||||||
|
|
||||||
|
|
||||||
class Crystals(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__":
|
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 = argparse.Namespace()
|
||||||
test.logic = Logic.from_text("no_logic")
|
test.logic = Logic.from_text("no_logic")
|
||||||
test.mapshuffle = mapshuffle.from_text("ON")
|
test.mapshuffle = mapshuffle.from_text("ON")
|
||||||
|
|||||||
15
Patch.py
15
Patch.py
@@ -12,7 +12,7 @@ from typing import Tuple, Optional
|
|||||||
import Utils
|
import Utils
|
||||||
from worlds.alttp.Rom import JAP10HASH
|
from worlds.alttp.Rom import JAP10HASH
|
||||||
|
|
||||||
current_patch_version = 1
|
current_patch_version = 2
|
||||||
|
|
||||||
|
|
||||||
def get_base_rom_path(file_name: str = "") -> str:
|
def get_base_rom_path(file_name: str = "") -> str:
|
||||||
@@ -43,9 +43,9 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||||
patch = yaml.dump({"meta": metadata,
|
patch = yaml.dump({"meta": metadata,
|
||||||
"patch": patch,
|
"patch": patch,
|
||||||
"game": "alttp",
|
"game": "A Link to the Past",
|
||||||
"compatible_version": 1,
|
|
||||||
# minimum version of patch system expected for patching to be successful
|
# minimum version of patch system expected for patching to be successful
|
||||||
|
"compatible_version": 1,
|
||||||
"version": current_patch_version,
|
"version": current_patch_version,
|
||||||
"base_checksum": JAP10HASH})
|
"base_checksum": JAP10HASH})
|
||||||
return patch.encode(encoding="utf-8-sig")
|
return patch.encode(encoding="utf-8-sig")
|
||||||
@@ -58,10 +58,13 @@ def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
|
|||||||
return generate_yaml(patch, metadata)
|
return generate_yaml(patch, metadata)
|
||||||
|
|
||||||
|
|
||||||
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None) -> str:
|
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None,
|
||||||
|
player: int = 0, player_name: str = "") -> str:
|
||||||
|
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
|
||||||
|
"player_id": player,
|
||||||
|
"player_name": player_name}
|
||||||
bytes = generate_patch(load_bytes(rom_file_to_patch),
|
bytes = generate_patch(load_bytes(rom_file_to_patch),
|
||||||
{
|
meta)
|
||||||
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise
|
|
||||||
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
|
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
|
||||||
write_lzma(bytes, target)
|
write_lzma(bytes, target)
|
||||||
return target
|
return target
|
||||||
|
|||||||
175
Utils.py
175
Utils.py
@@ -12,8 +12,9 @@ class Version(typing.NamedTuple):
|
|||||||
minor: int
|
minor: int
|
||||||
build: int
|
build: int
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
_version_tuple = tuplize_version(__version__)
|
__version__ = "0.1.4"
|
||||||
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
import os
|
import os
|
||||||
@@ -22,6 +23,7 @@ import sys
|
|||||||
import pickle
|
import pickle
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
|
import collections
|
||||||
|
|
||||||
from yaml import load, dump, safe_load
|
from yaml import load, dump, safe_load
|
||||||
|
|
||||||
@@ -52,7 +54,6 @@ def snes_to_pc(value):
|
|||||||
def parse_player_names(names, players, teams):
|
def parse_player_names(names, players, teams):
|
||||||
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
||||||
if len(names) != len(set(names)):
|
if len(names) != len(set(names)):
|
||||||
import collections
|
|
||||||
name_counter = collections.Counter(names)
|
name_counter = collections.Counter(names)
|
||||||
raise ValueError(f"Duplicate Player names is not supported, "
|
raise ValueError(f"Duplicate Player names is not supported, "
|
||||||
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
||||||
@@ -68,6 +69,21 @@ def parse_player_names(names, players, teams):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def cache_argsless(function):
|
||||||
|
if function.__code__.co_argcount:
|
||||||
|
raise Exception("Can only cache 0 argument functions with this cache.")
|
||||||
|
|
||||||
|
result = sentinel = object()
|
||||||
|
|
||||||
|
def _wrap():
|
||||||
|
nonlocal result
|
||||||
|
if result is sentinel:
|
||||||
|
result = function()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return _wrap
|
||||||
|
|
||||||
|
|
||||||
def is_bundled() -> bool:
|
def is_bundled() -> bool:
|
||||||
return getattr(sys, 'frozen', False)
|
return getattr(sys, 'frozen', False)
|
||||||
|
|
||||||
@@ -118,20 +134,10 @@ def open_file(filename):
|
|||||||
subprocess.call([open_command, filename])
|
subprocess.call([open_command, filename])
|
||||||
|
|
||||||
|
|
||||||
def close_console():
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
# windows
|
|
||||||
import ctypes.wintypes
|
|
||||||
try:
|
|
||||||
ctypes.windll.kernel32.FreeConsole()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
parse_yaml = safe_load
|
parse_yaml = safe_load
|
||||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_public_ipv4() -> str:
|
def get_public_ipv4() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -147,7 +153,7 @@ def get_public_ipv4() -> str:
|
|||||||
pass # we could be offline, in a local game, so no point in erroring out
|
pass # we could be offline, in a local game, so no point in erroring out
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_public_ipv6() -> str:
|
def get_public_ipv6() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -160,70 +166,68 @@ def get_public_ipv6() -> str:
|
|||||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_default_options() -> dict:
|
def get_default_options() -> dict:
|
||||||
if not hasattr(get_default_options, "options"):
|
# Refer to host.yaml for comments as to what all these options mean.
|
||||||
# Refer to host.yaml for comments as to what all these options mean.
|
options = {
|
||||||
options = {
|
"general_options": {
|
||||||
"general_options": {
|
"output_path": "output",
|
||||||
"output_path": "output",
|
},
|
||||||
},
|
"factorio_options": {
|
||||||
"factorio_options": {
|
"executable": "factorio\\bin\\x64\\factorio",
|
||||||
"executable": "factorio\\bin\\x64\\factorio",
|
},
|
||||||
},
|
"lttp_options": {
|
||||||
"lttp_options": {
|
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
"sni": "SNI",
|
||||||
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
|
"rom_start": True,
|
||||||
"rom_start": True,
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"server_options": {
|
"server_options": {
|
||||||
"host": None,
|
"host": None,
|
||||||
"port": 38281,
|
"port": 38281,
|
||||||
"password": None,
|
"password": None,
|
||||||
"multidata": None,
|
"multidata": None,
|
||||||
"savefile": None,
|
"savefile": None,
|
||||||
"disable_save": False,
|
"disable_save": False,
|
||||||
"loglevel": "info",
|
"loglevel": "info",
|
||||||
"server_password": None,
|
"server_password": None,
|
||||||
"disable_item_cheat": False,
|
"disable_item_cheat": False,
|
||||||
"location_check_points": 1,
|
"location_check_points": 1,
|
||||||
"hint_cost": 1000,
|
"hint_cost": 10,
|
||||||
"forfeit_mode": "goal",
|
"forfeit_mode": "goal",
|
||||||
"remaining_mode": "goal",
|
"remaining_mode": "goal",
|
||||||
"auto_shutdown": 0,
|
"auto_shutdown": 0,
|
||||||
"compatibility": 2,
|
"compatibility": 2,
|
||||||
"log_network": 0
|
"log_network": 0
|
||||||
},
|
},
|
||||||
"multi_mystery_options": {
|
"multi_mystery_options": {
|
||||||
"teams": 1,
|
"teams": 1,
|
||||||
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||||
"player_files_path": "Players",
|
"player_files_path": "Players",
|
||||||
"players": 0,
|
"players": 0,
|
||||||
"weights_file_path": "weights.yaml",
|
"weights_file_path": "weights.yaml",
|
||||||
"meta_file_path": "meta.yaml",
|
"meta_file_path": "meta.yaml",
|
||||||
"pre_roll": False,
|
"pre_roll": False,
|
||||||
"player_name": "",
|
"create_spoiler": 1,
|
||||||
"create_spoiler": 1,
|
"zip_roms": 0,
|
||||||
"zip_roms": 0,
|
"zip_diffs": 2,
|
||||||
"zip_diffs": 2,
|
"zip_apmcs": 1,
|
||||||
"zip_spoiler": 0,
|
"zip_spoiler": 0,
|
||||||
"zip_multidata": 1,
|
"zip_multidata": 1,
|
||||||
"zip_format": 1,
|
"zip_format": 1,
|
||||||
"glitch_triforce_room": 1,
|
"glitch_triforce_room": 1,
|
||||||
"race": 0,
|
"race": 0,
|
||||||
"cpu_threads": 0,
|
"cpu_threads": 0,
|
||||||
"max_attempts": 0,
|
"max_attempts": 0,
|
||||||
"take_first_working": False,
|
"take_first_working": False,
|
||||||
"keep_all_seeds": False,
|
"keep_all_seeds": False,
|
||||||
"log_output_path": "Output Logs",
|
"log_output_path": "Output Logs",
|
||||||
"log_level": None,
|
"log_level": None,
|
||||||
"plando_options": "bosses",
|
"plando_options": "bosses",
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get_default_options.options = options
|
return options
|
||||||
return get_default_options.options
|
|
||||||
|
|
||||||
|
|
||||||
blacklisted_options = {"multi_mystery_options.cpu_threads",
|
blacklisted_options = {"multi_mystery_options.cpu_threads",
|
||||||
@@ -253,7 +257,7 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
|||||||
dest[key] = update_options(value, dest[key], filename, new_keys)
|
dest[key] = update_options(value, dest[key], filename, new_keys)
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_options() -> dict:
|
def get_options() -> dict:
|
||||||
if not hasattr(get_options, "options"):
|
if not hasattr(get_options, "options"):
|
||||||
locations = ("options.yaml", "host.yaml",
|
locations = ("options.yaml", "host.yaml",
|
||||||
@@ -345,12 +349,12 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
|||||||
f"Enter yes, no or never: ")
|
f"Enter yes, no or never: ")
|
||||||
if adjust_wanted and adjust_wanted.startswith("y"):
|
if adjust_wanted and adjust_wanted.startswith("y"):
|
||||||
if hasattr(adjuster_settings, "sprite_pool"):
|
if hasattr(adjuster_settings, "sprite_pool"):
|
||||||
from Adjuster import AdjusterWorld
|
from LttPAdjuster import AdjusterWorld
|
||||||
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
|
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
|
||||||
|
|
||||||
adjusted = True
|
adjusted = True
|
||||||
import Adjuster
|
import LttPAdjuster
|
||||||
_, romfile = Adjuster.adjust(adjuster_settings)
|
_, romfile = LttPAdjuster.adjust(adjuster_settings)
|
||||||
|
|
||||||
if hasattr(adjuster_settings, "world"):
|
if hasattr(adjuster_settings, "world"):
|
||||||
delattr(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, adjusted
|
||||||
return romfile, False
|
return romfile, False
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
def get_unique_identifier():
|
def get_unique_identifier():
|
||||||
uuid = persistent_load().get("client", {}).get("uuid", None)
|
uuid = persistent_load().get("client", {}).get("uuid", None)
|
||||||
if uuid:
|
if uuid:
|
||||||
@@ -392,6 +396,11 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
||||||
import NetUtils
|
import NetUtils
|
||||||
return getattr(NetUtils, name)
|
return getattr(NetUtils, name)
|
||||||
|
if module == "Options":
|
||||||
|
import Options
|
||||||
|
obj = getattr(Options, name)
|
||||||
|
if issubclass(obj, Options.Option):
|
||||||
|
return obj
|
||||||
# Forbid everything else.
|
# Forbid everything else.
|
||||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||||
(module, name))
|
(module, name))
|
||||||
@@ -400,3 +409,9 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
def restricted_loads(s):
|
def restricted_loads(s):
|
||||||
"""Helper function analogous to pickle.loads()."""
|
"""Helper function analogous to pickle.loads()."""
|
||||||
return RestrictedUnpickler(io.BytesIO(s)).load()
|
return RestrictedUnpickler(io.BytesIO(s)).load()
|
||||||
|
|
||||||
|
|
||||||
|
class KeyedDefaultDict(collections.defaultdict):
|
||||||
|
def __missing__(self, key):
|
||||||
|
self[key] = value = self.default_factory(key)
|
||||||
|
return value
|
||||||
10
WebHost.py
10
WebHost.py
@@ -2,22 +2,24 @@ import os
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import ModuleUpdate
|
||||||
|
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
from WebHostLib import app as raw_app
|
from WebHostLib import app as raw_app
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
from WebHostLib.models import db
|
from WebHostLib.models import db
|
||||||
from WebHostLib.autolauncher import autohost
|
from WebHostLib.autolauncher import autohost
|
||||||
|
|
||||||
configpath = "config.yaml"
|
configpath = os.path.abspath("config.yaml")
|
||||||
|
|
||||||
|
|
||||||
def get_app():
|
def get_app():
|
||||||
app = raw_app
|
app = raw_app
|
||||||
if os.path.exists(configpath):
|
if os.path.exists(configpath):
|
||||||
import yaml
|
import yaml
|
||||||
with open(configpath) as c:
|
app.config.from_file(configpath, yaml.safe_load)
|
||||||
app.config.update(yaml.safe_load(c))
|
|
||||||
|
|
||||||
logging.info(f"Updated config from {configpath}")
|
logging.info(f"Updated config from {configpath}")
|
||||||
db.bind(**app.config["PONY"])
|
db.bind(**app.config["PONY"])
|
||||||
db.generate_mapping(create_tables=True)
|
db.generate_mapping(create_tables=True)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import uuid
|
|||||||
import base64
|
import base64
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
import jinja2.exceptions
|
||||||
from pony.flask import Pony
|
from pony.flask import Pony
|
||||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flaskext.autoversion import Autoversion
|
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
@@ -48,9 +48,6 @@ app.config["CACHE_TYPE"] = "simple"
|
|||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
app.config["PATCH_TARGET"] = "archipelago.gg"
|
app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||||
|
|
||||||
app.autoversion = True
|
|
||||||
|
|
||||||
av = Autoversion(app)
|
|
||||||
cache = Cache(app)
|
cache = Cache(app)
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|
||||||
@@ -78,6 +75,51 @@ def register_session():
|
|||||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
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>')
|
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||||
def tutorial(game, file, lang):
|
def tutorial(game, file, lang):
|
||||||
return render_template("tutorial.html", game=game, file=file, lang=lang)
|
return render_template("tutorial.html", game=game, file=file, lang=lang)
|
||||||
@@ -88,13 +130,8 @@ def tutorial_landing():
|
|||||||
return render_template("tutorialLanding.html")
|
return render_template("tutorialLanding.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/player-settings')
|
|
||||||
def player_settings_simple():
|
|
||||||
return render_template("playerSettings.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/weighted-settings')
|
@app.route('/weighted-settings')
|
||||||
def player_settings():
|
def weighted_settings():
|
||||||
return render_template("weightedSettings.html")
|
return render_template("weightedSettings.html")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import json
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from . import api_endpoints
|
from . import api_endpoints
|
||||||
@@ -46,7 +48,7 @@ def generate_api():
|
|||||||
gen = Generation(
|
gen = Generation(
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
# convert to json compatible
|
# convert to json compatible
|
||||||
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
|
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||||
owner=session["_id"])
|
owner=session["_id"])
|
||||||
commit()
|
commit()
|
||||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
return {"text": f"Generation of seed {gen.id} started successfully.",
|
||||||
@@ -58,6 +60,7 @@ def generate_api():
|
|||||||
return {"text": "Uncaught Exception:" + str(e)}, 500
|
return {"text": "Uncaught Exception:" + str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/status/<suuid:seed>')
|
@api_endpoints.route('/status/<suuid:seed>')
|
||||||
def wait_seed_api(seed: UUID):
|
def wait_seed_api(seed: UUID):
|
||||||
seed_id = seed
|
seed_id = seed
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
from pony.orm import db_session, select, commit
|
from pony.orm import db_session, select, commit
|
||||||
|
|
||||||
|
from Utils import restricted_loads
|
||||||
|
|
||||||
|
|
||||||
class CommonLocker():
|
class CommonLocker():
|
||||||
"""Uses a file lock to signal that something is already running"""
|
"""Uses a file lock to signal that something is already running"""
|
||||||
|
lock_folder = "file_locks"
|
||||||
def __init__(self, lockname: str):
|
def __init__(self, lockname: str, folder=None):
|
||||||
|
if folder:
|
||||||
|
self.lock_folder = folder
|
||||||
|
os.makedirs(self.lock_folder, exist_ok=True)
|
||||||
self.lockname = lockname
|
self.lockname = lockname
|
||||||
self.lockfile = f"./{self.lockname}.lck"
|
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRunningException(Exception):
|
class AlreadyRunningException(Exception):
|
||||||
@@ -23,8 +30,6 @@ class AlreadyRunningException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import os
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
class Locker(CommonLocker):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
@@ -43,6 +48,7 @@ if sys.platform == 'win32':
|
|||||||
else: # unix
|
else: # unix
|
||||||
import fcntl
|
import fcntl
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
class Locker(CommonLocker):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
@@ -78,14 +84,21 @@ def handle_generation_failure(result: BaseException):
|
|||||||
|
|
||||||
|
|
||||||
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||||
options = generation.options
|
try:
|
||||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
meta = json.loads(generation.meta)
|
||||||
|
options = restricted_loads(generation.options)
|
||||||
meta = generation.meta
|
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||||
pool.apply_async(gen_game, (options,),
|
pool.apply_async(gen_game, (options,),
|
||||||
{"race": meta["race"], "sid": generation.id, "owner": generation.owner},
|
{"race": meta["race"],
|
||||||
handle_generation_success, handle_generation_failure)
|
"sid": generation.id,
|
||||||
generation.state = STATE_STARTED
|
"owner": generation.owner},
|
||||||
|
handle_generation_success, handle_generation_failure)
|
||||||
|
except:
|
||||||
|
generation.state = STATE_ERROR
|
||||||
|
commit()
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
generation.state = STATE_STARTED
|
||||||
|
|
||||||
|
|
||||||
def init_db(pony_config: dict):
|
def init_db(pony_config: dict):
|
||||||
@@ -138,6 +151,7 @@ multiworlds = {}
|
|||||||
|
|
||||||
guardians = concurrent.futures.ThreadPoolExecutor(2, thread_name_prefix="Guardian")
|
guardians = concurrent.futures.ThreadPoolExecutor(2, thread_name_prefix="Guardian")
|
||||||
|
|
||||||
|
|
||||||
class MultiworldInstance():
|
class MultiworldInstance():
|
||||||
def __init__(self, room: Room, config: dict):
|
def __init__(self, room: Room, config: dict):
|
||||||
self.room_id = room.id
|
self.room_id = room.id
|
||||||
@@ -162,7 +176,7 @@ class MultiworldInstance():
|
|||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
def _collect(self):
|
def _collect(self):
|
||||||
self.process.join() # wait for process to finish
|
self.process.join() # wait for process to finish
|
||||||
self.process = None
|
self.process = None
|
||||||
self.guardian = None
|
self.guardian = None
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class WebHostContext(Context):
|
|||||||
def get_random_port():
|
def get_random_port():
|
||||||
return random.randint(49152, 65535)
|
return random.randint(49152, 65535)
|
||||||
|
|
||||||
|
|
||||||
def run_server_process(room_id, ponyconfig: dict):
|
def run_server_process(room_id, ponyconfig: dict):
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
@@ -144,7 +145,9 @@ def run_server_process(room_id, ponyconfig: dict):
|
|||||||
await ctx.shutdown_task
|
await ctx.shutdown_task
|
||||||
logging.info("Shutting down")
|
logging.info("Shutting down")
|
||||||
|
|
||||||
asyncio.run(main())
|
from .autolauncher import Locker
|
||||||
|
with Locker(room_id):
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib import LOGS_FOLDER
|
from WebHostLib import LOGS_FOLDER
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ from flask import send_file, Response
|
|||||||
from pony.orm import select
|
from pony.orm import select
|
||||||
|
|
||||||
from Patch import update_patch_data
|
from Patch import update_patch_data
|
||||||
from WebHostLib import app, Patch, Room, Seed
|
from WebHostLib import app, Slot, Room, Seed
|
||||||
|
import zipfile
|
||||||
|
|
||||||
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
||||||
def download_patch(room_id, patch_id):
|
def download_patch(room_id, patch_id):
|
||||||
patch = Patch.get(id=patch_id)
|
patch = Slot.get(id=patch_id)
|
||||||
if not patch:
|
if not patch:
|
||||||
return "Patch not found"
|
return "Patch not found"
|
||||||
else:
|
else:
|
||||||
@@ -30,8 +30,9 @@ def download_spoiler(seed_id):
|
|||||||
|
|
||||||
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
|
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
|
||||||
def download_raw_patch(seed_id, player_id: int):
|
def download_raw_patch(seed_id, player_id: int):
|
||||||
patch = select(patch for patch in Patch if
|
seed = Seed.get(id=seed_id)
|
||||||
patch.player_id == player_id and patch.seed.id == seed_id).first()
|
patch = select(patch for patch in seed.slots if
|
||||||
|
patch.player_id == player_id).first()
|
||||||
|
|
||||||
if not patch:
|
if not patch:
|
||||||
return "Patch not found"
|
return "Patch not found"
|
||||||
@@ -43,3 +44,25 @@ def download_raw_patch(seed_id, player_id: int):
|
|||||||
|
|
||||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp"
|
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp"
|
||||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
||||||
|
|
||||||
|
@app.route("/slot_file/<suuid:seed_id>/<int:player_id>")
|
||||||
|
def download_slot_file(seed_id, player_id: int):
|
||||||
|
seed = Seed.get(id=seed_id)
|
||||||
|
slot_data: Slot = select(patch for patch in seed.slots if
|
||||||
|
patch.player_id == player_id).first()
|
||||||
|
|
||||||
|
if not slot_data:
|
||||||
|
return "Slot Data not found"
|
||||||
|
else:
|
||||||
|
import io
|
||||||
|
|
||||||
|
if slot_data.game == "Minecraft":
|
||||||
|
fname = f"AP_{app.jinja_env.filters['suuid'](seed_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||||
|
elif slot_data.game == "Factorio":
|
||||||
|
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||||
|
for name in zf.namelist():
|
||||||
|
if name.endswith("info.json"):
|
||||||
|
fname = name.rsplit("/", 1)[0]+".zip"
|
||||||
|
else:
|
||||||
|
return "Game download not supported."
|
||||||
|
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import random
|
import random
|
||||||
|
import json
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
@@ -39,7 +40,7 @@ def generate(race=False):
|
|||||||
gen = Generation(
|
gen = Generation(
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
# convert to json compatible
|
# convert to json compatible
|
||||||
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
|
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||||
owner=session["_id"])
|
owner=session["_id"])
|
||||||
commit()
|
commit()
|
||||||
|
|
||||||
@@ -79,7 +80,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||||||
erargs.outputname = seedname
|
erargs.outputname = seedname
|
||||||
erargs.outputpath = target.name
|
erargs.outputpath = target.name
|
||||||
erargs.teams = 1
|
erargs.teams = 1
|
||||||
erargs.progression_balancing = {}
|
|
||||||
erargs.create_diff = True
|
erargs.create_diff = True
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
@@ -94,10 +94,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||||||
|
|
||||||
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
|
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
|
||||||
del (erargs.name)
|
del (erargs.name)
|
||||||
|
|
||||||
erargs.skip_progression_balancing = {player: not balanced for player, balanced in
|
|
||||||
erargs.progression_balancing.items()}
|
|
||||||
del (erargs.progression_balancing)
|
|
||||||
ERmain(erargs, seed)
|
ERmain(erargs, seed)
|
||||||
|
|
||||||
return upload_to_db(target.name, owner, sid, race)
|
return upload_to_db(target.name, owner, sid, race)
|
||||||
@@ -107,7 +103,11 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||||||
gen = Generation.get(id=sid)
|
gen = Generation.get(id=sid)
|
||||||
if gen is not None:
|
if gen is not None:
|
||||||
gen.state = STATE_ERROR
|
gen.state = STATE_ERROR
|
||||||
gen.meta = (e.__class__.__name__ + ": "+ str(e)).encode()
|
meta = json.loads(gen.meta)
|
||||||
|
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
|
||||||
|
gen.meta = json.dumps(meta)
|
||||||
|
|
||||||
|
commit()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -122,12 +122,12 @@ def wait_seed(seed: UUID):
|
|||||||
if not generation:
|
if not generation:
|
||||||
return "Generation not found."
|
return "Generation not found."
|
||||||
elif generation.state == STATE_ERROR:
|
elif generation.state == STATE_ERROR:
|
||||||
return render_template("seedError.html", seed_error=generation.meta.decode())
|
return render_template("seedError.html", seed_error=generation.meta)
|
||||||
return render_template("waitSeed.html", seed_id=seed_id)
|
return render_template("waitSeed.html", seed_id=seed_id)
|
||||||
|
|
||||||
|
|
||||||
def upload_to_db(folder, owner, sid, race:bool):
|
def upload_to_db(folder, owner, sid, race:bool):
|
||||||
patches = set()
|
slots = set()
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
|
|
||||||
multidata = None
|
multidata = None
|
||||||
@@ -137,8 +137,8 @@ def upload_to_db(folder, owner, sid, race:bool):
|
|||||||
player_text = file.split("_P", 1)[1]
|
player_text = file.split("_P", 1)[1]
|
||||||
player_name = player_text.split("_", 1)[1].split(".", 1)[0]
|
player_name = player_text.split("_", 1)[1].split(".", 1)[0]
|
||||||
player_id = int(player_text.split(".", 1)[0].split("_", 1)[0])
|
player_id = int(player_text.split(".", 1)[0].split("_", 1)[0])
|
||||||
patches.add(Patch(data=open(file, "rb").read(),
|
slots.add(Slot(data=open(file, "rb").read(),
|
||||||
player_id=player_id, player_name = player_name))
|
player_id=player_id, player_name = player_name, game = "A Link to the Past"))
|
||||||
elif file.endswith(".txt"):
|
elif file.endswith(".txt"):
|
||||||
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
||||||
elif file.endswith(".archipelago"):
|
elif file.endswith(".archipelago"):
|
||||||
@@ -146,12 +146,12 @@ def upload_to_db(folder, owner, sid, race:bool):
|
|||||||
if multidata:
|
if multidata:
|
||||||
with db_session:
|
with db_session:
|
||||||
if sid:
|
if sid:
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner,
|
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
|
||||||
id=sid, meta={"tags": ["generated"]})
|
id=sid, meta=json.dumps({"race": race, "tags": ["generated"]}))
|
||||||
else:
|
else:
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner,
|
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
|
||||||
meta={"tags": ["generated"]})
|
meta=json.dumps({"race": race, "tags": ["generated"]}))
|
||||||
for patch in patches:
|
for patch in slots:
|
||||||
patch.seed = seed
|
patch.seed = seed
|
||||||
if sid:
|
if sid:
|
||||||
gen = Generation.get(id=sid)
|
gen = Generation.get(id=sid)
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ STATE_STARTED = 1
|
|||||||
STATE_ERROR = -1
|
STATE_ERROR = -1
|
||||||
|
|
||||||
|
|
||||||
class Patch(db.Entity):
|
class Slot(db.Entity):
|
||||||
id = PrimaryKey(int, auto=True)
|
id = PrimaryKey(int, auto=True)
|
||||||
player_id = Required(int)
|
player_id = Required(int)
|
||||||
player_name = Required(str, 16)
|
player_name = Required(str, 16)
|
||||||
data = Required(bytes, lazy=True)
|
data = Optional(bytes, lazy=True)
|
||||||
seed = Optional('Seed')
|
seed = Optional('Seed')
|
||||||
|
game = Required(str)
|
||||||
|
|
||||||
|
|
||||||
class Room(db.Entity):
|
class Room(db.Entity):
|
||||||
@@ -37,9 +38,9 @@ class Seed(db.Entity):
|
|||||||
multidata = Required(bytes, lazy=True)
|
multidata = Required(bytes, lazy=True)
|
||||||
owner = Required(UUID, index=True)
|
owner = Required(UUID, index=True)
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||||
patches = Set(Patch)
|
slots = Set(Slot)
|
||||||
spoiler = Optional(LongStr, lazy=True)
|
spoiler = Optional(LongStr, lazy=True)
|
||||||
meta = Required(Json, lazy=True, default=lambda: {}) # additional meta information/tags
|
meta = Required(str, default=lambda: "{\"race\": false}") # additional meta information/tags
|
||||||
|
|
||||||
|
|
||||||
class Command(db.Entity):
|
class Command(db.Entity):
|
||||||
@@ -51,6 +52,6 @@ class Command(db.Entity):
|
|||||||
class Generation(db.Entity):
|
class Generation(db.Entity):
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
owner = Required(UUID)
|
owner = Required(UUID)
|
||||||
options = Required(Json, lazy=True)
|
options = Required(buffer, lazy=True)
|
||||||
meta = Required(Json, lazy=True)
|
meta = Required(str, default=lambda: "{\"race\": false}")
|
||||||
state = Required(int, default=0, index=True)
|
state = Required(int, default=0, index=True)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
flask>=1.1.2
|
flask>=2.0.1
|
||||||
pony>=0.7.14
|
pony>=0.7.14
|
||||||
waitress>=2.0.0
|
waitress>=2.0.0
|
||||||
flask-caching>=1.10.1
|
flask-caching>=1.10.1
|
||||||
Flask-Autoversion>=0.2.0
|
Flask-Compress>=1.10.1
|
||||||
Flask-Compress>=1.9.0
|
|
||||||
Flask-Limiter>=1.4
|
Flask-Limiter>=1.4
|
||||||
|
|||||||
@@ -17,6 +17,47 @@ window.addEventListener('load', () => {
|
|||||||
paging: false,
|
paging: false,
|
||||||
info: false,
|
info: false,
|
||||||
dom: "t",
|
dom: "t",
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: 'hours',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
if (type === "sort" || type === 'type') {
|
||||||
|
if (data === "None")
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return parseInt(data);
|
||||||
|
}
|
||||||
|
if (data === "None")
|
||||||
|
return data;
|
||||||
|
|
||||||
|
let hours = Math.floor(data / 3600);
|
||||||
|
let minutes = Math.floor((data - (hours * 3600)) / 60);
|
||||||
|
|
||||||
|
if (minutes < 10) {minutes = "0"+minutes;}
|
||||||
|
return hours+':'+minutes;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 'number',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
if (type === "sort" || type === 'type') {
|
||||||
|
return parseFloat(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 'fraction',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
let splitted = data.split("/", 1);
|
||||||
|
let current = splitted[0]
|
||||||
|
if (type === "sort" || type === 'type') {
|
||||||
|
return parseInt(current);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from
|
// DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from
|
||||||
// the tbody and render two separate tables.
|
// the tbody and render two separate tables.
|
||||||
|
|||||||
49
WebHostLib/static/assets/tutorial/factorio/setup_en.md
Normal file
49
WebHostLib/static/assets/tutorial/factorio/setup_en.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Factorio Randomizer Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
### Server Host
|
||||||
|
- [Factorio](https://factorio.com)
|
||||||
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
|
||||||
|
### Players
|
||||||
|
- [Factorio](https://factorio.com)
|
||||||
|
|
||||||
|
## General Concept
|
||||||
|
|
||||||
|
One Server Host exists per Factorio World in an Archipelago Multiworld, any number of modded Factorio players can then connect to that world. From the view of Archipelago, this Factorio host is a client.
|
||||||
|
## Installation Procedures
|
||||||
|
|
||||||
|
### Dedicated Server Setup
|
||||||
|
You need a dedicated isolated Factorio installation that the FactorioClient can take control over, if you intend to both emit a world and play, you need to follow both this setup and the player setup.
|
||||||
|
This requires two Factorio installations. The easiest and cheapest way to do so is to either buy or register a Factorio on factorio.com, which allows you to download as many Factorio games as you want.
|
||||||
|
1. Download the latest Factorio from https://factorio.com/download for your system, for Windows the recommendation is "win64-manual".
|
||||||
|
|
||||||
|
2. Make sure the Factorio you play and the Factorio you use for hosting do not share paths. If you downloaded the "manual" version, this is already the case, otherwise, go into the hosting Factorio's folder and put the following text into its `config-path.cfg`:
|
||||||
|
```ini
|
||||||
|
config-path=__PATH__executable__/../../config
|
||||||
|
use-system-read-write-data-directories=false
|
||||||
|
```
|
||||||
|
3. Navigate to where you installed Archipelago and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your Factorio. If you put Factorio into your Archipelago folder, this would already match.
|
||||||
|
|
||||||
|
|
||||||
|
### Player Setup
|
||||||
|
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer.
|
||||||
|
|
||||||
|
|
||||||
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
|
1. Install the generated Factorio AP Mod (would be in <Factorio Directory>/Mods after step 2 of Setup)
|
||||||
|
|
||||||
|
2. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
||||||
|
|
||||||
|
* It should start up, create a world and become ready for Factorio connections.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -68,7 +68,7 @@ game: Minecraft
|
|||||||
|
|
||||||
# Shared Options supported by all games:
|
# Shared Options supported by all games:
|
||||||
accessibility: locations
|
accessibility: locations
|
||||||
progression_balancing: off
|
progression_balancing: on
|
||||||
# Minecraft Specific Options
|
# Minecraft Specific Options
|
||||||
|
|
||||||
# Number of advancements required (out of 92 total) to spawn the
|
# Number of advancements required (out of 92 total) to spawn the
|
||||||
@@ -108,7 +108,6 @@ shuffle_structures:
|
|||||||
off: 0
|
off: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
For more detail on what each setting does check the default `PlayerSettings.yaml` that comes with the Archipelago install.
|
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
@@ -126,8 +125,8 @@ previously.
|
|||||||
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
|
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
|
||||||
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
|
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
|
||||||
|
|
||||||
Once in game type `/connect <AP-Address> (<Password>)` where `<AP-Address>` is the address of the
|
Once in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
|
||||||
Archipelago server. `(<Password>)`
|
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)`
|
||||||
is only required if the Archipleago server you are using has a password set.
|
is only required if the Archipleago server you are using has a password set.
|
||||||
|
|
||||||
### Play the game
|
### Play the game
|
||||||
|
|||||||
@@ -86,6 +86,25 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"gameTitle": "Factorio",
|
||||||
|
"tutorials": [
|
||||||
|
{
|
||||||
|
"name": "Multiworld Setup Tutorial",
|
||||||
|
"description": "A guide to setting up the Archipelago Factorio software on your computer.",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "factorio/setup_en.md",
|
||||||
|
"link": "factorio/setup/en",
|
||||||
|
"authors": [
|
||||||
|
"Berserker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"gameTitle": "Minecraft",
|
"gameTitle": "Minecraft",
|
||||||
"tutorials": [
|
"tutorials": [
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
# A Link to the Past Randomizer Setup Guide
|
# A Link to the Past Randomizer Setup Guide
|
||||||
|
|
||||||
<div id="tutorial-video-container">
|
|
||||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/icWPmse0Z3E" frameborder="0"
|
|
||||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
|
|
||||||
</iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Benötigte Software
|
## 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)
|
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
|
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
|
||||||
- Ein Emulator, der lua-scripts abspielen kann
|
- Ein Emulator, der lua-scripts abspielen kann
|
||||||
@@ -21,7 +15,7 @@
|
|||||||
### Windows
|
### Windows
|
||||||
1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die
|
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!**.
|
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.
|
- 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
|
- 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.
|
bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Required Software
|
## 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)
|
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||||
- Hardware or software capable of loading and playing SNES ROM files
|
- Hardware or software capable of loading and playing SNES ROM files
|
||||||
- An emulator capable of running Lua scripts
|
- An emulator capable of running Lua scripts
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
### Windows Setup
|
### Windows Setup
|
||||||
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
|
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
|
**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.
|
- 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
|
- 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
|
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
|
## Hosting a MultiWorld game
|
||||||
The recommended way to host a game is to use the hosting service provided on
|
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.
|
1. Collect YAML files from your players.
|
||||||
2. Create a zip file containing your players' YAML files.
|
2. Create a zip file containing your players' YAML files.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Software requerido
|
## 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)
|
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
|
||||||
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
|
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
|
||||||
- Un emulador capaz de ejecutar scripts Lua
|
- Un emulador capaz de ejecutar scripts Lua
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
### Instalación en Windows
|
### Instalación en Windows
|
||||||
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
|
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'
|
- 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.
|
- 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.
|
- 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
|
## Hospedando una partida de multiworld
|
||||||
La manera recomendad para hospedar una partida es usar el servicio proveído en
|
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.
|
1. Recolecta los ficheros YAML de todos los jugadores que participen.
|
||||||
2. Crea un fichero ZIP conteniendo esos ficheros.
|
2. Crea un fichero ZIP conteniendo esos ficheros.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Logiciels requis
|
## 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)
|
- [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
|
- 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
|
- Un émulateur capable d'éxécuter des scripts Lua
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
1. Plando features have to be enabled first, before they can be used (opt-in).
|
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.
|
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
|
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the
|
||||||
value to
|
value to
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
### Bosses
|
### Bosses
|
||||||
|
|
||||||
- This module is enabled by default and available to be used on
|
- 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.
|
- 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,
|
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
|
||||||
it defaults to vanilla
|
it defaults to vanilla
|
||||||
|
|||||||
@@ -473,5 +473,11 @@ const generateGame = (raceMode = false) => {
|
|||||||
race: raceMode ? '1' : '0',
|
race: raceMode ? '1' : '0',
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
window.location.href = response.data.url;
|
window.location.href = response.data.url;
|
||||||
|
}).catch((error) => {
|
||||||
|
const userMessage = document.getElementById('user-message');
|
||||||
|
userMessage.innerText = 'Something went wrong and your game could not be generated.';
|
||||||
|
userMessage.classList.add('visible');
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ const generateGame = (raceMode = false) => {
|
|||||||
race: raceMode ? '1' : '0',
|
race: raceMode ? '1' : '0',
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
window.location.href = response.data.url;
|
window.location.href = response.data.url;
|
||||||
|
}).catch((error) => {
|
||||||
|
const userMessage = document.getElementById('user-message');
|
||||||
|
userMessage.innerText = 'Something went wrong and your game could not be generated.';
|
||||||
|
userMessage.classList.add('visible');
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
10
WebHostLib/static/styles/404.css
Normal file
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
3
WebHostLib/static/styles/factorio/factorio.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#factorio{
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
html{
|
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-repeat: repeat;
|
||||||
background-size: 650px 650px;
|
background-size: 650px 650px;
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,20 @@ html{
|
|||||||
color: #000000;
|
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{
|
#player-settings h1{
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
61
WebHostLib/static/styles/games.css
Normal file
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{
|
html{
|
||||||
background-image: url('../static/backgrounds/oceans/oceans-0002.png');
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 250px 250px;
|
|
||||||
font-family: 'Jost', sans-serif;
|
font-family: 'Jost', sans-serif;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: #000000;
|
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{
|
#base-header{
|
||||||
background: url('../../static/backgrounds/header/dirt-header.png') repeat-x;
|
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;
|
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{
|
#base-header{
|
||||||
background: url('../../static/backgrounds/header/ocean-header.png') repeat-x;
|
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{
|
#host-room{
|
||||||
width: calc(100% - 5rem);
|
width: calc(100% - 5rem);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ html{
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-header{
|
#landing-header{
|
||||||
@@ -53,18 +52,19 @@ html{
|
|||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploads-button{
|
#far-left-button{
|
||||||
top: 65px;
|
top: 115px;
|
||||||
left: calc(50% - 416px - 200px - 75px);
|
left: calc(50% - 416px - 200px - 75px);
|
||||||
background-image: url("/static/static/button-images/button-a.png");
|
background-image: url("/static/static/button-images/button-a.png");
|
||||||
background-size: 200px auto;
|
background-size: 200px auto;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: calc(156px - 40px);
|
height: calc(156px - 40px);
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
#setup-guide-button{
|
#mid-left-button{
|
||||||
top: 270px;
|
top: 320px;
|
||||||
left: calc(50% - 416px - 200px + 140px);
|
left: calc(50% - 416px - 200px + 140px);
|
||||||
background-image: url("/static/static/button-images/button-b.png");
|
background-image: url("/static/static/button-images/button-b.png");
|
||||||
background-size: 260px auto;
|
background-size: 260px auto;
|
||||||
@@ -73,8 +73,8 @@ html{
|
|||||||
padding-top: 35px;
|
padding-top: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-settings-button{
|
#mid-button{
|
||||||
top: 350px;
|
top: 400px;
|
||||||
left: calc(50% - 100px);
|
left: calc(50% - 100px);
|
||||||
background-image: url("/static/static/button-images/button-a.png");
|
background-image: url("/static/static/button-images/button-a.png");
|
||||||
background-size: 200px auto;
|
background-size: 200px auto;
|
||||||
@@ -83,8 +83,8 @@ html{
|
|||||||
padding-top: 38px;
|
padding-top: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#discord-button{
|
#mid-right-button{
|
||||||
top: 250px;
|
top: 300px;
|
||||||
left: calc(50% + 416px - 166px);
|
left: calc(50% + 416px - 166px);
|
||||||
background-image: url("/static/static/button-images/button-c.png");
|
background-image: url("/static/static/button-images/button-c.png");
|
||||||
background-size: 250px auto;
|
background-size: 250px auto;
|
||||||
@@ -94,8 +94,8 @@ html{
|
|||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#generate-button{
|
#far-right-button{
|
||||||
top: 75px;
|
top: 125px;
|
||||||
left: calc(50% + 416px + 75px);
|
left: calc(50% + 416px + 75px);
|
||||||
background-image: url("/static/static/button-images/button-b.png");
|
background-image: url("/static/static/button-images/button-b.png");
|
||||||
background-size: 260px auto;
|
background-size: 260px auto;
|
||||||
@@ -111,7 +111,7 @@ html{
|
|||||||
#landing-clouds #cloud1{
|
#landing-clouds #cloud1{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 265px;
|
top: 365px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
|
|
||||||
@@ -147,23 +147,23 @@ html{
|
|||||||
@keyframes c1-float{
|
@keyframes c1-float{
|
||||||
from{
|
from{
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 265px;
|
top: 365px;
|
||||||
}
|
}
|
||||||
25%{
|
25%{
|
||||||
left: 14px;
|
left: 14px;
|
||||||
top: 267px;
|
top: 367px;
|
||||||
}
|
}
|
||||||
50%{
|
50%{
|
||||||
left: 17px;
|
left: 17px;
|
||||||
top: 265px;
|
top: 365px;
|
||||||
}
|
}
|
||||||
75%{
|
75%{
|
||||||
left: 14px;
|
left: 14px;
|
||||||
top: 262px;
|
top: 362px;
|
||||||
}
|
}
|
||||||
to{
|
to{
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 265px;
|
top: 365px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,32 +241,32 @@ html{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#landing-deco-1{
|
#landing-deco-1{
|
||||||
top: 430px;
|
top: 480px;
|
||||||
left: calc(50% - 276px);
|
left: calc(50% - 276px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-deco-2{
|
#landing-deco-2{
|
||||||
top: 200px;
|
top: 250px;
|
||||||
left: calc(50% + 150px);
|
left: calc(50% + 150px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-deco-3{
|
#landing-deco-3{
|
||||||
top: 300px;
|
top: 350px;
|
||||||
left: calc(50% - 150px);
|
left: calc(50% - 150px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-deco-4{
|
#landing-deco-4{
|
||||||
top: 240px;
|
top: 290px;
|
||||||
left: calc(50% - 580px);
|
left: calc(50% - 580px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-deco-5{
|
#landing-deco-5{
|
||||||
top: 40px;
|
top: 90px;
|
||||||
left: calc(50% + 450px);
|
left: calc(50% + 450px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-deco-6{
|
#landing-deco-6{
|
||||||
top: 412px;
|
top: 462px;
|
||||||
left: calc(50% + 196px);
|
left: calc(50% + 196px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
WebHostLib/static/styles/minecraft/minecraft.css
Normal file
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
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%;
|
height: 100%;
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%) contrast(75%) brightness(75%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table img.acquired{
|
#inventory-table img.acquired{
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
html{
|
|
||||||
background-image: url('../static/backgrounds/dirt/dirt-0005-large.png');
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 900px 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tracker-wrapper {
|
#tracker-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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{
|
#tutorial-wrapper{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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{
|
#tutorial-landing{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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{
|
#weighted-settings{
|
||||||
width: 60rem;
|
width: 60rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -14,7 +8,7 @@ html{
|
|||||||
color: #eeffeb;
|
color: #eeffeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user-warning{
|
#user-warning, #weighted-settings #user-message{
|
||||||
display: none;
|
display: none;
|
||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
background-color: #ffe86b;
|
background-color: #ffe86b;
|
||||||
@@ -25,6 +19,10 @@ html{
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#weighted-settings #user-message.visible{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
#weighted-settings code{
|
#weighted-settings code{
|
||||||
background-color: #d9cd8e;
|
background-color: #d9cd8e;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
129
WebHostLib/static/styles/zelda3/player-settings.css
Normal file
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
3
WebHostLib/static/styles/zelda3/zelda3.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#zelda3{
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
17
WebHostLib/templates/404.html
Normal file
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 %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "tablepage.html" %}
|
{% extends "tablepage.html" %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/autodatatable.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/autodatatable.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Mystery Check Result</title>
|
<title>Mystery Check Result</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/check.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/check.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/check.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/check.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Mystery YAML Test Roll Results</title>
|
<title>Mystery YAML Test Roll Results</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/checkResult.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/checkResult.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
15
WebHostLib/templates/games/factorio/factorio.html
Normal file
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
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
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
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
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,17 +1,18 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Player Settings</title>
|
<title>A Link to the Past Settings</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/playerSettings.css") }}" />
|
<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="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/js-yaml.min.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/playerSettings.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/zelda3/player-settings.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<div id="player-settings">
|
<div id="player-settings">
|
||||||
<h1>Start Game</h1>
|
<div id="user-message"></div>
|
||||||
|
<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,
|
<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
|
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>
|
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||||
15
WebHostLib/templates/games/zelda3/zelda3.html
Normal file
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 %}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Generate Game</title>
|
<title>Generate Game</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/generate.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/generate.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/generate.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/generate.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<p>
|
<p>
|
||||||
After generation is complete, you will have the option to download a patch file.
|
After generation is complete, you will have the option to download a patch file.
|
||||||
This patch file can be opened with the
|
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.
|
used to to create a rom file. In-browser patching is planned for the future.
|
||||||
</p>
|
</p>
|
||||||
<div id="generate-game-form-wrapper">
|
<div id="generate-game-form-wrapper">
|
||||||
|
|||||||
63
WebHostLib/templates/genericTracker.html
Normal file
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 %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/header/baseHeader.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/header/baseHeader.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@@ -11,10 +11,8 @@
|
|||||||
<a href="/">archipelago</a>
|
<a href="/">archipelago</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="base-header-right">
|
<div id="base-header-right">
|
||||||
<a href="/player-settings">start game</a>
|
<a href="/games">games</a>
|
||||||
<a href="/uploads">host game</a>
|
|
||||||
<a href="/tutorial">setup guides</a>
|
<a href="/tutorial">setup guides</a>
|
||||||
<a href="/generate">upload config</a>
|
|
||||||
<a href="https://discord.gg/8Z65BR2">discord</a>
|
<a href="https://discord.gg/8Z65BR2">discord</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/header/dirtHeader.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/header/dirtHeader.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% include 'header/baseHeader.html' %}
|
{% include 'header/baseHeader.html' %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/header/grassHeader.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/header/grassHeader.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% include 'header/baseHeader.html' %}
|
{% include 'header/baseHeader.html' %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/header/oceanHeader.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/header/oceanHeader.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% include 'header/baseHeader.html' %}
|
{% include 'header/baseHeader.html' %}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Upload Multidata</title>
|
<title>Upload Multidata</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/hostGame.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostGame.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/hostGame.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/hostGame.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Multiworld {{ room.id|suuid }}</title>
|
<title>Multiworld {{ room.id|suuid }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/hostRoom.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
you can simply refresh this page and the server will be started again.<br>
|
you can simply refresh this page and the server will be started again.<br>
|
||||||
{% if room.last_port %}
|
{% if room.last_port %}
|
||||||
You can connect to this room by using '/connect archipelago.gg:{{ room.last_port }}'
|
You can connect to this room by using '/connect archipelago.gg:{{ room.last_port }}'
|
||||||
in the <a href="https://github.com/Berserker66/MultiWorld-Utilities/releases">client</a>.<br>{% endif %}
|
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>{% endif %}
|
||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<form method=post>
|
<form method=post>
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
<footer id="island-footer">
|
<footer id="island-footer">
|
||||||
<div id="copyright-notice">Copyright 2021 Archipelago</div>
|
<div id="copyright-notice">Copyright 2021 Archipelago</div>
|
||||||
<div id="links">
|
<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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/islandFooter.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/islandFooter.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,21 +2,22 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>MultiWorld</title>
|
<title>MultiWorld</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/landing.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/landing.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% include 'header/oceanHeader.html' %}
|
||||||
<div id="landing-wrapper">
|
<div id="landing-wrapper">
|
||||||
<div id="landing-header">
|
<div id="landing-header">
|
||||||
<h4>the legend of zelda: a link to the past</h4>
|
<h1>ARCHIPELAGO</h1>
|
||||||
<h1>MULTIWORLD RANDOMIZER</h1>
|
<h4>multiworld randomizer ecosystem</h4>
|
||||||
</div>
|
</div>
|
||||||
<div id="landing-links">
|
<div id="landing-links">
|
||||||
<a href="/player-settings" id="player-settings-button">start<br />playing</a>
|
<a href="/games" id="mid-button">start<br />playing</a>
|
||||||
<a href="/uploads" id="uploads-button">host<br />game</a>
|
<a id="far-left-button"></a>
|
||||||
<a href="/tutorial" id="setup-guide-button">setup guides</a>
|
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
||||||
<a href="/generate" id="generate-button">upload config</a>
|
<a href="/uploads" id="far-right-button">Host Game</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" id="discord-button">discord</a>
|
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="landing-clouds">
|
<div id="landing-clouds">
|
||||||
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
|
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
|
||||||
@@ -33,19 +34,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="landing" class="grass-island">
|
<div id="landing" class="grass-island">
|
||||||
<div id="landing-body">
|
<div id="landing-body">
|
||||||
<p id="first-line">Welcome to the Archipelago Multiworld Randomizer!</p>
|
<p id="first-line">Welcome to Archipelago!</p>
|
||||||
<p>This is a <span data-tooltip="Allegedly.">randomizer</span> for The Legend of Zelda: A
|
<p>
|
||||||
Link to the Past.</p>
|
This is a cross-game modification system which randomizes different games, then uses the result to
|
||||||
<p>It is also a multi-world, meaning Link's items may have been placed into other players' games.
|
build a single unified multi-player game. Items from one game may be present in another, and
|
||||||
When a player picks up an item which does not belong to them, it is sent back to the player
|
you will need your fellow players to find items you need in their games to help you complete
|
||||||
it belongs to.</p>
|
your own.
|
||||||
<p>On this website you are able to generate and host multiworld games, and item and location
|
</p>
|
||||||
trackers are provided for games hosted here.</p>
|
|
||||||
<p>
|
<p>
|
||||||
This project is the cumulative effort of many
|
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
|
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.
|
our crazy idea into a reality.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ player_name }}'s Tracker</title>
|
<title>{{ player_name }}'s Tracker</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/playerTracker.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerTracker.css") }}"/>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/playerTracker.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerTracker.js") }}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
||||||
<td><img src="{{ gloves_url }}" class="{{ 'acquired' if gloves_acquired }}" /></td>
|
<td><img src="{{ glove_url }}" class="{{ 'acquired' if glove_acquired }}" /></td>
|
||||||
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
||||||
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||||
@@ -7,11 +7,19 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
{% macro list_patches_room(room) %}
|
{% macro list_patches_room(room) %}
|
||||||
{% if room.seed.patches %}
|
{% if room.seed.slots %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for patch in room.seed.patches|list|sort(attribute="player_id") %}
|
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||||
|
{% if patch.game == "Minecraft" %}
|
||||||
|
<li><a href="{{ url_for("download_slot_file", seed_id=room.seed.id, player_id=patch.player_id) }}">
|
||||||
|
APMC for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||||
|
{% elif patch.game == "Factorio" %}
|
||||||
|
<li><a href="{{ url_for("download_slot_file", seed_id=room.seed.id, player_id=patch.player_id) }}">
|
||||||
|
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||||
|
{% else %}
|
||||||
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
||||||
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tooltip.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/cookieNotice.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/globalStyles.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/styleController.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/styleController.js") }}"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/cookieNotice.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>MultiWorld</title>
|
<title>MultiWorld</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Generation failed, please retry.</title>
|
<title>Generation failed, please retry.</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/waitSeed.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="https://cdn.datatables.net/v/dt/jq-3.3.1/dt-1.10.21/sc-2.0.2/sp-1.1.1/datatables.min.js"
|
src="https://cdn.datatables.net/v/dt/jq-3.3.1/dt-1.10.21/sc-2.0.2/sp-1.1.1/datatables.min.js"
|
||||||
></script>
|
></script>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tablepage.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tablepage.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Multiworld Tracker</title>
|
<title>Multiworld Tracker</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tracker.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/jquery.scrollsync.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/tracker.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -98,20 +98,20 @@
|
|||||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
<th rowspan="2" class="center-column">Last<br>Activity</th>
|
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for area in ordered_areas %}
|
{% for area in ordered_areas %}
|
||||||
<th class="center-column lower-row">
|
<th class="center-column lower-row fraction">
|
||||||
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
||||||
</th>
|
</th>
|
||||||
{% if area in key_locations %}
|
{% if area in key_locations %}
|
||||||
<th class="center-column lower-row">
|
<th class="center-column lower-row number">
|
||||||
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
|
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if area in big_key_locations %}
|
{% if area in big_key_locations %}
|
||||||
<th class="center-column lower-row">
|
<th class="center-column lower-row number">
|
||||||
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
|
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
|
||||||
</th>
|
</th>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- if activity_timers[(team, player)] -%}
|
{%- if activity_timers[(team, player)] -%}
|
||||||
<td class="center-column">{{ activity_timers[(team, player)] | render_timedelta }}</td>
|
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<td class="center-column">None</td>
|
<td class="center-column">None</td>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<title>Archipelago</title>
|
<title>Archipelago</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tutorial.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorial.css") }}" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/tutorial.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tutorial.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<title>Archipelago Guides</title>
|
<title>Archipelago Guides</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tutorialLanding.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorialLanding.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/tutorialLanding.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tutorialLanding.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Generate Game</title>
|
<title>Generate Game</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/userContent.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/userContent.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/userContent.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/userContent.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -31,10 +31,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("viewSeed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
<td><a href="{{ url_for("viewSeed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
||||||
<td><a href="{{ url_for("hostRoom", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
<td><a href="{{ url_for("hostRoom", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
||||||
<td
|
<td>>={{ room.seed.slots|length }}</td>
|
||||||
class="center"
|
|
||||||
data-tooltip="{{ room.seed.multidata.names[0]|join(", ")|truncate(256, False, " ...") }}"
|
|
||||||
>{{ room.seed.multidata.names[0]|length }}</td>
|
|
||||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -59,11 +56,7 @@
|
|||||||
{% for seed in seeds %}
|
{% for seed in seeds %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("viewSeed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
<td><a href="{{ url_for("viewSeed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
||||||
<td class="center"
|
<td>{% if seed.multidata %}>={{ seed.slots|length }}{% else %}1{% endif %}
|
||||||
{% if seed.multidata %}
|
|
||||||
data-tooltip="{{ seed.multidata.names[0]|join(", ")|truncate(256, False, " ...") }}"
|
|
||||||
{% endif %}
|
|
||||||
>{% if seed.multidata %}{{ seed.multidata.names[0]|length }}{% else %}1{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>View Seed {{ seed.id|suuid }}</title>
|
<title>View Seed {{ seed.id|suuid }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/viewSeed.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/viewSeed.css") }}"/>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/viewSeed.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/viewSeed.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -37,17 +37,10 @@
|
|||||||
<td>Players: </td>
|
<td>Players: </td>
|
||||||
<td>
|
<td>
|
||||||
<ul>
|
<ul>
|
||||||
{% for team in seed.multidata["names"] %}
|
{% for patch in seed.slots|sort(attribute='player_id') %}
|
||||||
{% set outer_loop = loop %}
|
<li>
|
||||||
<li>Team #{{ loop.index }} - {{ team | length }}
|
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player_id) }}">{{ patch.player_name }}</a>
|
||||||
<ul>
|
</li>
|
||||||
{% for player in team %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=loop.index, team_id=outer_loop.index0) }}">{{ player }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
@@ -64,13 +57,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Patches: </td>
|
<td>Files: </td>
|
||||||
<td>
|
<td>
|
||||||
<ul>
|
<ul>
|
||||||
{% for patch in seed.patches %}
|
{% for slot in seed.slots %}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player, team_id=0) }}">Player {{ patch.player }}</a>
|
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=slot.player_id) }}">Player {{ slot.player_name }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Generation in Progress</title>
|
<title>Generation in Progress</title>
|
||||||
<meta http-equiv="refresh" content="1">
|
<meta http-equiv="refresh" content="1">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/waitSeed.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Player Settings</title>
|
<title>Player Settings</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/weightedSettings.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedSettings.css") }}" />
|
||||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/js-yaml.min.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/weightedSettings.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedSettings.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<div id="weighted-settings">
|
<div id="weighted-settings">
|
||||||
<header id="user-warning"></header>
|
<header id="user-warning"></header>
|
||||||
|
<div id="user-message"></div>
|
||||||
<h1>Weighted Settings</h1>
|
<h1>Weighted Settings</h1>
|
||||||
<div id="instructions">
|
<div id="instructions">
|
||||||
This page is used to configure your weighted settings. You have three presets you can control, which
|
This page is used to configure your weighted settings. You have three presets you can control, which
|
||||||
|
|||||||
@@ -5,18 +5,17 @@ from werkzeug.exceptions import abort
|
|||||||
import datetime
|
import datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from worlds.alttp import Items, Regions
|
from worlds.alttp import Items
|
||||||
from WebHostLib import app, cache, Room
|
from WebHostLib import app, cache, Room
|
||||||
from NetUtils import Hint
|
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
|
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||||
|
|
||||||
|
def get_alttp_id(item_name):
|
||||||
def get_id(item_name):
|
|
||||||
return Items.item_table[item_name][2]
|
return Items.item_table[item_name][2]
|
||||||
|
|
||||||
|
|
||||||
app.jinja_env.filters["location_name"] = lambda location: Regions.lookup_id_to_name.get(location, location)
|
app.jinja_env.filters["location_name"] = lambda location: lookup_any_location_id_to_name.get(location, location)
|
||||||
app.jinja_env.filters['item_name'] = lambda id: Items.lookup_id_to_name.get(id, id)
|
app.jinja_env.filters['item_name'] = lambda id: lookup_any_item_id_to_name.get(id, id)
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||||
@@ -155,9 +154,9 @@ levels = {"Fighter Sword": 1,
|
|||||||
"Bow": 1,
|
"Bow": 1,
|
||||||
"Silver Bow": 2}
|
"Silver Bow": 2}
|
||||||
|
|
||||||
multi_items = {get_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")}
|
multi_items = {get_alttp_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")}
|
||||||
links = {get_id(key): get_id(value) for key, value in links.items()}
|
links = {get_alttp_id(key): get_alttp_id(value) for key, value in links.items()}
|
||||||
levels = {get_id(key): value for key, value in levels.items()}
|
levels = {get_alttp_id(key): value for key, value in levels.items()}
|
||||||
|
|
||||||
tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer",
|
tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer",
|
||||||
"Hookshot", "Magic Mirror", "Flute",
|
"Hookshot", "Magic Mirror", "Flute",
|
||||||
@@ -237,7 +236,7 @@ ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower',
|
|||||||
tracking_ids = []
|
tracking_ids = []
|
||||||
|
|
||||||
for item in tracking_names:
|
for item in tracking_names:
|
||||||
tracking_ids.append(get_id(item))
|
tracking_ids.append(get_alttp_id(item))
|
||||||
|
|
||||||
small_key_ids = {}
|
small_key_ids = {}
|
||||||
big_key_ids = {}
|
big_key_ids = {}
|
||||||
@@ -266,6 +265,7 @@ def attribute_item(inventory, team, recipient, item):
|
|||||||
|
|
||||||
|
|
||||||
def attribute_item_solo(inventory, item):
|
def attribute_item_solo(inventory, item):
|
||||||
|
"""Adds item to inventory counter, converts everything to progressive."""
|
||||||
target_item = links.get(item, item)
|
target_item = links.get(item, item)
|
||||||
if item in levels: # non-progressive
|
if item in levels: # non-progressive
|
||||||
inventory[target_item] = max(inventory[target_item], levels[item])
|
inventory[target_item] = max(inventory[target_item], levels[item])
|
||||||
@@ -319,20 +319,22 @@ def get_static_room_data(room: Room):
|
|||||||
|
|
||||||
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||||
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||||
for _, (item_id, item_player) in locations.items():
|
for loc_data in locations.values():
|
||||||
if item_id in ids_big_key:
|
for item_id, item_player in loc_data.values():
|
||||||
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
if item_id in ids_big_key:
|
||||||
if item_id in ids_small_key:
|
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
||||||
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
elif item_id in ids_small_key:
|
||||||
|
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
||||||
|
|
||||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
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
|
_multidata_cache[room.seed.id] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||||
@cache.memoize(timeout=15)
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||||
# Team and player must be positive and greater than zero
|
# Team and player must be positive and greater than zero
|
||||||
if tracked_team < 0 or tracked_player < 1:
|
if tracked_team < 0 or tracked_player < 1:
|
||||||
@@ -343,9 +345,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Collect seed information and pare it down to a single player
|
# Collect seed information and pare it down to a single player
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, 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]
|
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]
|
location_to_area = player_location_to_area[tracked_player]
|
||||||
inventory = collections.Counter()
|
inventory = collections.Counter()
|
||||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||||
@@ -362,130 +364,82 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
|||||||
multisave = {}
|
multisave = {}
|
||||||
|
|
||||||
# Add items to player inventory
|
# Add items to player inventory
|
||||||
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}):
|
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items():
|
||||||
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
|
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
|
||||||
# Skip teams and players not matching the request
|
# Skip teams and players not matching the request
|
||||||
|
player_locations = locations[ms_player]
|
||||||
if ms_team == tracked_team:
|
if ms_team == tracked_team:
|
||||||
# If the player does not have the item, do nothing
|
# If the player does not have the item, do nothing
|
||||||
for location in locations_checked:
|
for location in locations_checked:
|
||||||
if (location, ms_player) not in locations:
|
if location in player_locations:
|
||||||
continue
|
item, recipient = player_locations[location]
|
||||||
|
if recipient == tracked_player: # a check done for the tracked player
|
||||||
item, recipient = locations[location, ms_player]
|
attribute_item_solo(inventory, item)
|
||||||
if recipient == tracked_player: # a check done for the tracked player
|
if ms_player == tracked_player: # a check done by the tracked player
|
||||||
attribute_item_solo(inventory, item)
|
checks_done[location_to_area[location]] += 1
|
||||||
if ms_player == tracked_player: # a check done by the tracked player
|
checks_done["Total"] += 1
|
||||||
checks_done[location_to_area[location]] += 1
|
if games[tracked_player] == "A Link to the Past":
|
||||||
checks_done["Total"] += 1
|
# Note the presence of the triforce item
|
||||||
|
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||||
# Note the presence of the triforce item
|
if game_state == 30:
|
||||||
for (ms_team, ms_player), game_state in multisave.get("client_game_state", []):
|
|
||||||
# Skip teams and players not matching the request
|
|
||||||
if ms_team != tracked_team or ms_player != tracked_player:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if game_state:
|
|
||||||
inventory[106] = 1 # Triforce
|
inventory[106] = 1 # Triforce
|
||||||
|
|
||||||
acquired_items = []
|
# Progressive items need special handling for icons and class
|
||||||
for itm in inventory:
|
progressive_items = {
|
||||||
acquired_items.append(get_item_name_from_id(itm))
|
"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
|
# Determine which icon to use
|
||||||
progressive_items = {
|
display_data = {}
|
||||||
"Progressive Sword": 94,
|
for item_name, item_id in progressive_items.items():
|
||||||
"Progressive Glove": 97,
|
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||||
"Progressive Bow": 100,
|
display_name = progressive_names[item_name][level]
|
||||||
"Progressive Mail": 96,
|
acquired = True
|
||||||
"Progressive Shield": 95,
|
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 for the sword
|
|
||||||
sword_url = icons["Fighter Sword"]
|
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||||
sword_acquired = False
|
sp_areas = ordered_areas[2:15]
|
||||||
sword_names = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']
|
|
||||||
if "Progressive Sword" in acquired_items:
|
return render_template("lttpTracker.html", inventory=inventory,
|
||||||
sword_url = icons[sword_names[min(inventory[progressive_items["Progressive Sword"]], 4) - 1]]
|
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
|
||||||
sword_acquired = True
|
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:
|
else:
|
||||||
for sword in reversed(sword_names):
|
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||||
if sword in acquired_items:
|
return render_template("genericTracker.html",
|
||||||
sword_url = icons[sword]
|
inventory=inventory,
|
||||||
sword_acquired = True
|
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||||
break
|
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
|
||||||
|
|
||||||
gloves_url = icons["Power Glove"]
|
|
||||||
gloves_acquired = False
|
|
||||||
glove_names = ["Power Glove", "Titan Mitts"]
|
|
||||||
if "Progressive Glove" in acquired_items:
|
|
||||||
gloves_url = icons[glove_names[min(inventory[progressive_items["Progressive Glove"]], 2) - 1]]
|
|
||||||
gloves_acquired = True
|
|
||||||
else:
|
|
||||||
for glove in reversed(glove_names):
|
|
||||||
if glove in acquired_items:
|
|
||||||
gloves_url = icons[glove]
|
|
||||||
gloves_acquired = True
|
|
||||||
break
|
|
||||||
|
|
||||||
bow_url = icons["Bow"]
|
|
||||||
bow_acquired = False
|
|
||||||
bow_names = ["Bow", "Silver Bow"]
|
|
||||||
if "Progressive Bow" in acquired_items:
|
|
||||||
bow_url = icons[bow_names[min(inventory[progressive_items["Progressive Bow"]], 2) - 1]]
|
|
||||||
bow_acquired = True
|
|
||||||
else:
|
|
||||||
for bow in reversed(bow_names):
|
|
||||||
if bow in acquired_items:
|
|
||||||
bow_url = icons[bow]
|
|
||||||
bow_acquired = True
|
|
||||||
break
|
|
||||||
|
|
||||||
mail_url = icons["Green Mail"]
|
|
||||||
mail_names = ["Blue Mail", "Red Mail"]
|
|
||||||
if "Progressive Mail" in acquired_items:
|
|
||||||
mail_url = icons[mail_names[min(inventory[progressive_items["Progressive Mail"]], 2) - 1]]
|
|
||||||
else:
|
|
||||||
for mail in reversed(mail_names):
|
|
||||||
if mail in acquired_items:
|
|
||||||
mail_url = icons[mail]
|
|
||||||
break
|
|
||||||
|
|
||||||
shield_url = icons["Blue Shield"]
|
|
||||||
shield_acquired = False
|
|
||||||
shield_names = ["Blue Shield", "Red Shield", "Mirror Shield"]
|
|
||||||
if "Progressive Shield" in acquired_items:
|
|
||||||
shield_url = icons[shield_names[min(inventory[progressive_items["Progressive Shield"]], 3) - 1]]
|
|
||||||
shield_acquired = True
|
|
||||||
else:
|
|
||||||
for shield in reversed(shield_names):
|
|
||||||
if shield in acquired_items:
|
|
||||||
shield_url = icons[shield]
|
|
||||||
shield_acquired = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# 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=acquired_items,
|
|
||||||
sword_url=sword_url, sword_acquired=sword_acquired, gloves_url=gloves_url,
|
|
||||||
gloves_acquired=gloves_acquired, bow_url=bow_url, bow_acquired=bow_acquired,
|
|
||||||
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],
|
|
||||||
mail_url=mail_url, shield_url=shield_url, shield_acquired=shield_acquired)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>')
|
@app.route('/tracker/<suuid:tracker>')
|
||||||
@cache.memoize(timeout=30) # update every 30 seconds
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
def getTracker(tracker: UUID):
|
def getTracker(tracker: UUID):
|
||||||
room = Room.get(tracker=tracker)
|
room = Room.get(tracker=tracker)
|
||||||
if not room:
|
if not room:
|
||||||
abort(404)
|
abort(404)
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
|
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)}
|
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||||
for teamnumber, team in enumerate(names)}
|
for teamnumber, team in enumerate(names)}
|
||||||
@@ -500,26 +454,26 @@ def getTracker(tracker: UUID):
|
|||||||
else:
|
else:
|
||||||
multisave = {}
|
multisave = {}
|
||||||
if "hints" in multisave:
|
if "hints" in multisave:
|
||||||
for key, hintdata in multisave["hints"]:
|
for (team, slot), slot_hints in multisave["hints"].items():
|
||||||
for hint in hintdata:
|
hints[team] |= set(slot_hints)
|
||||||
hints[key[0]].add(Hint(*hint))
|
|
||||||
|
|
||||||
for (team, player), locations_checked in multisave.get("location_checks", {}):
|
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||||
|
player_locations = locations[player]
|
||||||
if precollected_items:
|
if precollected_items:
|
||||||
precollected = precollected_items[player]
|
precollected = precollected_items[player]
|
||||||
for item_id in precollected:
|
for item_id in precollected:
|
||||||
attribute_item(inventory, team, player, item_id)
|
attribute_item(inventory, team, player, item_id)
|
||||||
for location in locations_checked:
|
for location in locations_checked:
|
||||||
if (location, player) not in locations or location not in player_location_to_area[player]:
|
if location not in player_locations or location not in player_location_to_area[player]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item, recipient = locations[location, player]
|
item, recipient = player_locations[location]
|
||||||
attribute_item(inventory, team, recipient, item)
|
attribute_item(inventory, team, recipient, item)
|
||||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||||
checks_done[team][player]["Total"] += 1
|
checks_done[team][player]["Total"] += 1
|
||||||
|
|
||||||
for (team, player), game_state in multisave.get("client_game_state", []):
|
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
||||||
if game_state:
|
if game_state == 30:
|
||||||
inventory[team][player][106] = 1 # Triforce
|
inventory[team][player][106] = 1 # Triforce
|
||||||
|
|
||||||
group_big_key_locations = set()
|
group_big_key_locations = set()
|
||||||
@@ -538,7 +492,7 @@ def getTracker(tracker: UUID):
|
|||||||
for player, name in enumerate(names, 1):
|
for player, name in enumerate(names, 1):
|
||||||
player_names[(team, player)] = name
|
player_names[(team, player)] = name
|
||||||
long_player_names = player_names.copy()
|
long_player_names = player_names.copy()
|
||||||
for (team, player), alias in multisave.get("name_aliases", []):
|
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||||
player_names[(team, player)] = alias
|
player_names[(team, player)] = alias
|
||||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import json
|
|
||||||
import zlib
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import logging
|
import lzma
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
import MultiServer
|
import MultiServer
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
from pony.orm import commit, select
|
from pony.orm import flush, select
|
||||||
|
|
||||||
from WebHostLib import app, Seed, Room, Patch
|
from WebHostLib import app, Seed, Room, Slot
|
||||||
|
from Utils import parse_yaml
|
||||||
|
|
||||||
accepted_zip_contents = {"patches": ".apbp",
|
accepted_zip_contents = {"patches": ".apbp",
|
||||||
"spoiler": ".txt",
|
"spoiler": ".txt",
|
||||||
@@ -30,7 +31,7 @@ def uploads():
|
|||||||
flash('No selected file')
|
flash('No selected file')
|
||||||
elif file and allowed_file(file.filename):
|
elif file and allowed_file(file.filename):
|
||||||
if file.filename.endswith(".zip"):
|
if file.filename.endswith(".zip"):
|
||||||
patches = set()
|
slots = set()
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
multidata = None
|
multidata = None
|
||||||
with zipfile.ZipFile(file, 'r') as zfile:
|
with zipfile.ZipFile(file, 'r') as zfile:
|
||||||
@@ -40,9 +41,28 @@ def uploads():
|
|||||||
if file.filename.endswith(banned_zip_contents):
|
if file.filename.endswith(banned_zip_contents):
|
||||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||||
elif file.filename.endswith(".apbp"):
|
elif file.filename.endswith(".apbp"):
|
||||||
splitted = file.filename.split("/")[-1][3:].split("P", 1)
|
data = zfile.open(file, "r").read()
|
||||||
player_id, player_name = splitted[1].split(".")[0].split("_")
|
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||||
patches.add(Patch(data=zfile.open(file, "r").read(), player_name=player_name, player_id=player_id))
|
if yaml_data["version"] < 2:
|
||||||
|
return "Old format cannot be uploaded (outdated .apbp)", 500
|
||||||
|
metadata = yaml_data["meta"]
|
||||||
|
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||||
|
player_id=metadata["player_id"],
|
||||||
|
game="A Link to the Past"))
|
||||||
|
|
||||||
|
elif file.filename.endswith(".apmc"):
|
||||||
|
data = zfile.open(file, "r").read()
|
||||||
|
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||||
|
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||||
|
player_id=metadata["player_id"],
|
||||||
|
game="Minecraft"))
|
||||||
|
|
||||||
|
elif file.filename.endswith(".zip"):
|
||||||
|
# Factorio mods needs a specific name or they do no function
|
||||||
|
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-")
|
||||||
|
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
||||||
|
player_id=int(slot_id[1:]), game="Factorio"))
|
||||||
|
|
||||||
elif file.filename.endswith(".txt"):
|
elif file.filename.endswith(".txt"):
|
||||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||||
elif file.filename.endswith(".archipelago"):
|
elif file.filename.endswith(".archipelago"):
|
||||||
@@ -54,11 +74,11 @@ def uploads():
|
|||||||
else:
|
else:
|
||||||
multidata = zfile.open(file).read()
|
multidata = zfile.open(file).read()
|
||||||
if multidata:
|
if multidata:
|
||||||
commit() # commit patches
|
flush() # commit slots
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=session["_id"])
|
||||||
commit() # create seed
|
flush() # create seed
|
||||||
for patch in patches:
|
for slot in slots:
|
||||||
patch.seed = seed
|
slot.seed = seed
|
||||||
|
|
||||||
return redirect(url_for("viewSeed", seed=seed.id))
|
return redirect(url_for("viewSeed", seed=seed.id))
|
||||||
else:
|
else:
|
||||||
@@ -72,7 +92,7 @@ def uploads():
|
|||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||||
commit() # place into DB and generate ids
|
flush() # place into DB and generate ids
|
||||||
return redirect(url_for("viewSeed", seed=seed.id))
|
return redirect(url_for("viewSeed", seed=seed.id))
|
||||||
else:
|
else:
|
||||||
flash("Not recognized file format. Awaiting a .multidata file.")
|
flash("Not recognized file format. Awaiting a .multidata file.")
|
||||||
|
|||||||
159
WebUI.py
159
WebUI.py
@@ -1,159 +0,0 @@
|
|||||||
import http.server
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
import typing
|
|
||||||
import socket
|
|
||||||
import socketserver
|
|
||||||
import threading
|
|
||||||
import webbrowser
|
|
||||||
import asyncio
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from NetUtils import Node
|
|
||||||
from LttPClient import Context
|
|
||||||
import Utils
|
|
||||||
|
|
||||||
|
|
||||||
class WebUiClient(Node, logging.Handler):
|
|
||||||
loader = staticmethod(json.loads)
|
|
||||||
dumper = staticmethod(json.dumps)
|
|
||||||
def __init__(self):
|
|
||||||
super(WebUiClient, self).__init__()
|
|
||||||
self.manual_snes = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_message(msg_type: str, content: typing.Union[str, dict]) -> dict:
|
|
||||||
return {'type': msg_type, 'content': content}
|
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord) -> None:
|
|
||||||
self.broadcast_all({"type": record.levelname.lower(), "content": str(record.msg)})
|
|
||||||
|
|
||||||
def send_chat_message(self, message):
|
|
||||||
self.broadcast_all(self.build_message('chat', message))
|
|
||||||
|
|
||||||
def send_connection_status(self, ctx: Context):
|
|
||||||
asyncio.create_task(self._send_connection_status(ctx))
|
|
||||||
|
|
||||||
async def _send_connection_status(self, ctx: Context):
|
|
||||||
cache = Utils.persistent_load()
|
|
||||||
cached_address = cache.get("servers", {}).get("default", None)
|
|
||||||
server_address = ctx.server_address if ctx.server_address else cached_address if cached_address else None
|
|
||||||
|
|
||||||
self.broadcast_all(self.build_message('connections', {
|
|
||||||
'snesDevice': ctx.snes_attached_device[1] if ctx.snes_attached_device else None,
|
|
||||||
'snes': ctx.snes_state,
|
|
||||||
'serverAddress': server_address,
|
|
||||||
'server': 1 if ctx.server is not None and not ctx.server.socket.closed else 0,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_device_list(self, devices):
|
|
||||||
self.broadcast_all(self.build_message('availableDevices', {
|
|
||||||
'devices': devices,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def poll_for_server_ip(self):
|
|
||||||
self.broadcast_all(self.build_message('serverAddress', {}))
|
|
||||||
|
|
||||||
def notify_item_sent(self, finder, recipient, item, location, i_am_finder: bool, i_am_recipient: bool,
|
|
||||||
item_is_unique: bool = False):
|
|
||||||
self.broadcast_all(self.build_message('itemSent', {
|
|
||||||
'finder': finder,
|
|
||||||
'recipient': recipient,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'iAmFinder': int(i_am_finder),
|
|
||||||
'iAmRecipient': int(i_am_recipient),
|
|
||||||
'itemIsUnique': int(item_is_unique),
|
|
||||||
}))
|
|
||||||
|
|
||||||
def notify_item_found(self, finder: str, item: str, location: str, i_am_finder: bool, item_is_unique: bool = False):
|
|
||||||
self.broadcast_all(self.build_message('itemFound', {
|
|
||||||
'finder': finder,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'iAmFinder': int(i_am_finder),
|
|
||||||
'itemIsUnique': int(item_is_unique),
|
|
||||||
}))
|
|
||||||
|
|
||||||
def notify_item_received(self, finder: str, item: str, location: str, item_index: int, queue_length: int,
|
|
||||||
item_is_unique: bool = False):
|
|
||||||
self.broadcast_all(self.build_message('itemReceived', {
|
|
||||||
'finder': finder,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'itemIndex': item_index,
|
|
||||||
'queueLength': queue_length,
|
|
||||||
'itemIsUnique': int(item_is_unique),
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_hint(self, finder, recipient, item, location, found, i_am_finder: bool, i_am_recipient: bool,
|
|
||||||
entrance_location: str = None):
|
|
||||||
self.broadcast_all(self.build_message('hint', {
|
|
||||||
'finder': finder,
|
|
||||||
'recipient': recipient,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'found': int(found),
|
|
||||||
'iAmFinder': int(i_am_finder),
|
|
||||||
'iAmRecipient': int(i_am_recipient),
|
|
||||||
'entranceLocation': entrance_location,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_game_info(self, ctx: Context):
|
|
||||||
self.broadcast_all(self.build_message('gameInfo', {
|
|
||||||
'clientVersion': Utils.__version__,
|
|
||||||
'hintCost': ctx.hint_cost,
|
|
||||||
'checkPoints': ctx.check_points,
|
|
||||||
'forfeitMode': ctx.forfeit_mode,
|
|
||||||
'remainingMode': ctx.remaining_mode,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_location_check(self, ctx: Context, last_check: str):
|
|
||||||
self.broadcast_all(self.build_message('locationCheck', {
|
|
||||||
'totalChecks': len(ctx.locations_checked),
|
|
||||||
'hintPoints': ctx.hint_points,
|
|
||||||
'lastCheck': last_check,
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
web_thread = None
|
|
||||||
PORT = 5050
|
|
||||||
|
|
||||||
|
|
||||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
||||||
def log_request(self, code='-', size='-'):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def log_date_time_string(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
Handler = partial(RequestHandler,
|
|
||||||
directory=Utils.local_path("data", "web", "public"))
|
|
||||||
|
|
||||||
|
|
||||||
def start_server(socket_port: int, on_start=lambda: None):
|
|
||||||
global web_thread
|
|
||||||
try:
|
|
||||||
server = socketserver.TCPServer(("", PORT), Handler)
|
|
||||||
except OSError:
|
|
||||||
# In most cases "Only one usage of each socket address (protocol/network address/port) is normally permitted"
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# If the exception is caused by our desired port being unavailable, assume the web server is already running
|
|
||||||
# from another client instance
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
||||||
if sock.connect_ex(('localhost', PORT)) == 0:
|
|
||||||
logging.info("Web server is already running in another client window.")
|
|
||||||
webbrowser.open(f'http://localhost:{PORT}?port={socket_port}')
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the exception is caused by something else, report on it
|
|
||||||
logging.exception("Unable to bind port for local web server. The CLI client should work in all cases.")
|
|
||||||
else:
|
|
||||||
print("serving at port", PORT)
|
|
||||||
on_start()
|
|
||||||
web_thread = threading.Thread(target=server.serve_forever).start()
|
|
||||||
BIN
data/ER.icns
BIN
data/ER.icns
Binary file not shown.
BIN
data/ER.ico
BIN
data/ER.ico
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
BIN
data/ER16.gif
BIN
data/ER16.gif
Binary file not shown.
|
Before Width: | Height: | Size: 123 B |
BIN
data/ER32.gif
BIN
data/ER32.gif
Binary file not shown.
|
Before Width: | Height: | Size: 370 B |
BIN
data/ER48.gif
BIN
data/ER48.gif
Binary file not shown.
|
Before Width: | Height: | Size: 882 B |
1
data/factorio/machines.json
Normal file
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}}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user