Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b5ac3f926 | ||
|
|
72e5acfb86 | ||
|
|
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 | ||
|
|
fb192b989d | ||
|
|
d35adc5868 | ||
|
|
c0bf4f58ad | ||
|
|
f24a81fdaf | ||
|
|
40ff0e867c | ||
|
|
a231850911 | ||
|
|
1b2283b173 | ||
|
|
729088fd85 | ||
|
|
88d75a41ae | ||
|
|
237b44ca66 | ||
|
|
6fef30d9b3 |
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/
|
||||||
@@ -35,4 +37,110 @@ mystery_result_*.yaml
|
|||||||
success.txt
|
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/
|
||||||
|
|||||||
@@ -44,6 +44,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
|
||||||
@@ -879,13 +880,12 @@ 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):
|
||||||
if self.combat_difficulty(player) == 'easy':
|
if self.combat_difficulty(player) == 'easy':
|
||||||
@@ -1193,6 +1193,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 +1229,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
|
||||||
@@ -1467,6 +1475,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:
|
||||||
@@ -1490,16 +1499,21 @@ class Spoiler(object):
|
|||||||
'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:
|
if player in self.world.hk_player_ids:
|
||||||
import Options
|
|
||||||
for hk_option in Options.hollow_knight_options:
|
for hk_option in Options.hollow_knight_options:
|
||||||
res = getattr(self.world, hk_option)[player]
|
res = getattr(self.world, hk_option)[player]
|
||||||
outfile.write(f'{hk_option+":":33}{res}\n')
|
outfile.write(f'{hk_option+":":33}{res}\n')
|
||||||
if player in self.world.minecraft_player_ids:
|
|
||||||
import Options
|
elif player in self.world.factorio_player_ids:
|
||||||
|
for f_option in Options.factorio_options:
|
||||||
|
res = getattr(self.world, f_option)[player]
|
||||||
|
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
||||||
|
|
||||||
|
elif player in self.world.minecraft_player_ids:
|
||||||
for mc_option in Options.minecraft_options:
|
for mc_option in Options.minecraft_options:
|
||||||
res = getattr(self.world, mc_option)[player]
|
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')
|
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:
|
|
||||||
|
elif player in self.world.alttp_player_ids:
|
||||||
for team in range(self.world.teams):
|
for team in range(self.world.teams):
|
||||||
outfile.write('%s%s\n' % (
|
outfile.write('%s%s\n' % (
|
||||||
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
|
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
|
||||||
@@ -1575,17 +1589,24 @@ 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.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 +1616,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
|
||||||
@@ -49,10 +49,6 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
"""List all received items"""
|
"""List all received items"""
|
||||||
logger.info('Received items:')
|
logger.info('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.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)' % (
|
logging.info('%s from %s (%s) (%d/%d in list)' % (
|
||||||
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
|
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
|
||||||
color(self.ctx.player_names[item.player], 'yellow'),
|
color(self.ctx.player_names[item.player], 'yellow'),
|
||||||
@@ -116,7 +112,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 +125,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 +190,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.')
|
||||||
@@ -223,9 +219,6 @@ class CommonContext():
|
|||||||
|
|
||||||
|
|
||||||
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 +230,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 +241,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 +262,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 +279,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")
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import string
|
import string
|
||||||
import copy
|
import copy
|
||||||
|
import sys
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -21,8 +23,6 @@ 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"
|
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)
|
||||||
options = Utils.get_options()
|
options = Utils.get_options()
|
||||||
executable = options["factorio_options"]["executable"]
|
executable = options["factorio_options"]["executable"]
|
||||||
@@ -35,10 +35,14 @@ if not os.path.exists(executable):
|
|||||||
else:
|
else:
|
||||||
raise FileNotFoundError(executable)
|
raise FileNotFoundError(executable)
|
||||||
|
|
||||||
threadpool = ThreadPoolExecutor(10)
|
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
||||||
|
|
||||||
|
thread_pool = ThreadPoolExecutor(10)
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
@@ -63,7 +67,9 @@ 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:
|
||||||
@@ -79,42 +85,50 @@ 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):
|
async def game_watcher(ctx: FactorioContext, bridge_file: str):
|
||||||
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
|
bridge_counter = 0
|
||||||
try:
|
try:
|
||||||
while 1:
|
while not ctx.exit_event.is_set():
|
||||||
if os.path.exists(bridge_file):
|
if os.path.exists(bridge_file):
|
||||||
bridge_logger.info("Found Factorio Bridge file.")
|
bridge_logger.info("Found Factorio Bridge file.")
|
||||||
while 1:
|
while not ctx.exit_event.is_set():
|
||||||
with open(bridge_file) as f:
|
if ctx.awaiting_bridge:
|
||||||
data = json.load(f)
|
ctx.awaiting_bridge = False
|
||||||
research_data = data["research_done"]
|
with open(bridge_file) as f:
|
||||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
data = json.load(f)
|
||||||
victory = data["victory"]
|
research_data = data["research_done"]
|
||||||
ctx.auth = data["slot_name"]
|
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||||
|
victory = data["victory"]
|
||||||
|
ctx.auth = data["slot_name"]
|
||||||
|
ctx.seed_name = data["seed_name"]
|
||||||
|
|
||||||
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: "
|
||||||
ctx.locations_checked = research_data
|
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
ctx.locations_checked = research_data
|
||||||
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
bridge_counter += 1
|
bridge_counter += 1
|
||||||
@@ -157,12 +171,13 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
||||||
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
||||||
script_folder = None
|
script_folder = None
|
||||||
|
progression_watcher = None
|
||||||
try:
|
try:
|
||||||
while 1:
|
while not ctx.exit_event.is_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')")
|
||||||
@@ -174,7 +189,10 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
if os.path.exists(bridge_file):
|
if os.path.exists(bridge_file):
|
||||||
os.remove(bridge_file)
|
os.remove(bridge_file)
|
||||||
logging.info(f"Bridge File Path: {bridge_file}")
|
logging.info(f"Bridge File Path: {bridge_file}")
|
||||||
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
progression_watcher = asyncio.create_task(
|
||||||
|
game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
||||||
|
if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
|
||||||
|
ctx.awaiting_bridge = True
|
||||||
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]
|
||||||
@@ -193,6 +211,11 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
logging.error("Aborted Factorio Server Bridge")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
factorio_process.terminate()
|
||||||
|
if progression_watcher:
|
||||||
|
await progression_watcher
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
ctx = FactorioContext(None, None, True)
|
ctx = FactorioContext(None, None, True)
|
||||||
@@ -223,6 +246,20 @@ async def main():
|
|||||||
await input_task
|
await input_task
|
||||||
|
|
||||||
|
|
||||||
|
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__':
|
||||||
colorama.init()
|
colorama.init()
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|||||||
167
FactorioClientGUI.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
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 server_loop, logger
|
||||||
|
from FactorioClient import FactorioContext, factorio_server_watcher
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
ctx = FactorioContext(None, None, True)
|
||||||
|
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||||
|
ui_app = FactorioManager(ctx)
|
||||||
|
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
||||||
|
|
||||||
|
await ctx.exit_event.wait() # wait for signal to exit application
|
||||||
|
ui_app.stop()
|
||||||
|
ctx.server_address = None
|
||||||
|
ctx.snes_reconnect_address = None
|
||||||
|
# allow tasks to quit
|
||||||
|
await ui_task
|
||||||
|
await factorio_server_task
|
||||||
|
await ctx.server_task
|
||||||
|
|
||||||
|
if ctx.server is not None and not ctx.server.socket.closed:
|
||||||
|
await ctx.server.socket.close()
|
||||||
|
if ctx.server_task is not None:
|
||||||
|
await ctx.server_task
|
||||||
|
|
||||||
|
while ctx.input_requests > 0: # clear queue for shutdown
|
||||||
|
ctx.input_queue.put_nowait(None)
|
||||||
|
ctx.input_requests -= 1
|
||||||
|
|
||||||
|
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.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 = "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 File 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()
|
||||||
|
loop.run_until_complete(main())
|
||||||
|
loop.close()
|
||||||
4
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())
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
107
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
|
||||||
@@ -495,7 +471,7 @@ async def get_snes_devices(ctx: Context):
|
|||||||
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 +493,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,7 +512,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:
|
if 'sd2snes' in device.lower() or 'COM' in device:
|
||||||
logger.info("SD2SNES/FXPAK Detected")
|
logger.info("SD2SNES/FXPAK Detected")
|
||||||
@@ -607,7 +580,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
|
||||||
|
|
||||||
@@ -743,8 +715,6 @@ 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:
|
||||||
@@ -887,10 +857,6 @@ 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),
|
|
||||||
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 + 1, len(ctx.items_received)))
|
||||||
@@ -920,57 +886,6 @@ 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()
|
||||||
@@ -982,8 +897,6 @@ async def main():
|
|||||||
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:
|
||||||
@@ -1002,23 +915,9 @@ async def main():
|
|||||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||||
|
|
||||||
port = None
|
port = None
|
||||||
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)
|
ctx = Context(args.snes, args.connect, args.password, args.founditems)
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
if 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")
|
||||||
|
|||||||
272
Main.py
@@ -7,7 +7,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
|
||||||
@@ -67,6 +67,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()
|
||||||
@@ -170,14 +171,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.
|
||||||
@@ -314,7 +316,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):
|
||||||
@@ -366,8 +368,7 @@ 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 = {
|
||||||
@@ -410,145 +411,168 @@ def main(args, seed=None):
|
|||||||
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 = []
|
mod_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.factorio_player_ids:
|
||||||
mod_futures.append(pool.submit(generate_mod, world, player,
|
mod_futures.append(pool.submit(generate_mod, world, player))
|
||||||
str(args.outputname if args.outputname else world.seed)))
|
|
||||||
|
|
||||||
def get_entrance_to_region(region: Region):
|
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 world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||||
from worlds.alttp.Regions import RegionType
|
from worlds.alttp.Regions import RegionType
|
||||||
for region in world.regions:
|
for region in world.regions:
|
||||||
if region.player in er_hint_data and region.locations:
|
if region.player in er_hint_data and region.locations:
|
||||||
main_entrance = get_entrance_to_region(region)
|
main_entrance = get_entrance_to_region(region)
|
||||||
for location in region.locations:
|
for location in region.locations:
|
||||||
if type(location.address) == int: # skips events and crystals
|
if type(location.address) == int: # skips events and crystals
|
||||||
if lookup_vanilla_location_to_entrance[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
|
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 world.retro[player]]:
|
||||||
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
||||||
player = region.player
|
player = region.player
|
||||||
location_id = SHOP_ID_START + total_shop_slots + index
|
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)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FillDisabledShopSlots(world)
|
||||||
|
|
||||||
|
def write_multidata(roms, mods):
|
||||||
|
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:
|
||||||
|
if world.game[slot] == "Factorio":
|
||||||
|
client_versions[slot] = (0, 1, 2)
|
||||||
|
else:
|
||||||
|
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}
|
||||||
precollected_items = {player: [] for player in range(1, world.players+1)}
|
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 visibility
|
||||||
|
sending_visible_players = set()
|
||||||
|
for player in world.factorio_player_ids:
|
||||||
|
if world.visibility[player]:
|
||||||
|
sending_visible_players.add(player)
|
||||||
|
|
||||||
FillDisabledShopSlots(world)
|
for i, team in enumerate(parsed_names):
|
||||||
|
for player, name in enumerate(team, 1):
|
||||||
|
if player not in world.alttp_player_ids:
|
||||||
|
connect_names[name] = (i, player)
|
||||||
|
for slot in world.hk_player_ids:
|
||||||
|
slots_data = slot_data[slot] = {}
|
||||||
|
for option_name in Options.hollow_knight_options:
|
||||||
|
option = getattr(world, option_name)[slot]
|
||||||
|
slots_data[option_name] = int(option.value)
|
||||||
|
for slot in world.minecraft_player_ids:
|
||||||
|
slot_data[slot] = fill_minecraft_slot_data(world, slot)
|
||||||
|
|
||||||
def write_multidata(roms, mods):
|
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||||
import base64
|
for location in world.get_filled_locations():
|
||||||
for future in roms:
|
if type(location.address) == int:
|
||||||
rom_name = future.result()
|
locations_data[location.player][location.address] = (location.item.code, location.item.player)
|
||||||
rom_names.append(rom_name)
|
if location.player in sending_visible_players and location.item.player != location.player:
|
||||||
slot_data = {}
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
client_versions = {}
|
location.item.code, False)
|
||||||
minimum_versions = {"server": (0, 0, 4), "clients": client_versions}
|
precollected_hints[location.player].add(hint)
|
||||||
games = {}
|
precollected_hints[location.item.player].add(hint)
|
||||||
for slot in world.player_ids:
|
elif location.item.name in args.start_hints[location.item.player]:
|
||||||
client_versions[slot] = (0, 0, 3)
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
games[slot] = world.game[slot]
|
location.item.code, False,
|
||||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||||
slot, team, rom_name in rom_names}
|
precollected_hints[location.player].add(hint)
|
||||||
|
precollected_hints[location.item.player].add(hint)
|
||||||
|
|
||||||
for i, team in enumerate(parsed_names):
|
multidata = zlib.compress(pickle.dumps({
|
||||||
for player, name in enumerate(team, 1):
|
"slot_data" : slot_data,
|
||||||
if player not in world.alttp_player_ids:
|
"games": games,
|
||||||
connect_names[name] = (i, player)
|
"names": parsed_names,
|
||||||
for slot in world.hk_player_ids:
|
"connect_names": connect_names,
|
||||||
slots_data = slot_data[slot] = {}
|
"remote_items": {player for player in range(1, world.players + 1) if
|
||||||
for option_name in Options.hollow_knight_options:
|
world.remote_items[player]},
|
||||||
option = getattr(world, option_name)[slot]
|
"locations": locations_data,
|
||||||
slots_data[option_name] = int(option.value)
|
"checks_in_area": checks_in_area,
|
||||||
for slot in world.minecraft_player_ids:
|
"server_options": get_options()["server_options"],
|
||||||
slot_data[slot] = fill_minecraft_slot_data(world, slot)
|
"er_hint_data": er_hint_data,
|
||||||
multidata = zlib.compress(pickle.dumps({
|
"precollected_items": precollected_items,
|
||||||
"slot_data" : slot_data,
|
"precollected_hints": precollected_hints,
|
||||||
"games": games,
|
"version": tuple(_version_tuple),
|
||||||
"names": parsed_names,
|
"tags": ["AP"],
|
||||||
"connect_names": connect_names,
|
"minimum_versions": minimum_versions,
|
||||||
"remote_items": {player for player in range(1, world.players + 1) if
|
"seed_name": world.seed_name
|
||||||
world.remote_items[player]},
|
}), 9)
|
||||||
"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:
|
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||||
f.write(bytes([1])) # version of format
|
f.write(bytes([1])) # version of format
|
||||||
f.write(multidata)
|
f.write(multidata)
|
||||||
for future in mods:
|
for future in mods:
|
||||||
future.result() # collect errors if they occured
|
future.result() # collect errors if they occured
|
||||||
|
|
||||||
multidata_task = pool.submit(write_multidata, rom_futures, mod_futures)
|
multidata_task = pool.submit(write_multidata, rom_futures, mod_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.")
|
||||||
@@ -558,7 +582,7 @@ def main(args, seed=None):
|
|||||||
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
|
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))
|
generate_mc_data(world, player)
|
||||||
if not args.skip_playthrough:
|
if not args.skip_playthrough:
|
||||||
logger.info('Calculating playthrough.')
|
logger.info('Calculating playthrough.')
|
||||||
create_playthrough(world)
|
create_playthrough(world)
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -26,7 +26,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 +40,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"]
|
||||||
@@ -124,16 +123,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 +157,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 +168,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)
|
||||||
|
|||||||
147
MultiServer.py
@@ -43,7 +43,7 @@ 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
|
||||||
@@ -78,7 +78,7 @@ class Context(Node):
|
|||||||
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 +114,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()
|
||||||
@@ -132,7 +137,7 @@ class Context(Node):
|
|||||||
|
|
||||||
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 = {}
|
||||||
@@ -155,7 +160,8 @@ 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)
|
||||||
@@ -240,42 +246,40 @@ class Context(Node):
|
|||||||
|
|
||||||
def get_save(self) -> dict:
|
def get_save(self) -> dict:
|
||||||
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 +320,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):
|
||||||
@@ -437,10 +445,6 @@ 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
|
||||||
@@ -449,22 +453,22 @@ def send_new_items(ctx: Context):
|
|||||||
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 +477,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)
|
||||||
@@ -504,29 +508,26 @@ def notify_team(ctx: Context, team: int, text: str):
|
|||||||
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 = Regions.lookup_name_to_id[location]
|
seeked_location: int = Regions.lookup_name_to_id[location]
|
||||||
for check, result in ctx.locations.items():
|
item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
|
||||||
location_id, finding_player = check
|
if item_id:
|
||||||
if finding_player == slot and location_id == seeked_location:
|
found = seeked_location in ctx.location_checks[team, slot]
|
||||||
item_id, receiving_player = result
|
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||||
found = location_id in ctx.location_checks[team, finding_player]
|
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance)]
|
||||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
return []
|
||||||
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:
|
||||||
@@ -786,7 +787,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 +800,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.")
|
||||||
@@ -864,7 +865,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 +886,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 +900,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 +927,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,21 +942,19 @@ 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):
|
||||||
@@ -1032,7 +1031,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)
|
||||||
@@ -1047,7 +1046,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
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 +1057,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}])
|
||||||
@@ -1206,7 +1205,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))
|
||||||
|
|||||||
18
Mystery.py
@@ -303,7 +303,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]:
|
||||||
@@ -538,6 +541,8 @@ 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(weights.get('start_hints', []))
|
||||||
|
|
||||||
|
|
||||||
if ret.game == "A Link to the Past":
|
if ret.game == "A Link to the Past":
|
||||||
roll_alttp_settings(ret, weights, plando_options)
|
roll_alttp_settings(ret, weights, plando_options)
|
||||||
@@ -559,6 +564,17 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||||||
setattr(ret, option_name, option.from_any(get_choice(option_name, 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))
|
||||||
|
# bad hardcoded behavior to make this work for now
|
||||||
|
ret.plando_connections = []
|
||||||
|
if "connections" in plando_options:
|
||||||
|
options = 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")
|
||||||
|
))
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unsupported game {ret.game}")
|
raise Exception(f"Unsupported game {ret.game}")
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -308,3 +308,7 @@ class Hint(typing.NamedTuple):
|
|||||||
add_json_text(parts, ".")
|
add_json_text(parts, ".")
|
||||||
|
|
||||||
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
|
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def local(self):
|
||||||
|
return self.receiving_player == self.finding_player
|
||||||
|
|||||||
23
Options.py
@@ -139,6 +139,8 @@ 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):
|
||||||
|
return str(self.value)
|
||||||
|
|
||||||
class Logic(Choice):
|
class Logic(Choice):
|
||||||
option_no_glitches = 0
|
option_no_glitches = 0
|
||||||
@@ -309,8 +311,16 @@ class TechTreeLayout(Choice):
|
|||||||
option_single = 0
|
option_single = 0
|
||||||
option_small_diamonds = 1
|
option_small_diamonds = 1
|
||||||
option_medium_diamonds = 2
|
option_medium_diamonds = 2
|
||||||
option_pyramid = 3
|
option_large_diamonds = 3
|
||||||
option_funnel = 4
|
option_small_pyramids = 4
|
||||||
|
option_medium_pyramids = 5
|
||||||
|
option_large_pyramids = 6
|
||||||
|
option_small_funnels = 7
|
||||||
|
option_medium_funnels = 8
|
||||||
|
option_large_funnels = 9
|
||||||
|
option_funnels = 4
|
||||||
|
alias_pyramid = 6
|
||||||
|
alias_funnel = 9
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -319,6 +329,12 @@ class Visibility(Choice):
|
|||||||
option_sending = 1
|
option_sending = 1
|
||||||
default = 1
|
default = 1
|
||||||
|
|
||||||
|
class RecipeTime(Choice):
|
||||||
|
option_vanilla = 0
|
||||||
|
option_fast = 1
|
||||||
|
option_normal = 2
|
||||||
|
option_slow = 4
|
||||||
|
option_chaos = 5
|
||||||
|
|
||||||
class FactorioStartItems(OptionDict):
|
class FactorioStartItems(OptionDict):
|
||||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||||
@@ -330,7 +346,8 @@ factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxScien
|
|||||||
"free_samples": FreeSamples,
|
"free_samples": FreeSamples,
|
||||||
"visibility": Visibility,
|
"visibility": Visibility,
|
||||||
"random_tech_ingredients": Toggle,
|
"random_tech_ingredients": Toggle,
|
||||||
"starting_items": FactorioStartItems}
|
"starting_items": FactorioStartItems,
|
||||||
|
"recipe_time": RecipeTime}
|
||||||
|
|
||||||
|
|
||||||
class AdvancementGoal(Choice):
|
class AdvancementGoal(Choice):
|
||||||
|
|||||||
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
|
||||||
|
|||||||
17
Utils.py
@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
|
|||||||
minor: int
|
minor: int
|
||||||
build: int
|
build: int
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.2"
|
||||||
_version_tuple = tuplize_version(__version__)
|
_version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
@@ -188,7 +188,7 @@ def get_default_options() -> dict:
|
|||||||
"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,
|
||||||
@@ -203,10 +203,10 @@ def get_default_options() -> dict:
|
|||||||
"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,
|
||||||
@@ -345,12 +345,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")
|
||||||
@@ -392,6 +392,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))
|
||||||
|
|||||||
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)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import socket
|
|||||||
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 +47,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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -9,6 +10,8 @@ import time
|
|||||||
|
|
||||||
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"""
|
||||||
@@ -25,6 +28,7 @@ class AlreadyRunningException(Exception):
|
|||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
class Locker(CommonLocker):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
@@ -43,6 +47,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 +83,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 +150,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 +175,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
|
||||||
|
|
||||||
|
|||||||
@@ -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.9.0
|
Flask-Compress>=1.9.0
|
||||||
Flask-Limiter>=1.4
|
Flask-Limiter>=1.4
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
51
WebHostLib/static/assets/tutorial/factorio/setup_en.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 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. Make a fresh world. Using any Factorio, create a savegame with options of your choice and save that world as Archipelago.
|
||||||
|
|
||||||
|
2. Take that savegame and put it into your Archipelago folder
|
||||||
|
|
||||||
|
3. Install the generated Factorio AP Mod
|
||||||
|
|
||||||
|
4. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
||||||
|
|
||||||
|
* It should say it loaded the Archipelago mod and found a bridge file. If not, the most likely case is that the mod is not correctly installed or activated.
|
||||||
|
|
||||||
|
5. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
|
||||||
|
|
||||||
|
* / commands are run on your local client, ! commands are requests for the AP server
|
||||||
|
|
||||||
|
* Players should be able to connect to your Factorio Server and begin playing.
|
||||||
|
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -14,7 +14,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 +25,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;
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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
@@ -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 %}
|
||||||
|
|||||||
@@ -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,7 +2,7 @@
|
|||||||
|
|
||||||
{% 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 %}
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
trackers are provided for games hosted here.</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 %}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Player Settings</title>
|
<title>Player 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/playerSettings.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/playerSettings.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<div id="player-settings">
|
<div id="player-settings">
|
||||||
|
<div id="user-message"></div>
|
||||||
<h1>Start Game</h1>
|
<h1>Start Game</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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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
@@ -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.ico
|
Before Width: | Height: | Size: 38 KiB |
BIN
data/ER16.gif
|
Before Width: | Height: | Size: 123 B |
BIN
data/ER32.gif
|
Before Width: | Height: | Size: 370 B |
BIN
data/ER48.gif
|
Before Width: | Height: | Size: 882 B |
1
data/factorio/machines.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}}
|
||||||
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 24 KiB |
BIN
data/factorio/mod/graphics/icons/ap_unimportant.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,15 +1,11 @@
|
|||||||
{% macro dict_to_lua(dict) -%}
|
{% from "macros.lua" import dict_to_lua %}
|
||||||
{
|
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||||
{% for key, value in dict.items() %}
|
|
||||||
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
}
|
|
||||||
{%- endmacro %}
|
|
||||||
require "lib"
|
require "lib"
|
||||||
require "util"
|
require "util"
|
||||||
|
|
||||||
FREE_SAMPLES = {{ free_samples }}
|
FREE_SAMPLES = {{ free_samples }}
|
||||||
SLOT_NAME = "{{ slot_name }}"
|
SLOT_NAME = "{{ slot_name }}"
|
||||||
|
SEED_NAME = "{{ seed_name }}"
|
||||||
--SUPPRESS_INVENTORY_EVENTS = false
|
--SUPPRESS_INVENTORY_EVENTS = false
|
||||||
|
|
||||||
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
|
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
|
||||||
@@ -137,16 +133,18 @@ script.on_init(function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
-- for testing
|
-- for testing
|
||||||
script.on_event(defines.events.on_tick, function(event)
|
-- script.on_event(defines.events.on_tick, function(event)
|
||||||
if event.tick%600 == 300 then
|
-- if event.tick%3600 == 300 then
|
||||||
dumpInfo(game.forces["player"])
|
-- dumpInfo(game.forces["player"])
|
||||||
end
|
-- end
|
||||||
end)
|
-- end)
|
||||||
|
|
||||||
-- hook into researches done
|
-- hook into researches done
|
||||||
script.on_event(defines.events.on_research_finished, function(event)
|
script.on_event(defines.events.on_research_finished, function(event)
|
||||||
local technology = event.research
|
local technology = event.research
|
||||||
dumpInfo(technology.force)
|
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
||||||
|
dumpInfo(technology.force) --is sendable
|
||||||
|
end
|
||||||
if FREE_SAMPLES == 0 then
|
if FREE_SAMPLES == 0 then
|
||||||
return -- Nothing else to do
|
return -- Nothing else to do
|
||||||
end
|
end
|
||||||
@@ -180,7 +178,8 @@ function dumpInfo(force)
|
|||||||
local data_collection = {
|
local data_collection = {
|
||||||
["research_done"] = research_done,
|
["research_done"] = research_done,
|
||||||
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
||||||
["slot_name"] = SLOT_NAME
|
["slot_name"] = SLOT_NAME,
|
||||||
|
["seed_name"] = SEED_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
for tech_name, tech in pairs(force.technologies) do
|
for tech_name, tech in pairs(force.technologies) do
|
||||||
@@ -189,6 +188,7 @@ function dumpInfo(force)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
|
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
|
||||||
|
log("Archipelago Bridge File written for game tick ".. game.tick .. ".")
|
||||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
||||||
-- game.print("Sent progress to Archipelago.")
|
-- game.print("Sent progress to Archipelago.")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
{% from "macros.lua" import dict_to_recipe %}
|
||||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||||
require('lib')
|
require('lib')
|
||||||
|
|
||||||
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }}
|
data.raw["recipe"]["rocket-part"].ingredients = {{ dict_to_recipe(rocket_recipe) }}
|
||||||
|
|
||||||
local technologies = data.raw["technology"]
|
local technologies = data.raw["technology"]
|
||||||
local original_tech
|
local original_tech
|
||||||
@@ -30,31 +31,51 @@ function prep_copy(new_copy, old_tech)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function set_ap_icon(tech)
|
||||||
|
tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
|
||||||
|
tech.icons = nil
|
||||||
|
tech.icon_size = 128
|
||||||
|
end
|
||||||
|
|
||||||
|
function set_ap_unimportant_icon(tech)
|
||||||
|
tech.icon = "__{{ mod_name }}__/graphics/icons/ap_unimportant.png"
|
||||||
|
tech.icons = nil
|
||||||
|
tech.icon_size = 128
|
||||||
|
end
|
||||||
|
|
||||||
|
function copy_factorio_icon(tech, tech_source)
|
||||||
|
tech.icon = table.deepcopy(technologies[tech_source].icon)
|
||||||
|
tech.icons = table.deepcopy(technologies[tech_source].icons)
|
||||||
|
tech.icon_size = table.deepcopy(technologies[tech_source].icon_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
function adjust_energy(recipe_name, factor)
|
||||||
|
local energy = data.raw.recipe[recipe_name].energy_required
|
||||||
|
if (energy == nil) then
|
||||||
|
energy = 1
|
||||||
|
end
|
||||||
|
data.raw.recipe[recipe_name].energy_required = energy * factor
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
|
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
|
||||||
|
|
||||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||||
{%- for original_tech_name, item_name, receiving_player in locations %}
|
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||||
original_tech = technologies["{{original_tech_name}}"]
|
original_tech = technologies["{{original_tech_name}}"]
|
||||||
{#- the tech researched by the local player #}
|
{#- the tech researched by the local player #}
|
||||||
new_tree_copy = table.deepcopy(template_tech)
|
new_tree_copy = table.deepcopy(template_tech)
|
||||||
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
||||||
prep_copy(new_tree_copy, original_tech)
|
prep_copy(new_tree_copy, original_tech)
|
||||||
{% if tech_cost != 1 %}
|
{% if tech_cost != 1 %}
|
||||||
if new_tree_copy.unit.count then
|
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||||
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
|
||||||
end
|
|
||||||
{% endif %}
|
|
||||||
{% if item_name in tech_table and visibility %}
|
|
||||||
{#- copy Factorio Technology Icon #}
|
|
||||||
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
|
|
||||||
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
|
|
||||||
new_tree_copy.icon_size = table.deepcopy(technologies["{{ item_name }}"].icon_size)
|
|
||||||
{% else %}
|
|
||||||
{#- use default AP icon if no Factorio graphics exist #}
|
|
||||||
new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
|
|
||||||
new_tree_copy.icons = nil
|
|
||||||
new_tree_copy.icon_size = 512
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{%- if item_name in tech_table and visibility -%}
|
||||||
|
{#- copy Factorio Technology Icon -#}
|
||||||
|
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
|
||||||
|
{%- else -%}
|
||||||
|
{#- use default AP icon if no Factorio graphics exist -#}
|
||||||
|
{% if advancement %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
|
||||||
|
{%- endif -%}
|
||||||
{#- connect Technology #}
|
{#- connect Technology #}
|
||||||
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
||||||
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||||
@@ -63,5 +84,9 @@ table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
|||||||
{% endif -%}
|
{% endif -%}
|
||||||
{#- add new Technology to game #}
|
{#- add new Technology to game #}
|
||||||
data:extend{new_tree_copy}
|
data:extend{new_tree_copy}
|
||||||
|
{% endfor %}
|
||||||
{% endfor %}
|
{% if recipe_time_scale %}
|
||||||
|
{%- for recipe in recipes %}
|
||||||
|
adjust_energy("{{ recipe }}", {{ random.triangular(*recipe_time_scale) }})
|
||||||
|
{%- endfor -%}
|
||||||
|
{% endif %}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
[technology-name]
|
[technology-name]
|
||||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||||
{%- if visibility -%}
|
{%- if visibility -%}
|
||||||
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -9,9 +9,9 @@ ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
[technology-description]
|
[technology-description]
|
||||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||||
{%- if visibility -%}
|
{%- if visibility -%}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}.
|
||||||
{% else %}
|
{% else %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
14
data/factorio/mod_template/macros.lua
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% macro dict_to_lua(dict) -%}
|
||||||
|
{
|
||||||
|
{%- for key, value in dict.items() -%}
|
||||||
|
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor -%}
|
||||||
|
}
|
||||||
|
{%- endmacro %}
|
||||||
|
{% macro dict_to_recipe(dict) -%}
|
||||||
|
{
|
||||||
|
{%- for key, value in dict.items() -%}
|
||||||
|
{"{{ key }}", {{ value | safe }}}{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor -%}
|
||||||
|
}
|
||||||
|
{%- endmacro %}
|
||||||
BIN
data/icon.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["@babel/preset-react", "@babel/preset-env"],
|
|
||||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'airbnb',
|
|
||||||
],
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
globals: {
|
|
||||||
Atomics: 'readonly',
|
|
||||||
SharedArrayBuffer: 'readonly',
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'react',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
"react/jsx-filename-extension": 0,
|
|
||||||
"react/jsx-one-expression-per-line": 0,
|
|
||||||
"react/destructuring-assignment": 0,
|
|
||||||
"react/jsx-curly-spacing": [2, { "when": "always" }],
|
|
||||||
"react/prop-types": 0,
|
|
||||||
"react/no-access-state-in-setstate": 0,
|
|
||||||
"react/button-has-type": 0,
|
|
||||||
"max-len": [2, { code: 120 }],
|
|
||||||
"operator-linebreak": [2, "after"],
|
|
||||||
"no-console": [2, { allow: ["error", "warn"] }],
|
|
||||||
"linebreak-style": 0,
|
|
||||||
"jsx-a11y/no-static-element-interactions": 0,
|
|
||||||
"jsx-a11y/click-events-have-key-events": 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
2
data/web/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
*.map
|
|
||||||
14170
data/web/package-lock.json
generated
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "web-ui",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.jsx",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --config webpack.config.js",
|
|
||||||
"dev": "webpack --config webpack.dev.js"
|
|
||||||
},
|
|
||||||
"author": "LegendaryLinux",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
|
||||||
"crypto-browserify": "^3.12.0",
|
|
||||||
"crypto-js": "^4.0.0",
|
|
||||||
"css-loader": "^5.1.3",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"prop-types": "^15.7.2",
|
|
||||||
"react": "^17.0.1",
|
|
||||||
"react-dom": "^17.0.1",
|
|
||||||
"react-redux": "^7.2.2",
|
|
||||||
"react-router-dom": "^5.2.0",
|
|
||||||
"redux": "^4.0.5",
|
|
||||||
"redux-devtools-extension": "^2.13.9",
|
|
||||||
"sass-loader": "^10.1.1",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"webpack-cli": "^4.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.13.10",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
|
||||||
"@babel/preset-env": "^7.13.10",
|
|
||||||
"@babel/preset-react": "^7.12.13",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"babel-loader": "^8.2.2",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"eslint": "^7.22.0",
|
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
|
||||||
"eslint-plugin-import": "^2.22.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
||||||
"eslint-plugin-react": "^7.22.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"stream-browserify": "^3.0.0",
|
|
||||||
"webpack": "^5.27.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Berserker Multiworld Web GUI</title>
|
|
||||||
<script type="application/ecmascript" src="assets/index.bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<!-- Populated by React/JSX -->
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 242 KiB |
@@ -1,10 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import '../../../styles/HeaderBar/components/HeaderBar.scss';
|
|
||||||
|
|
||||||
const HeaderBar = () => (
|
|
||||||
<div id="header-bar">
|
|
||||||
Multiworld WebUI
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default HeaderBar;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const APPEND_MESSAGE = 'APPEND_MESSAGE';
|
|
||||||
|
|
||||||
const appendMessage = (content) => ({
|
|
||||||
type: APPEND_MESSAGE,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default appendMessage;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const SET_MONITOR_FONT_SIZE = 'SET_MONITOR_FONT_SIZE';
|
|
||||||
|
|
||||||
const setMonitorFontSize = (fontSize) => ({
|
|
||||||
type: SET_MONITOR_FONT_SIZE,
|
|
||||||
fontSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setMonitorFontSize;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const SET_SHOW_RELEVANT = 'SET_SHOW_RELEVANT';
|
|
||||||
|
|
||||||
const setShowRelevant = (showRelevant) => ({
|
|
||||||
type: SET_SHOW_RELEVANT,
|
|
||||||
showRelevant,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setShowRelevant;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const SET_SIMPLE_FONT = 'SET_SIMPLE_FONT';
|
|
||||||
|
|
||||||
const setSimpleFont = (simpleFont) => ({
|
|
||||||
type: SET_SIMPLE_FONT,
|
|
||||||
simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setSimpleFont;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import _assign from 'lodash-es/assign';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
fontSize: 18,
|
|
||||||
simpleFont: false,
|
|
||||||
showRelevantOnly: false,
|
|
||||||
messageLog: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const appendToLog = (log, item) => {
|
|
||||||
const trimmedLog = log.slice(-349);
|
|
||||||
trimmedLog.push(item);
|
|
||||||
return trimmedLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitorReducer = (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_MONITOR_FONT_SIZE':
|
|
||||||
return _assign({}, state, {
|
|
||||||
fontSize: action.fontSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'SET_SIMPLE_FONT':
|
|
||||||
return _assign({}, state, {
|
|
||||||
simpleFont: action.simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'SET_SHOW_RELEVANT':
|
|
||||||
return _assign({}, state, {
|
|
||||||
showRelevantOnly: action.showRelevant,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'APPEND_MESSAGE':
|
|
||||||
return _assign({}, state, {
|
|
||||||
messageLog: appendToLog(state.messageLog, action.content),
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default monitorReducer;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import '../../../styles/Monitor/components/Monitor.scss';
|
|
||||||
import MonitorControls from '../containers/MonitorControls';
|
|
||||||
import MonitorWindow from '../containers/MonitorWindow';
|
|
||||||
|
|
||||||
const Monitor = () => (
|
|
||||||
<div id="monitor">
|
|
||||||
<MonitorControls />
|
|
||||||
<MonitorWindow />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Monitor;
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import _forEach from 'lodash-es/forEach';
|
|
||||||
import WebSocketUtils from '../../global/WebSocketUtils';
|
|
||||||
import '../../../styles/Monitor/containers/MonitorControls.scss';
|
|
||||||
|
|
||||||
// Redux actions
|
|
||||||
import setMonitorFontSize from '../Redux/actions/setMonitorFontSize';
|
|
||||||
import setShowRelevant from '../Redux/actions/setShowRelevant';
|
|
||||||
import setSimpleFont from '../Redux/actions/setSimpleFont';
|
|
||||||
|
|
||||||
const mapReduxStateToProps = (reduxState) => ({
|
|
||||||
fontSize: reduxState.monitor.fontSize,
|
|
||||||
webSocket: reduxState.webUI.webSocket,
|
|
||||||
availableDevices: reduxState.webUI.availableDevices,
|
|
||||||
snesDevice: reduxState.gameState.connections.snesDevice,
|
|
||||||
snesConnected: reduxState.gameState.connections.snesConnected,
|
|
||||||
serverAddress: reduxState.gameState.connections.serverAddress,
|
|
||||||
serverConnected: reduxState.gameState.connections.serverConnected,
|
|
||||||
simpleFont: reduxState.monitor.simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
updateFontSize: (fontSize) => {
|
|
||||||
dispatch(setMonitorFontSize(fontSize));
|
|
||||||
},
|
|
||||||
doToggleRelevance: (showRelevantOnly) => {
|
|
||||||
dispatch(setShowRelevant(showRelevantOnly));
|
|
||||||
},
|
|
||||||
doSetSimpleFont: (simpleFont) => {
|
|
||||||
dispatch(setSimpleFont(simpleFont));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class MonitorControls extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
deviceId: null,
|
|
||||||
serverAddress: this.props.serverAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.props.webSocket) {
|
|
||||||
// Poll for available devices
|
|
||||||
this.pollSnesDevices();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
// If there is only one SNES device available, connect to it automatically
|
|
||||||
if (
|
|
||||||
prevProps.availableDevices.length !== this.props.availableDevices.length &&
|
|
||||||
this.props.availableDevices.length === 1
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
|
||||||
this.setState({ deviceId: this.props.availableDevices[0] }, () => {
|
|
||||||
if (!this.props.snesConnected) {
|
|
||||||
this.connectToSnes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have moved from a disconnected state (default) into a connected state, request the game information
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
(prevProps.snesConnected !== this.props.snesConnected) || // SNES status changed
|
|
||||||
(prevProps.serverConnected !== this.props.serverConnected) // OR server status changed
|
|
||||||
) && ((this.props.serverConnected) && (this.props.snesConnected)) // AND both are connected
|
|
||||||
) {
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'gameInfo'));
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'checkData'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
increaseTextSize = () => {
|
|
||||||
if (this.props.fontSize >= 25) return;
|
|
||||||
this.props.updateFontSize(this.props.fontSize + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
decreaseTextSize = () => {
|
|
||||||
if (this.props.fontSize <= 10) return;
|
|
||||||
this.props.updateFontSize(this.props.fontSize - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateSnesOptions = () => {
|
|
||||||
const options = [];
|
|
||||||
// No available devices, show waiting for devices
|
|
||||||
if (this.props.availableDevices.length === 0) {
|
|
||||||
options.push(<option key="0" value="-1">Waiting for devices...</option>);
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
// More than one available device, list all options
|
|
||||||
options.push(<option key="-1" value="-1">Select a device</option>);
|
|
||||||
_forEach(this.props.availableDevices, (device) => {
|
|
||||||
options.push(<option key={ device } value={ device }>{device}</option>);
|
|
||||||
});
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDeviceId = (event) => this.setState({ deviceId: event.target.value }, this.connectToSnes);
|
|
||||||
|
|
||||||
pollSnesDevices = () => {
|
|
||||||
if (!this.props.webSocket) { return; }
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'devices'));
|
|
||||||
}
|
|
||||||
|
|
||||||
connectToSnes = () => {
|
|
||||||
if (!this.props.webSocket) { return; }
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webConfig', { deviceId: this.state.deviceId }));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateServerAddress = (event) => this.setState({ serverAddress: event.target.value ? event.target.value : null });
|
|
||||||
|
|
||||||
connectToServer = (event) => {
|
|
||||||
if (event.key !== 'Enter') { return; }
|
|
||||||
|
|
||||||
// If the user presses enter on an empty textbox, disconnect from the server
|
|
||||||
if (!event.target.value) {
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webControl', 'disconnect'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.webSocket.send(
|
|
||||||
WebSocketUtils.formatSocketData('webConfig', { serverAddress: this.state.serverAddress }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRelevance = (event) => {
|
|
||||||
this.props.doToggleRelevance(event.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
setSimpleFont = (event) => this.props.doSetSimpleFont(event.target.checked);
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="monitor-controls">
|
|
||||||
<div id="connection-status">
|
|
||||||
<div id="snes-connection">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>SNES Device:</td>
|
|
||||||
<td>
|
|
||||||
<select
|
|
||||||
onChange={ this.updateDeviceId }
|
|
||||||
disabled={ this.props.availableDevices.length === 0 }
|
|
||||||
value={ this.state.deviceId }
|
|
||||||
>
|
|
||||||
{this.generateSnesOptions()}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status:</td>
|
|
||||||
<td>
|
|
||||||
<span className={ this.props.snesConnected ? 'connected' : 'not-connected' }>
|
|
||||||
{this.props.snesConnected ? 'Connected' : 'Not Connected'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="server-connection">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Server:</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
defaultValue={ this.props.serverAddress }
|
|
||||||
onKeyUp={ this.updateServerAddress }
|
|
||||||
onKeyDown={ this.connectToServer }
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status:</td>
|
|
||||||
<td>
|
|
||||||
<span className={ this.props.serverConnected ? 'connected' : 'not-connected' }>
|
|
||||||
{this.props.serverConnected ? 'Connected' : 'Not Connected'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="accessibility">
|
|
||||||
<div>
|
|
||||||
Text Size:
|
|
||||||
<button disabled={ this.props.fontSize <= 10 } onClick={ this.decreaseTextSize }>-</button>
|
|
||||||
{ this.props.fontSize }
|
|
||||||
<button disabled={ this.props.fontSize >= 25 } onClick={ this.increaseTextSize }>+</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Only show my items <input type="checkbox" onChange={ this.toggleRelevance } />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Use alternate font
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
onChange={ this.setSimpleFont }
|
|
||||||
defaultChecked={ this.props.simpleFont }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapReduxStateToProps, mapDispatchToProps)(MonitorControls);
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import md5 from 'crypto-js/md5';
|
|
||||||
import WebSocketUtils from '../../global/WebSocketUtils';
|
|
||||||
import '../../../styles/Monitor/containers/MonitorWindow.scss';
|
|
||||||
|
|
||||||
// Redux actions
|
|
||||||
import appendMessage from '../Redux/actions/appendMessage';
|
|
||||||
|
|
||||||
const mapReduxStateToProps = (reduxState) => ({
|
|
||||||
fontSize: reduxState.monitor.fontSize,
|
|
||||||
webSocket: reduxState.webUI.webSocket,
|
|
||||||
messageLog: reduxState.monitor.messageLog,
|
|
||||||
showRelevantOnly: reduxState.monitor.showRelevantOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
doAppendMessage: (message) => dispatch(appendMessage(
|
|
||||||
<div
|
|
||||||
key={ `${md5(message)}${Math.floor((Math.random() * 1000000))}` }
|
|
||||||
className="user-command relevant"
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</div>,
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class MonitorWindow extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.monitorRef = React.createRef();
|
|
||||||
this.commandRef = React.createRef();
|
|
||||||
this.commandInputRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// Adjust the monitor height to match user's viewport
|
|
||||||
this.adjustMonitorHeight();
|
|
||||||
|
|
||||||
// Resize the monitor as the user adjusts the window size
|
|
||||||
window.addEventListener('resize', this.adjustMonitorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.monitorRef.current.style.fontSize = `${this.props.fontSize}px`;
|
|
||||||
this.adjustMonitorHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// If one day we have different components occupying the main viewport, let us not attempt to
|
|
||||||
// perform actions on an unmounted component
|
|
||||||
window.removeEventListener('resize', this.adjustMonitorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustMonitorHeight = () => {
|
|
||||||
const monitorDimensions = this.monitorRef.current.getBoundingClientRect();
|
|
||||||
const commandDimensions = this.commandRef.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Set monitor height
|
|
||||||
const newMonitorHeight = window.innerHeight - monitorDimensions.top - commandDimensions.height - 30;
|
|
||||||
this.monitorRef.current.style.height = `${newMonitorHeight}px`;
|
|
||||||
this.scrollToBottom();
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollToBottom = () => {
|
|
||||||
this.monitorRef.current.scrollTo(0, this.monitorRef.current.scrollHeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
sendCommand = (event) => {
|
|
||||||
// If the user didn't press enter, or the command is empty, do nothing
|
|
||||||
if (event.key !== 'Enter' || !event.target.value) return;
|
|
||||||
this.props.doAppendMessage(event.target.value);
|
|
||||||
this.scrollToBottom();
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webCommand', event.target.value));
|
|
||||||
this.commandInputRef.current.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="monitor-window-wrapper">
|
|
||||||
<div
|
|
||||||
id="monitor-window"
|
|
||||||
ref={ this.monitorRef }
|
|
||||||
className={ `${this.props.showRelevantOnly ? 'relevant-only' : null}` }
|
|
||||||
>
|
|
||||||
{ this.props.messageLog }
|
|
||||||
</div>
|
|
||||||
<div id="command-wrapper" ref={ this.commandRef }>
|
|
||||||
Command: <input onKeyDown={ this.sendCommand } ref={ this.commandInputRef } />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapReduxStateToProps, mapDispatchToProps)(MonitorWindow);
|
|
||||||