mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-13 11:03:49 -07:00
Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4813fcac08 | ||
|
|
e50db61030 | ||
|
|
f8c3b695d0 | ||
|
|
431b64c574 | ||
|
|
f06d160615 | ||
|
|
909172cbad | ||
|
|
382c6d0445 | ||
|
|
4efd27694a | ||
|
|
fa24fd31d0 | ||
|
|
c55983af5f | ||
|
|
9c3d12dc55 | ||
|
|
37755cd362 | ||
|
|
eb02d65dbb | ||
|
|
4d38f44da3 | ||
|
|
212abc2b5a | ||
|
|
3797c20488 | ||
|
|
2f7e532f4f | ||
|
|
eb2a3009f4 | ||
|
|
5c9aa09c80 | ||
|
|
e5bbcb8d27 | ||
|
|
cf488e5a5d | ||
|
|
36ef9e8a72 | ||
|
|
e7d254aed7 | ||
|
|
298f2f652a | ||
|
|
5cb2689609 | ||
|
|
9ab5ec426d | ||
|
|
77d3bf9172 | ||
|
|
328f132498 | ||
|
|
de2ead3a9b | ||
|
|
ff96b391b9 | ||
|
|
4a75d27261 | ||
|
|
05e464e379 | ||
|
|
e8e141b206 | ||
|
|
bed8fe82cf | ||
|
|
3a1d33f499 | ||
|
|
6ea68dd290 | ||
|
|
97030590c2 | ||
|
|
60f64cc46b | ||
|
|
5087b78c28 | ||
|
|
95358bc523 | ||
|
|
4fc1ce77ac | ||
|
|
b8c7d6a72f | ||
|
|
e04fbd1d77 | ||
|
|
fb8229fda5 | ||
|
|
569e0e3004 | ||
|
|
73ed18c11d | ||
|
|
65df153947 | ||
|
|
2dd6dcab20 | ||
|
|
4494207717 | ||
|
|
88265c5585 | ||
|
|
501c55cc26 | ||
|
|
a5efed83b9 | ||
|
|
e7a746c06c | ||
|
|
063997610d | ||
|
|
432ae5865d | ||
|
|
73bc5fb376 | ||
|
|
a7c9474a37 | ||
|
|
0cf9baef4b | ||
|
|
6a06117786 | ||
|
|
ee30914b2c | ||
|
|
a995627e98 | ||
|
|
7884c6cd97 | ||
|
|
4fe10b88b3 | ||
|
|
b7327138f3 | ||
|
|
433981fd3d | ||
|
|
2df7e4e537 | ||
|
|
764e6e7926 | ||
|
|
4292cdddd5 | ||
|
|
9aef76767a | ||
|
|
3858a12f26 | ||
|
|
1943586221 | ||
|
|
6d15aef88a | ||
|
|
50f06c3aac | ||
|
|
7f3c46dd8a | ||
|
|
ea15f221ae | ||
|
|
d4b422840a | ||
|
|
0586b24579 | ||
|
|
e11016b0a2 | ||
|
|
74a368458e | ||
|
|
1b70d485c0 | ||
|
|
2355f9c8d3 | ||
|
|
ceea55e3c6 | ||
|
|
c4d6ac50be | ||
|
|
4461cb67f0 | ||
|
|
f0a6b5a8e4 | ||
|
|
443fc03700 | ||
|
|
6567f14415 | ||
|
|
32560eac92 | ||
|
|
4c71662719 | ||
|
|
96a28ed41e | ||
|
|
bc1d0ed583 | ||
|
|
635897574f | ||
|
|
0eca0b2209 | ||
|
|
20b72369d8 | ||
|
|
d451145d53 | ||
|
|
4ab59d522d | ||
|
|
250099f5fd | ||
|
|
c14a150795 | ||
|
|
91bcd59940 | ||
|
|
b871a688a4 | ||
|
|
d225eb9ca8 |
111
.gitignore
vendored
111
.gitignore
vendored
@@ -12,6 +12,8 @@
|
||||
*.db3
|
||||
*multidata
|
||||
*multisave
|
||||
*.archipelago
|
||||
*.apsave
|
||||
|
||||
build
|
||||
bundle/components.wxs
|
||||
@@ -19,7 +21,6 @@ dist
|
||||
README.html
|
||||
.vs/
|
||||
EnemizerCLI/
|
||||
.mypy_cache/
|
||||
RaceRom.py
|
||||
weights/
|
||||
/MultiMystery/
|
||||
@@ -35,4 +36,110 @@ mystery_result_*.yaml
|
||||
success.txt
|
||||
output/
|
||||
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/
|
||||
162
BaseClasses.py
162
BaseClasses.py
@@ -24,6 +24,13 @@ class MultiWorld():
|
||||
plando_connections: List[PlandoConnection]
|
||||
er_seeds: Dict[int, str]
|
||||
|
||||
class AttributeProxy():
|
||||
def __init__(self, rule):
|
||||
self.rule = rule
|
||||
|
||||
def __getitem__(self, player) -> bool:
|
||||
return self.rule(player)
|
||||
|
||||
def __init__(self, players: int):
|
||||
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
|
||||
|
||||
@@ -37,6 +44,7 @@ class MultiWorld():
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
self.seed = None
|
||||
self.seed_name: str = "Unavailable"
|
||||
self.precollected_items = []
|
||||
self.state = CollectionState(self)
|
||||
self._cached_entrances = None
|
||||
@@ -56,15 +64,22 @@ class MultiWorld():
|
||||
self.dynamic_regions = []
|
||||
self.dynamic_locations = []
|
||||
self.spoiler = Spoiler(self)
|
||||
self.fix_trock_doors = self.AttributeProxy(lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||
self.fix_skullwoods_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
self.fix_palaceofdarkness_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
self.fix_trock_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
|
||||
self.remote_items = self.AttributeProxy(lambda player: self.game[player] != "A Link to the Past")
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('tech_tree_layout_prerequisites', {})
|
||||
set_player_attr('_region_cache', {})
|
||||
set_player_attr('shuffle', "vanilla")
|
||||
set_player_attr('logic', "noglitches")
|
||||
set_player_attr('mode', 'open')
|
||||
set_player_attr('swords', 'random')
|
||||
set_player_attr('swordless', False)
|
||||
set_player_attr('difficulty', 'normal')
|
||||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
@@ -74,17 +89,11 @@ class MultiWorld():
|
||||
set_player_attr('retro', False)
|
||||
set_player_attr('hints', True)
|
||||
set_player_attr('player_names', [])
|
||||
set_player_attr('remote_items', False)
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
set_player_attr('powder_patch_required', False)
|
||||
set_player_attr('ganon_at_pyramid', True)
|
||||
set_player_attr('ganonstower_vanilla', True)
|
||||
set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
|
||||
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||
set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
set_player_attr('can_access_trock_eyebridge', None)
|
||||
set_player_attr('can_access_trock_front', None)
|
||||
set_player_attr('can_access_trock_big_chest', None)
|
||||
@@ -135,14 +144,12 @@ class MultiWorld():
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
set_player_attr(hk_option, False)
|
||||
|
||||
self.worlds = []
|
||||
#for i in range(players):
|
||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
||||
|
||||
@property
|
||||
def NOTCURSED(self): # not here to stay
|
||||
return {player: not cursed for player, cursed in self.CURSED.items()}
|
||||
self.custom_data = {}
|
||||
for player in range(1, players+1):
|
||||
self.custom_data[player] = {}
|
||||
# self.worlds = []
|
||||
# for i in range(players):
|
||||
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
@@ -163,6 +170,11 @@ class MultiWorld():
|
||||
def factorio_player_ids(self):
|
||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
|
||||
|
||||
@property
|
||||
def minecraft_player_ids(self):
|
||||
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
|
||||
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||
|
||||
@@ -227,7 +239,8 @@ class MultiWorld():
|
||||
ret = CollectionState(self)
|
||||
|
||||
def soft_collect(item):
|
||||
if item.name.startswith('Progressive '):
|
||||
if item.game == "A Link to the Past" and item.name.startswith('Progressive '):
|
||||
# ALttP items
|
||||
if 'Sword' in item.name:
|
||||
if ret.has('Golden Sword', item.player):
|
||||
pass
|
||||
@@ -317,7 +330,7 @@ class MultiWorld():
|
||||
if location.can_fill(self.state, item, False):
|
||||
location.item = item
|
||||
item.location = location
|
||||
item.world = self # try to not have this here anymore
|
||||
item.world = self # try to not have this here anymore
|
||||
if collect:
|
||||
self.state.collect(item, location.event, location)
|
||||
|
||||
@@ -399,24 +412,24 @@ class MultiWorld():
|
||||
if self.has_beaten_game(self.state):
|
||||
return True
|
||||
state = CollectionState(self)
|
||||
prog_locations = {location for location in self.get_locations() if location.item is not None and (
|
||||
location.item.advancement or location.event) and location not in state.locations_checked}
|
||||
prog_locations = {location for location in self.get_locations() if location.item
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
while prog_locations:
|
||||
sphere = []
|
||||
sphere = set()
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
for location in prog_locations:
|
||||
if location.can_reach(state):
|
||||
sphere.append(location)
|
||||
sphere.add(location)
|
||||
|
||||
if not sphere:
|
||||
# ran out of places and did not finish yet, quit
|
||||
return False
|
||||
|
||||
for location in sphere:
|
||||
prog_locations.remove(location)
|
||||
state.collect(location.item, True, location)
|
||||
prog_locations -= sphere
|
||||
|
||||
if self.has_beaten_game(state):
|
||||
return True
|
||||
@@ -726,7 +739,7 @@ class CollectionState(object):
|
||||
|
||||
def can_retrieve_tablet(self, player:int) -> bool:
|
||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
||||
(self.world.swords[player] == "swordless" and
|
||||
(self.world.swordless[player] and
|
||||
self.has("Hammer", player)))
|
||||
|
||||
def has_sword(self, player: int) -> bool:
|
||||
@@ -747,7 +760,7 @@ class CollectionState(object):
|
||||
def can_melt_things(self, player: int) -> bool:
|
||||
return self.has('Fire Rod', player) or \
|
||||
(self.has('Bombos', player) and
|
||||
(self.world.swords[player] == "swordless" or
|
||||
(self.world.swordless[player] or
|
||||
self.has_sword(player)))
|
||||
|
||||
def can_avoid_lasers(self, player: int) -> bool:
|
||||
@@ -800,6 +813,90 @@ class CollectionState(object):
|
||||
rules.append(self.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
|
||||
# Minecraft logic functions
|
||||
def has_iron_ingots(self, player: int):
|
||||
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
|
||||
|
||||
def has_gold_ingots(self, player: int):
|
||||
return self.has('Ingot Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
|
||||
|
||||
def has_diamond_pickaxe(self, player: int):
|
||||
return self.has('Progressive Tools', player, 3) and self.has_iron_ingots(player)
|
||||
|
||||
def craft_crossbow(self, player: int):
|
||||
return self.has('Archery', player) and self.has_iron_ingots(player)
|
||||
|
||||
def has_bottle_mc(self, player: int):
|
||||
return self.has('Bottles', player) and self.has('Ingot Crafting', player)
|
||||
|
||||
def can_enchant(self, player: int):
|
||||
return self.has('Enchanting', player) and self.has_diamond_pickaxe(player) # mine obsidian and lapis
|
||||
|
||||
def can_use_anvil(self, player: int):
|
||||
return self.has('Enchanting', player) and self.has('Resource Blocks', player) and self.has_iron_ingots(player)
|
||||
|
||||
def fortress_loot(self, player: int): # saddles, blaze rods, wither skulls
|
||||
return self.can_reach('Nether Fortress', 'Region', player) and self.basic_combat(player)
|
||||
|
||||
def can_brew_potions(self, player: int):
|
||||
return self.fortress_loot(player) and self.has('Brewing', player) and self.has_bottle_mc(player)
|
||||
|
||||
def can_piglin_trade(self, player: int):
|
||||
return self.has_gold_ingots(player) and (self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region', player))
|
||||
|
||||
def enter_stronghold(self, player: int):
|
||||
return self.fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def combat_difficulty(self, player: int):
|
||||
return self.world.combat_difficulty[player].get_option_name()
|
||||
|
||||
def can_adventure(self, player: int):
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player)
|
||||
elif self.combat_difficulty(player) == 'hard':
|
||||
return True
|
||||
return self.has('Progressive Weapons', player) and (self.has('Ingot Crafting', player) or self.has('Campfire', player))
|
||||
|
||||
def basic_combat(self, player: int):
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \
|
||||
self.has('Shield', player) and self.has_iron_ingots(player)
|
||||
elif self.combat_difficulty(player) == 'hard':
|
||||
return True
|
||||
return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self.has_iron_ingots(player)
|
||||
|
||||
def complete_raid(self, player: int):
|
||||
reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player)
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return reach_regions and \
|
||||
self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \
|
||||
self.has('Shield', player) and self.has('Archery', player) and \
|
||||
self.has('Progressive Tools', player, 2) and self.has_iron_ingots(player)
|
||||
elif self.combat_difficulty(player) == 'hard': # might be too hard?
|
||||
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
|
||||
(self.has('Progressive Armor', player) or self.has('Shield', player))
|
||||
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
|
||||
self.has('Progressive Armor', player) and self.has('Shield', player)
|
||||
|
||||
def can_kill_wither(self, player: int):
|
||||
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.fortress_loot(player) and normal_kill and self.has('Archery', player)
|
||||
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
|
||||
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 self.fortress_loot(player) and normal_kill
|
||||
|
||||
def can_kill_ender_dragon(self, player: int):
|
||||
if self.combat_difficulty(player) == 'easy':
|
||||
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.has('Archery', player) and \
|
||||
self.can_brew_potions(player) and self.can_enchant(player)
|
||||
if self.combat_difficulty(player) == 'hard':
|
||||
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
|
||||
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
|
||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
|
||||
|
||||
|
||||
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
@@ -870,7 +967,7 @@ class CollectionState(object):
|
||||
def remove(self, item):
|
||||
if item.advancement:
|
||||
to_remove = item.name
|
||||
if to_remove.startswith('Progressive '):
|
||||
if item.game == "A Link to the Past" and to_remove.startswith('Progressive '):
|
||||
if 'Sword' in to_remove:
|
||||
if self.has('Golden Sword', item.player):
|
||||
to_remove = 'Golden Sword'
|
||||
@@ -897,7 +994,7 @@ class CollectionState(object):
|
||||
elif self.has('Blue Shield', item.player):
|
||||
to_remove = 'Blue Shield'
|
||||
else:
|
||||
to_remove = 'None'
|
||||
to_remove = None
|
||||
elif 'Bow' in item.name:
|
||||
if self.has('Silver Bow', item.player):
|
||||
to_remove = 'Silver Bow'
|
||||
@@ -906,7 +1003,7 @@ class CollectionState(object):
|
||||
else:
|
||||
to_remove = None
|
||||
|
||||
if to_remove is not None:
|
||||
if to_remove:
|
||||
|
||||
self.prog_items[to_remove, item.player] -= 1
|
||||
if self.prog_items[to_remove, item.player] < 1:
|
||||
@@ -1111,7 +1208,7 @@ class Location():
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return getattr(self, "_hint_text", self.name)
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
|
||||
class Item():
|
||||
location: Optional[Location] = None
|
||||
@@ -1313,7 +1410,7 @@ class Spoiler(object):
|
||||
'dark_room_logic': self.world.dark_room_logic,
|
||||
'mode': self.world.mode,
|
||||
'retro': self.world.retro,
|
||||
'weapons': self.world.swords,
|
||||
'swordless': self.world.swordless,
|
||||
'goal': self.world.goal,
|
||||
'shuffle': self.world.shuffle,
|
||||
'item_pool': self.world.difficulty,
|
||||
@@ -1397,6 +1494,11 @@ class Spoiler(object):
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
res = getattr(self.world, hk_option)[player]
|
||||
outfile.write(f'{hk_option+":":33}{res}\n')
|
||||
if player in self.world.minecraft_player_ids:
|
||||
import Options
|
||||
for mc_option in Options.minecraft_options:
|
||||
res = getattr(self.world, mc_option)[player]
|
||||
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
||||
if player in self.world.alttp_player_ids:
|
||||
for team in range(self.world.teams):
|
||||
outfile.write('%s%s\n' % (
|
||||
@@ -1412,7 +1514,7 @@ class Spoiler(object):
|
||||
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||
outfile.write('Retro: %s\n' %
|
||||
('Yes' if self.metadata['retro'][player] else 'No'))
|
||||
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
||||
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
|
||||
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
||||
outfile.write("Pieces available for Triforce: %s\n" %
|
||||
|
||||
104
CommonClient.py
104
CommonClient.py
@@ -49,10 +49,6 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
"""List all received items"""
|
||||
logger.info('Received items:')
|
||||
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)' % (
|
||||
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
|
||||
color(self.ctx.player_names[item.player], 'yellow'),
|
||||
@@ -116,7 +112,7 @@ class CommonContext():
|
||||
self.team = None
|
||||
self.slot = None
|
||||
self.auth = None
|
||||
self.ui_node = None
|
||||
self.seed_name = None
|
||||
|
||||
self.locations_checked: typing.Set[int] = set()
|
||||
self.locations_scouted: typing.Set[int] = set()
|
||||
@@ -129,7 +125,7 @@ class CommonContext():
|
||||
self.input_requests = 0
|
||||
|
||||
# game state
|
||||
self.player_names: typing.Dict[int: str] = {}
|
||||
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
|
||||
self.exit_event = asyncio.Event()
|
||||
self.watcher_event = asyncio.Event()
|
||||
|
||||
@@ -184,7 +180,6 @@ class CommonContext():
|
||||
async def disconnect(self):
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.server.socket.close()
|
||||
self.ui_node.send_connection_status(self)
|
||||
if self.server_task is not None:
|
||||
await self.server_task
|
||||
|
||||
@@ -195,6 +190,7 @@ class CommonContext():
|
||||
|
||||
def consume_players_package(self, package: typing.List[tuple]):
|
||||
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
|
||||
self.player_names[0] = "Archipelago"
|
||||
|
||||
def event_invalid_slot(self):
|
||||
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
|
||||
@@ -213,11 +209,16 @@ class CommonContext():
|
||||
await self.disconnect()
|
||||
self.server_task = asyncio.create_task(server_loop(self, address))
|
||||
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
|
||||
|
||||
async def server_loop(ctx: CommonContext, address=None):
|
||||
ui_node = getattr(ctx, "ui_node", None)
|
||||
if ui_node:
|
||||
ui_node.send_connection_status(ctx)
|
||||
cached_address = None
|
||||
if ctx.server and ctx.server.socket:
|
||||
logger.error('Already connected')
|
||||
@@ -229,8 +230,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||
# Wait for the user to provide a multiworld server address
|
||||
if not address:
|
||||
logger.info('Please connect to an Archipelago server.')
|
||||
if ui_node:
|
||||
ui_node.poll_for_server_ip()
|
||||
return
|
||||
|
||||
address = f"ws://{address}" if "://" not in address else address
|
||||
@@ -242,8 +241,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||
ctx.server = Endpoint(socket)
|
||||
logger.info('Connected')
|
||||
ctx.server_address = address
|
||||
if ui_node:
|
||||
ui_node.send_connection_status(ctx)
|
||||
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
|
||||
async for data in ctx.server.socket:
|
||||
for msg in decode(data):
|
||||
@@ -265,8 +262,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||
await ctx.connection_closed()
|
||||
if ctx.server_address:
|
||||
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))
|
||||
ctx.current_reconnect_delay *= 2
|
||||
|
||||
@@ -284,41 +279,42 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
logger.exception(f"Could not get command from {args}")
|
||||
raise
|
||||
if cmd == 'RoomInfo':
|
||||
logger.info('--------------------------------')
|
||||
logger.info('Room Information:')
|
||||
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')
|
||||
if ctx.seed_name and ctx.seed_name != args["seed_name"]:
|
||||
logger.info("The server is running a different multiworld than your client is. (invalid seed_name)")
|
||||
else:
|
||||
args['players'].sort()
|
||||
current_team = -1
|
||||
logger.info('Players:')
|
||||
for team, slot, name in args['players']:
|
||||
if team != current_team:
|
||||
logger.info(f' Team #{team + 1}')
|
||||
current_team = team
|
||||
logger.info(' %s (Player %d)' % (name, slot))
|
||||
if args["datapackage_version"] > network_data_package["version"]:
|
||||
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
|
||||
await ctx.server_auth(args['password'])
|
||||
logger.info('--------------------------------')
|
||||
logger.info('Room Information:')
|
||||
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']}% 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':
|
||||
logger.info("Got new ID/Name Datapackage")
|
||||
@@ -356,7 +352,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
if msgs:
|
||||
await ctx.send_msgs(msgs)
|
||||
if ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": CLientStatus.CLIENT_GOAL}])
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
|
||||
# Get the server side view of missing as of time of connecting.
|
||||
# This list is used to only send to the server what is reported as ACTUALLY Missing.
|
||||
@@ -394,12 +390,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
ctx.hint_points = args['hint_points']
|
||||
|
||||
elif cmd == 'Print':
|
||||
logger.info(args["text"])
|
||||
ctx.on_print(args)
|
||||
|
||||
elif cmd == 'PrintJSON':
|
||||
if not ctx.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
logger.info(ctx.jsontotextparser(args["data"]))
|
||||
ctx.on_print_json(args)
|
||||
|
||||
elif cmd == 'InvalidArguments':
|
||||
logger.warning(f"Invalid Arguments: {args['text']}")
|
||||
|
||||
@@ -2,16 +2,18 @@ import os
|
||||
import logging
|
||||
import json
|
||||
import string
|
||||
import copy
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import colorama
|
||||
import asyncio
|
||||
from queue import Queue, Empty
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor
|
||||
from queue import Queue
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
||||
from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
import random
|
||||
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus
|
||||
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
|
||||
@@ -19,7 +21,6 @@ rcon_port = 24242
|
||||
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
|
||||
save_name = "Archipelago"
|
||||
|
||||
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
||||
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
||||
options = Utils.get_options()
|
||||
@@ -33,10 +34,12 @@ if not os.path.exists(executable):
|
||||
else:
|
||||
raise FileNotFoundError(executable)
|
||||
|
||||
script_folder = options["factorio_options"]["script-output"]
|
||||
import sys
|
||||
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
|
||||
|
||||
threadpool = ThreadPoolExecutor(10)
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
@mark_raw
|
||||
def _cmd_factorio(self, text: str) -> bool:
|
||||
@@ -48,6 +51,12 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _cmd_connect(self, address: str = "") -> bool:
|
||||
"""Connect to a MultiWorld Server"""
|
||||
if not self.ctx.auth:
|
||||
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
||||
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
||||
|
||||
|
||||
class FactorioContext(CommonContext):
|
||||
command_processor = FactorioCommandProcessor
|
||||
@@ -56,13 +65,11 @@ class FactorioContext(CommonContext):
|
||||
super(FactorioContext, self).__init__(*args, **kwargs)
|
||||
self.send_index = 0
|
||||
self.rcon_client = None
|
||||
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
if self.auth is None:
|
||||
logging.info('Enter the name of your slot to join this game:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils._version_tuple,
|
||||
@@ -70,34 +77,62 @@ class FactorioContext(CommonContext):
|
||||
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
||||
}])
|
||||
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
if self.rcon_client:
|
||||
cleaned_text = args['text'].replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
||||
|
||||
async def game_watcher(ctx: FactorioContext):
|
||||
research_logger = logging.getLogger("FactorioWatcher")
|
||||
researches_done_file = os.path.join(script_folder, "research_done.json")
|
||||
if os.path.exists(researches_done_file):
|
||||
os.remove(researches_done_file)
|
||||
from worlds.factorio.Technologies import lookup_id_to_name, tech_table
|
||||
def on_print_json(self, args: dict):
|
||||
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
|
||||
pass # don't want info on other player's local pickups.
|
||||
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
if self.rcon_client:
|
||||
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
|
||||
|
||||
async def game_watcher(ctx: FactorioContext, bridge_file: str):
|
||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
bridge_counter = 0
|
||||
try:
|
||||
while 1:
|
||||
if os.path.exists(researches_done_file):
|
||||
research_logger.info("Found Factorio Bridge file.")
|
||||
if os.path.exists(bridge_file):
|
||||
bridge_logger.info("Found Factorio Bridge file.")
|
||||
while 1:
|
||||
with open(researches_done_file) as f:
|
||||
with open(bridge_file) as f:
|
||||
data = json.load(f)
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in data if tech_name.startswith("ap-")}
|
||||
research_data = data["research_done"]
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
victory = data["victory"]
|
||||
ctx.auth = data["slot_name"]
|
||||
ctx.seed_name = data["seed_name"]
|
||||
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if ctx.locations_checked != research_data:
|
||||
research_logger.info(f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
bridge_logger.info(f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
ctx.locations_checked = research_data
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
research_logger.info("Did not find Factorio Bridge file.")
|
||||
await asyncio.sleep(5)
|
||||
bridge_counter += 1
|
||||
if bridge_counter >= 60:
|
||||
bridge_logger.info(
|
||||
"Did not find Factorio Bridge file, "
|
||||
"waiting for mod to run, which requires the server to run, "
|
||||
"which requires a player to be connected.")
|
||||
bridge_counter = 0
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
|
||||
|
||||
def stream_factorio_output(pipe, queue):
|
||||
def queuer():
|
||||
while 1:
|
||||
@@ -124,6 +159,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
factorio_queue = Queue()
|
||||
stream_factorio_output(factorio_process.stdout, factorio_queue)
|
||||
stream_factorio_output(factorio_process.stderr, factorio_queue)
|
||||
script_folder = None
|
||||
try:
|
||||
while 1:
|
||||
while not factorio_queue.empty():
|
||||
@@ -134,17 +170,28 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
# trigger lua interface confirmation
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/ap-sync")
|
||||
if not script_folder and "Write data path:" in msg:
|
||||
script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
|
||||
bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json")
|
||||
if os.path.exists(bridge_file):
|
||||
os.remove(bridge_file)
|
||||
logging.info(f"Bridge File Path: {bridge_file}")
|
||||
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
|
||||
if ctx.rcon_client:
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
item_id = ctx.items_received[ctx.send_index].item
|
||||
item_name = lookup_id_to_name[item_id]
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
|
||||
response = ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
|
||||
if response:
|
||||
factorio_server_logger.info(response)
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
item_id = transfer_item.item
|
||||
player_name = ctx.player_names[transfer_item.player]
|
||||
if item_id not in lookup_id_to_name:
|
||||
logging.error(f"Cannot send unknown item ID: {item_id}")
|
||||
else:
|
||||
item_name = lookup_id_to_name[item_id]
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
|
||||
ctx.send_index += 1
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
@@ -158,14 +205,13 @@ async def main():
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
await asyncio.sleep(3)
|
||||
watcher_task = asyncio.create_task(game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
ctx.snes_reconnect_address = None
|
||||
|
||||
await asyncio.gather(watcher_task, input_task, factorio_server_task)
|
||||
await asyncio.gather(input_task, factorio_server_task)
|
||||
|
||||
if ctx.server is not None and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
|
||||
9
Gui.py
9
Gui.py
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# module is planned to be removed
|
||||
from argparse import Namespace
|
||||
from glob import glob
|
||||
import json
|
||||
@@ -427,7 +428,6 @@ def guiMain(args=None):
|
||||
guiargs.fastmenu = rom_vars.fastMenuVar.get()
|
||||
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
||||
guiargs.skip_playthrough = not bool(createSpoilerVar.get())
|
||||
guiargs.suppress_rom = bool(suppressRomVar.get())
|
||||
guiargs.open_pyramid = openpyramidVar.get()
|
||||
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
||||
guiargs.compassshuffle = bool(compassshuffleVar.get())
|
||||
@@ -512,7 +512,7 @@ def guiMain(args=None):
|
||||
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)})
|
||||
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}")
|
||||
if guiargs.count is not None:
|
||||
seed = guiargs.seed
|
||||
@@ -1203,7 +1203,6 @@ def guiMain(args=None):
|
||||
setattr(args, k, v[1]) # only get values for player 1 for now
|
||||
# load values from commandline args
|
||||
createSpoilerVar.set(int(args.create_spoiler))
|
||||
suppressRomVar.set(int(args.suppress_rom))
|
||||
mapshuffleVar.set(args.mapshuffle)
|
||||
compassshuffleVar.set(args.compassshuffle)
|
||||
keyshuffleVar.set(args.keyshuffle)
|
||||
@@ -1745,7 +1744,8 @@ def update_sprites(task, on_finish=None):
|
||||
try:
|
||||
task.update_status("Determining needed sprites")
|
||||
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
|
||||
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
|
||||
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
|
||||
for sprite in sprites_arr if sprite["author"] != "Nintendo"]
|
||||
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
|
||||
|
||||
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
|
||||
@@ -1909,6 +1909,7 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||
|
||||
return image.zoom(2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if "update_sprites" in sys.argv:
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -3,7 +3,7 @@ MIT License
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2021 Berserker66
|
||||
Copyright (c) 2021 CaitSith2
|
||||
Copyright (c) 2020 LegendaryLinux
|
||||
Copyright (c) 2021 LegendaryLinux
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -5,6 +5,10 @@ import logging
|
||||
import textwrap
|
||||
import sys
|
||||
import time
|
||||
from tkinter import Tk
|
||||
|
||||
from Gui import update_sprites
|
||||
from GuiUtils import BackgroundTaskProgress
|
||||
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings
|
||||
from Utils import output_path
|
||||
@@ -16,46 +20,67 @@ class AdjusterWorld(object):
|
||||
self.sprite_pool = {1: sprite_pool}
|
||||
self.rom_seeds = {1: random}
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
def _get_help_string(self, action):
|
||||
return textwrap.dedent(action.help)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP rom to adjust.')
|
||||
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
|
||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?',
|
||||
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
||||
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||
help='''\
|
||||
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
|
||||
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||
help='''\
|
||||
Hide the triforce hud in certain circumstances.
|
||||
hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
|
||||
(Both can be revealed when speaking to Murahalda)
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--enableflashing', help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)', action='store_false', dest="reduceflashing")
|
||||
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
parser.add_argument('--enableflashing',
|
||||
help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)',
|
||||
action='store_false', dest="reduceflashing")
|
||||
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?',
|
||||
choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
help='''\
|
||||
Select the rate at which the heart beep sound is played at
|
||||
low health. (default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?',
|
||||
choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--link_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--shield_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--sword_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--hud_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--ow_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--link_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--shield_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--sword_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--hud_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--uw_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
@@ -64,8 +89,11 @@ def main():
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--names', default='', type=str)
|
||||
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.update_sprites:
|
||||
run_sprite_update()
|
||||
sys.exit()
|
||||
# set up logger
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
||||
args.loglevel]
|
||||
@@ -99,13 +127,13 @@ def adjust(args):
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||
palettes_options={}
|
||||
palettes_options['dungeon']=args.uw_palettes
|
||||
palettes_options = {}
|
||||
palettes_options['dungeon'] = args.uw_palettes
|
||||
|
||||
palettes_options['overworld']=args.ow_palettes
|
||||
palettes_options['hud']=args.hud_palettes
|
||||
palettes_options['sword']=args.sword_palettes
|
||||
palettes_options['shield']=args.shield_palettes
|
||||
palettes_options['overworld'] = args.ow_palettes
|
||||
palettes_options['hud'] = args.hud_palettes
|
||||
palettes_options['sword'] = args.sword_palettes
|
||||
palettes_options['shield'] = args.shield_palettes
|
||||
# palettes_options['link']=args.link_palettesvera
|
||||
|
||||
racerom = rom.read_byte(0x180213) > 0
|
||||
@@ -123,6 +151,7 @@ def adjust(args):
|
||||
|
||||
return args, path
|
||||
|
||||
|
||||
def adjustGUI():
|
||||
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \
|
||||
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
|
||||
@@ -148,6 +177,7 @@ def adjustGUI():
|
||||
def RomSelect2():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")])
|
||||
romVar2.set(rom)
|
||||
|
||||
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
|
||||
romDialogFrame.pack(side=TOP, expand=True, fill=X)
|
||||
baseRomLabel2.pack(side=LEFT)
|
||||
@@ -198,5 +228,16 @@ def adjustGUI():
|
||||
adjustWindow.mainloop()
|
||||
|
||||
|
||||
def run_sprite_update():
|
||||
import threading
|
||||
done = threading.Event()
|
||||
top = Tk()
|
||||
top.withdraw()
|
||||
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||
while not done.isSet():
|
||||
top.update()
|
||||
print("Done updating sprites")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
102
LttPClient.py
102
LttPClient.py
@@ -24,7 +24,6 @@ ModuleUpdate.update()
|
||||
import colorama
|
||||
|
||||
from NetUtils import *
|
||||
import WebUI
|
||||
|
||||
from worlds.alttp import Regions, Shops
|
||||
from worlds.alttp import Items
|
||||
@@ -45,12 +44,6 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
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
|
||||
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"""
|
||||
@@ -69,21 +62,9 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
class Context(CommonContext):
|
||||
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)
|
||||
|
||||
# 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
|
||||
self.snes_address = snes_address
|
||||
self.snes_socket = None
|
||||
@@ -495,7 +476,7 @@ async def get_snes_devices(ctx: Context):
|
||||
reply = loads(await socket.recv())
|
||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||
|
||||
ctx.ui_node.send_device_list(devices)
|
||||
|
||||
await socket.close()
|
||||
return devices
|
||||
|
||||
@@ -517,8 +498,6 @@ async def snes_connect(ctx: Context, address):
|
||||
|
||||
if len(devices) == 1:
|
||||
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:
|
||||
if ctx.snes_attached_device[1] in devices:
|
||||
device = ctx.snes_attached_device[1]
|
||||
@@ -538,7 +517,6 @@ async def snes_connect(ctx: Context, address):
|
||||
await ctx.snes_socket.send(dumps(Attach_Request))
|
||||
ctx.snes_state = SNESState.SNES_ATTACHED
|
||||
ctx.snes_attached_device = (devices.index(device), device)
|
||||
ctx.ui_node.send_connection_status(ctx)
|
||||
|
||||
if 'sd2snes' in device.lower() or 'COM' in device:
|
||||
logger.info("SD2SNES/FXPAK Detected")
|
||||
@@ -607,7 +585,6 @@ async def snes_recv_loop(ctx: Context):
|
||||
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
||||
ctx.snes_recv_queue = asyncio.Queue()
|
||||
ctx.hud_message_queue = []
|
||||
ctx.ui_node.send_connection_status(ctx)
|
||||
|
||||
ctx.rom = None
|
||||
|
||||
@@ -743,8 +720,6 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
ctx.locations_checked.add(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)})')
|
||||
ctx.ui_node.send_location_check(ctx, location)
|
||||
|
||||
|
||||
try:
|
||||
if roomid in location_shop_ids:
|
||||
@@ -887,10 +862,6 @@ async def game_watcher(ctx: Context):
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
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)' % (
|
||||
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)))
|
||||
@@ -920,57 +891,6 @@ async def run_game(romfile):
|
||||
subprocess.Popen([auto_start, romfile],
|
||||
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():
|
||||
multiprocessing.freeze_support()
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -982,8 +902,6 @@ async def main():
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--founditems', default=False, action='store_true',
|
||||
help='Show items found by other players for themselves.')
|
||||
parser.add_argument('--disable_web_ui', default=False, action='store_true',
|
||||
help="Turn off emitting a webserver for the webbrowser based user interface.")
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
if args.diff_file:
|
||||
@@ -1002,23 +920,9 @@ async def main():
|
||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||
|
||||
port = None
|
||||
if not args.disable_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")
|
||||
if not args.disable_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:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
432
Main.py
432
Main.py
@@ -7,11 +7,10 @@ import time
|
||||
import zlib
|
||||
import concurrent.futures
|
||||
import pickle
|
||||
from typing import Dict
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
||||
from worlds.alttp import ALttPLocation
|
||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups
|
||||
from worlds.alttp.Items import ItemFactory, item_name_groups
|
||||
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
|
||||
lookup_vanilla_location_to_entrance
|
||||
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
@@ -23,12 +22,14 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p
|
||||
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
||||
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
|
||||
from worlds.hk import gen_hollow, set_rules as set_hk_rules
|
||||
from worlds.hk import gen_hollow
|
||||
from worlds.hk import create_regions as hk_create_regions
|
||||
from worlds.factorio import gen_factorio, factorio_create_regions
|
||||
from worlds.factorio.Mod import generate_mod
|
||||
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
|
||||
from worlds.minecraft.Regions import minecraft_create_regions
|
||||
from worlds.generic.Rules import locality_rules
|
||||
from worlds import Games
|
||||
from worlds import Games, lookup_any_item_name_to_id
|
||||
import Patch
|
||||
|
||||
seeddigits = 20
|
||||
@@ -66,11 +67,12 @@ def main(args, seed=None):
|
||||
world.secure()
|
||||
else:
|
||||
world.random.seed(world.seed)
|
||||
world.seed_name = str(args.outputname if args.outputname else world.seed)
|
||||
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
world.mode = args.mode.copy()
|
||||
world.swords = args.swords.copy()
|
||||
world.swordless = args.swordless.copy()
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
@@ -88,7 +90,6 @@ def main(args, seed=None):
|
||||
|
||||
world.hints = args.hints.copy()
|
||||
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
@@ -135,6 +136,10 @@ def main(args, seed=None):
|
||||
import Options
|
||||
for hk_option in Options.hollow_knight_options:
|
||||
setattr(world, hk_option, getattr(args, hk_option, {}))
|
||||
for factorio_option in Options.factorio_options:
|
||||
setattr(world, factorio_option, getattr(args, factorio_option, {}))
|
||||
for minecraft_option in Options.minecraft_options:
|
||||
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
|
||||
@@ -166,15 +171,15 @@ def main(args, seed=None):
|
||||
world.player_names[player].append(name)
|
||||
|
||||
logger.info('')
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
|
||||
world.push_precollected(item)
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
item = ItemFactory(tok.strip(), player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
# enforce pre-defined local items.
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
@@ -206,12 +211,15 @@ def main(args, seed=None):
|
||||
for player in world.factorio_player_ids:
|
||||
factorio_create_regions(world, player)
|
||||
|
||||
for player in world.minecraft_player_ids:
|
||||
minecraft_create_regions(world, player)
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
if world.open_pyramid[player] == 'goal':
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
||||
elif world.open_pyramid[player] == 'auto':
|
||||
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
|
||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon)
|
||||
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
|
||||
else:
|
||||
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
|
||||
|
||||
@@ -265,6 +273,9 @@ def main(args, seed=None):
|
||||
for player in world.factorio_player_ids:
|
||||
gen_factorio(world, player)
|
||||
|
||||
for player in world.minecraft_player_ids:
|
||||
gen_minecraft(world, player)
|
||||
|
||||
logger.info("Running Item Plando")
|
||||
|
||||
for item in world.itempool:
|
||||
@@ -304,9 +315,7 @@ def main(args, seed=None):
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
logger.info('Generating output files.')
|
||||
|
||||
outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
rom_names = []
|
||||
|
||||
def _gen_rom(team: int, player: int):
|
||||
@@ -358,8 +367,7 @@ def main(args, seed=None):
|
||||
'U' if world.keyshuffle[player] == "universal" else 'S' if world.keyshuffle[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(' ', '_')}" \
|
||||
if world.player_names[player][team] != 'Player%d' % player else ''
|
||||
outfilestuffs = {
|
||||
@@ -402,128 +410,165 @@ def main(args, seed=None):
|
||||
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||
rom.write_to_file(rompath, hide_enemizer=True)
|
||||
if args.create_diff:
|
||||
Patch.create_patch_file(rompath)
|
||||
Patch.create_patch_file(rompath, player=player, player_name = world.player_names[player][team])
|
||||
return player, team, bytes(rom.name)
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor()
|
||||
multidata_task = None
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
if not args.suppress_rom:
|
||||
|
||||
rom_futures = []
|
||||
mod_futures = []
|
||||
for team in range(world.teams):
|
||||
for player in world.alttp_player_ids:
|
||||
rom_futures.append(pool.submit(_gen_rom, team, player))
|
||||
for player in world.factorio_player_ids:
|
||||
mod_futures.append(pool.submit(generate_mod, world, player))
|
||||
rom_futures = []
|
||||
mod_futures = []
|
||||
for team in range(world.teams):
|
||||
for player in world.alttp_player_ids:
|
||||
rom_futures.append(pool.submit(_gen_rom, team, player))
|
||||
for player in world.factorio_player_ids:
|
||||
mod_futures.append(pool.submit(generate_mod, world, player))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld):
|
||||
return entrance
|
||||
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
||||
return get_entrance_to_region(entrance.parent_region)
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld):
|
||||
return entrance
|
||||
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
||||
return get_entrance_to_region(entrance.parent_region)
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
from worlds.alttp.Regions import RegionType
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
# 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]}
|
||||
from worlds.alttp.Regions import RegionType
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
|
||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||
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',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
for player in range(1, world.players + 1)}
|
||||
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):
|
||||
checks_in_area[player]["Total"] = 0
|
||||
for player in range(1, world.players + 1):
|
||||
checks_in_area[player]["Total"] = 0
|
||||
|
||||
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)
|
||||
if location.game != Games.LTTP:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'}\
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
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)
|
||||
if location.game != Games.LTTP:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'}\
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]:
|
||||
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]:
|
||||
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
checks_in_area[player]["Dark World"].append(location_id)
|
||||
checks_in_area[player]["Total"] += 1
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
checks_in_area[player]["Dark World"].append(location_id)
|
||||
checks_in_area[player]["Total"] += 1
|
||||
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
precollected_items = [[] for player in range(world.players)]
|
||||
|
||||
|
||||
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:
|
||||
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)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player - 1].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):
|
||||
import base64
|
||||
for future in roms:
|
||||
rom_name = future.result()
|
||||
rom_names.append(rom_name)
|
||||
minimum_versions = {"server": (0, 0, 2)}
|
||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
||||
slot, team, rom_name in rom_names}
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
locations_data[location.player][location.address] = (location.item.code, location.item.player)
|
||||
if location.player in sending_visible_players and location.item.player != location.player:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False)
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
elif location.item.name in args.start_hints[location.item.player]:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False,
|
||||
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
|
||||
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)
|
||||
multidata = zlib.compress(pickle.dumps({"names": parsed_names,
|
||||
"connect_names": connect_names,
|
||||
"remote_items": {player for player in range(1, world.players + 1) if
|
||||
world.remote_items[player] or
|
||||
world.game[player] != "A Link to the Past"},
|
||||
"locations": {
|
||||
(location.address, location.player):
|
||||
(location.item.code, location.item.player)
|
||||
for location in world.get_filled_locations() if
|
||||
type(location.address) is int},
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"version": tuple(_version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
}), 9)
|
||||
multidata = zlib.compress(pickle.dumps({
|
||||
"slot_data" : slot_data,
|
||||
"games": games,
|
||||
"names": parsed_names,
|
||||
"connect_names": connect_names,
|
||||
"remote_items": {player for player in range(1, world.players + 1) if
|
||||
world.remote_items[player]},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"precollected_hints": precollected_hints,
|
||||
"version": tuple(_version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name
|
||||
}), 9)
|
||||
|
||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
for future in mods:
|
||||
future.result() # collect errors if they occured
|
||||
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
for future in mods:
|
||||
future.result() # collect errors if they occured
|
||||
|
||||
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 world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
@@ -532,9 +577,11 @@ def main(args, seed=None):
|
||||
if multidata_task:
|
||||
multidata_task.result() # retrieve exception if one exists
|
||||
pool.shutdown() # wait for all queued tasks to complete
|
||||
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
|
||||
generate_mc_data(world, player)
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
create_playthrough(world)
|
||||
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
@@ -542,155 +589,8 @@ def main(args, seed=None):
|
||||
return world
|
||||
|
||||
|
||||
|
||||
def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
# delete now?
|
||||
ret = MultiWorld(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.item_functionality, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
ret.teams = world.teams
|
||||
ret.player_names = copy.deepcopy(world.player_names)
|
||||
ret.remote_items = world.remote_items.copy()
|
||||
ret.required_medallions = world.required_medallions.copy()
|
||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||
ret.powder_patch_required = world.powder_patch_required.copy()
|
||||
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
|
||||
ret.treasure_hunt_count = world.treasure_hunt_count.copy()
|
||||
ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
|
||||
ret.sewer_light_cone = world.sewer_light_cone.copy()
|
||||
ret.light_world_light_cone = world.light_world_light_cone
|
||||
ret.dark_world_light_cone = world.dark_world_light_cone
|
||||
ret.seed = world.seed
|
||||
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
|
||||
ret.can_access_trock_front = world.can_access_trock_front.copy()
|
||||
ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
|
||||
ret.can_access_trock_middle = world.can_access_trock_middle.copy()
|
||||
ret.can_take_damage = world.can_take_damage
|
||||
ret.difficulty_requirements = world.difficulty_requirements.copy()
|
||||
ret.fix_fake_world = world.fix_fake_world.copy()
|
||||
ret.mapshuffle = world.mapshuffle.copy()
|
||||
ret.compassshuffle = world.compassshuffle.copy()
|
||||
ret.keyshuffle = world.keyshuffle.copy()
|
||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
||||
ret.open_pyramid = world.open_pyramid.copy()
|
||||
ret.boss_shuffle = world.boss_shuffle.copy()
|
||||
ret.enemy_shuffle = world.enemy_shuffle.copy()
|
||||
ret.enemy_health = world.enemy_health.copy()
|
||||
ret.enemy_damage = world.enemy_damage.copy()
|
||||
ret.beemizer = world.beemizer.copy()
|
||||
ret.timer = world.timer.copy()
|
||||
ret.shufflepots = world.shufflepots.copy()
|
||||
ret.shuffle_prizes = world.shuffle_prizes.copy()
|
||||
ret.shop_shuffle = world.shop_shuffle.copy()
|
||||
ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
|
||||
ret.dark_room_logic = world.dark_room_logic.copy()
|
||||
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
|
||||
ret.game = world.game.copy()
|
||||
ret.completion_condition = world.completion_condition.copy()
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(ret, player)
|
||||
else:
|
||||
create_inverted_regions(ret, player)
|
||||
create_shops(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
|
||||
for player in world.hk_player_ids:
|
||||
hk_create_regions(ret, player)
|
||||
|
||||
copy_dynamic_regions_and_locations(world, ret)
|
||||
|
||||
# copy bosses
|
||||
for dungeon in world.dungeons:
|
||||
for level, boss in dungeon.bosses.items():
|
||||
ret.get_dungeon(dungeon.name, dungeon.player).bosses[level] = boss
|
||||
|
||||
for shop in world.shops:
|
||||
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
|
||||
copied_shop.inventory = copy.copy(shop.inventory)
|
||||
|
||||
# connect copied world
|
||||
for region in world.regions:
|
||||
copied_region = ret.get_region(region.name, region.player)
|
||||
copied_region.is_light_world = region.is_light_world
|
||||
copied_region.is_dark_world = region.is_dark_world
|
||||
for exit in copied_region.exits:
|
||||
old_connection = world.get_entrance(exit.name, exit.player).connected_region
|
||||
exit.connect(ret.get_region(old_connection.name, old_connection.player))
|
||||
|
||||
# fill locations
|
||||
for location in world.get_locations():
|
||||
if location.item is not None:
|
||||
item = Item(location.item.name, location.item.advancement, location.item.code, player = location.item.player)
|
||||
ret.get_location(location.name, location.player).item = item
|
||||
item.location = ret.get_location(location.name, location.player)
|
||||
item.world = ret
|
||||
item.type = location.item.type
|
||||
item.game = location.item.game
|
||||
|
||||
if location.event:
|
||||
ret.get_location(location.name, location.player).event = True
|
||||
if location.locked:
|
||||
ret.get_location(location.name, location.player).locked = True
|
||||
|
||||
|
||||
# copy remaining itempool. No item in itempool should have an assigned location
|
||||
for old_item in world.itempool:
|
||||
item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player)
|
||||
item.type = old_item.type
|
||||
ret.itempool.append(item)
|
||||
|
||||
for old_item in world.precollected_items:
|
||||
item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player)
|
||||
item.type = old_item.type
|
||||
ret.push_precollected(item)
|
||||
|
||||
# copy progress items in state
|
||||
ret.state.prog_items = world.state.prog_items.copy()
|
||||
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
||||
|
||||
for player in world.alttp_player_ids:
|
||||
set_rules(ret, player)
|
||||
|
||||
for player in world.hk_player_ids:
|
||||
set_hk_rules(ret, player)
|
||||
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def copy_dynamic_regions_and_locations(world, ret):
|
||||
for region in world.dynamic_regions:
|
||||
new_reg = Region(region.name, region.type, region.hint_text, region.player)
|
||||
ret.regions.append(new_reg)
|
||||
ret.initialize_regions([new_reg])
|
||||
ret.dynamic_regions.append(new_reg)
|
||||
|
||||
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
|
||||
|
||||
if region.shop:
|
||||
new_reg.shop = region.shop.__class__(new_reg, region.shop.room_id, region.shop.shopkeeper_config,
|
||||
region.shop.custom, region.shop.locked, region.shop.sram_offset)
|
||||
ret.shops.append(new_reg.shop)
|
||||
|
||||
for location in world.dynamic_locations:
|
||||
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player)
|
||||
new_loc = ALttPLocation(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg)
|
||||
# todo: this is potentially dangerous. later refactor so we
|
||||
# can apply dynamic region rules on top of copied world like other rules
|
||||
new_loc.access_rule = location.access_rule
|
||||
new_loc.always_allow = location.always_allow
|
||||
new_loc.item_rule = location.item_rule
|
||||
new_reg.locations.append(new_loc)
|
||||
|
||||
ret.clear_location_cache()
|
||||
|
||||
|
||||
def create_playthrough(world):
|
||||
"""Destructive to the world it is run on."""
|
||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||
# get locations containing progress items
|
||||
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
|
||||
state_cache = [None]
|
||||
|
||||
@@ -5,6 +5,7 @@ import threading
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
|
||||
|
||||
def feedback(text: str):
|
||||
@@ -24,8 +25,7 @@ if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
|
||||
from Utils import get_public_ipv4, get_options
|
||||
|
||||
from Patch import create_patch_file
|
||||
from Mystery import get_seed_name
|
||||
|
||||
options = get_options()
|
||||
|
||||
@@ -40,6 +40,7 @@ if __name__ == "__main__":
|
||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||
zip_roms = multi_mystery_options["zip_roms"]
|
||||
zip_diffs = multi_mystery_options["zip_diffs"]
|
||||
zip_apmcs = multi_mystery_options["zip_apmcs"]
|
||||
zip_spoiler = multi_mystery_options["zip_spoiler"]
|
||||
zip_multidata = multi_mystery_options["zip_multidata"]
|
||||
zip_format = multi_mystery_options["zip_format"]
|
||||
@@ -89,10 +90,11 @@ if __name__ == "__main__":
|
||||
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
|
||||
else:
|
||||
logging.info(f"{target_player_count} Players found.")
|
||||
|
||||
seed_name = get_seed_name(random)
|
||||
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
|
||||
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
|
||||
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\""
|
||||
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\" " \
|
||||
f"--seed_name {seed_name}"
|
||||
|
||||
if create_spoiler:
|
||||
command += " --create_spoiler"
|
||||
@@ -117,15 +119,9 @@ if __name__ == "__main__":
|
||||
start = time.perf_counter()
|
||||
text = subprocess.check_output(command, shell=True).decode()
|
||||
logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
|
||||
seedname = ""
|
||||
|
||||
for segment in text.split():
|
||||
if segment.startswith("M"):
|
||||
seedname = segment
|
||||
break
|
||||
|
||||
multidataname = f"AP_{seedname}.archipelago"
|
||||
spoilername = f"AP_{seedname}_Spoiler.txt"
|
||||
multidataname = f"AP_{seed_name}.archipelago"
|
||||
spoilername = f"AP_{seed_name}_Spoiler.txt"
|
||||
romfilename = ""
|
||||
|
||||
if player_name:
|
||||
@@ -137,7 +133,7 @@ if __name__ == "__main__":
|
||||
asyncio.run(MultiClient.run_game(os.path.join(output_path, file)))
|
||||
break
|
||||
|
||||
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
|
||||
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)):
|
||||
import zipfile
|
||||
|
||||
compression = {1: zipfile.ZIP_DEFLATED,
|
||||
@@ -162,7 +158,7 @@ if __name__ == "__main__":
|
||||
logging.info(f"Removed {file} which is now present in the zipfile")
|
||||
|
||||
|
||||
zipname = os.path.join(output_path, f"AP_{seedname}.{typical_zip_ending}")
|
||||
zipname = os.path.join(output_path, f"AP_{seed_name}.{typical_zip_ending}")
|
||||
|
||||
logging.info(f"Creating zipfile {zipname}")
|
||||
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
|
||||
@@ -182,15 +178,28 @@ if __name__ == "__main__":
|
||||
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:
|
||||
futures = []
|
||||
files = os.listdir(output_path)
|
||||
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
|
||||
for file in os.listdir(output_path):
|
||||
if seedname in file:
|
||||
for file in files:
|
||||
if seed_name in file:
|
||||
if file.endswith(".sfc"):
|
||||
futures.append(pool.submit(_handle_sfc_file, file))
|
||||
elif file.endswith(".apbp"):
|
||||
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)):
|
||||
pack_file(multidataname)
|
||||
|
||||
241
MultiServer.py
241
MultiServer.py
@@ -29,9 +29,9 @@ from worlds.alttp import Items, Regions
|
||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_item_name_to_id, \
|
||||
lookup_any_location_id_to_name, lookup_any_location_name_to_id
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
_version_tuple, restricted_loads, Version
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||
|
||||
colorama.init()
|
||||
lttp_console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
|
||||
@@ -43,7 +43,7 @@ class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str] = []
|
||||
|
||||
def __init__(self, socket: websockets.server.WebSocketServerProtocol, ctx: Context):
|
||||
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
|
||||
super().__init__(socket)
|
||||
self.auth = False
|
||||
self.name = None
|
||||
@@ -78,7 +78,7 @@ class Context(Node):
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.allow_forfeits = {}
|
||||
self.remote_items = set()
|
||||
self.locations = {}
|
||||
self.locations:typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_password = server_password
|
||||
@@ -110,7 +110,14 @@ class Context(Node):
|
||||
self.auto_saver_thread = None
|
||||
self.save_dirty = False
|
||||
self.tags = ['AP']
|
||||
self.minimum_client_versions: typing.Dict[typing.Tuple[int, int], Utils.Version] = {}
|
||||
self.games = {}
|
||||
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
||||
self.seed_name = ""
|
||||
|
||||
def 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):
|
||||
with open(multidatapath, 'rb') as f:
|
||||
@@ -119,37 +126,49 @@ class Context(Node):
|
||||
self._load(self._decompress(data), use_embedded_server_options)
|
||||
self.data_filename = multidatapath
|
||||
|
||||
def _decompress(self, data: bytes) -> dict:
|
||||
@staticmethod
|
||||
def _decompress(data: bytes) -> dict:
|
||||
format_version = data[0]
|
||||
if format_version != 1:
|
||||
raise Exception("Incompatible multidata.")
|
||||
return restricted_loads(zlib.decompress(data[1:]))
|
||||
|
||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > Utils._version_tuple:
|
||||
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
|
||||
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}")
|
||||
clients_ver = decoded_obj["minimum_versions"].get("clients", [])
|
||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||
self.minimum_client_versions = {}
|
||||
for team, player, version in clients_ver:
|
||||
self.minimum_client_versions[team, player] = Utils.Version(*version)
|
||||
for player, version in clients_ver.items():
|
||||
self.minimum_client_versions[player] = Utils.Version(*version)
|
||||
|
||||
for team, names in enumerate(decoded_obj['names']):
|
||||
for player, name in enumerate(names, 1):
|
||||
self.player_names[(team, player)] = name
|
||||
|
||||
self.seed_name = decoded_obj["seed_name"]
|
||||
self.connect_names = decoded_obj['connect_names']
|
||||
self.remote_items = decoded_obj['remote_items']
|
||||
self.locations = decoded_obj['locations']
|
||||
self.slot_data = decoded_obj['slot_data']
|
||||
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
||||
for player, loc_data in decoded_obj["er_hint_data"].items()}
|
||||
self.games = decoded_obj["games"]
|
||||
# award remote-items start inventory:
|
||||
for team in range(len(decoded_obj['names'])):
|
||||
for slot, item_codes in decoded_obj["precollected_items"].items():
|
||||
if slot in self.remote_items:
|
||||
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||
for slot, hints in decoded_obj["precollected_hints"].items():
|
||||
self.hints[team, slot].update(hints)
|
||||
if use_embedded_server_options:
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
|
||||
def get_players_package(self):
|
||||
return [(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
@@ -227,42 +246,40 @@ class Context(Node):
|
||||
|
||||
def get_save(self) -> dict:
|
||||
d = {
|
||||
"rom_names": list(self.connect_names.items()),
|
||||
"received_items": tuple((k, v) for k, v in self.received_items.items()),
|
||||
"hints_used": tuple((key, value) for key, value in self.hints_used.items()),
|
||||
"hints": tuple(
|
||||
(key, list(hint.re_check(self, key[0]) for hint in value)) for key, value in self.hints.items()),
|
||||
"location_checks": tuple((key, tuple(value)) for key, value in self.location_checks.items()),
|
||||
"name_aliases": tuple((key, value) for key, value in self.name_aliases.items()),
|
||||
"client_game_state": tuple((key, value) for key, value in self.client_game_state.items()),
|
||||
"connect_names": self.connect_names,
|
||||
"received_items": self.received_items,
|
||||
"hints_used": dict(self.hints_used),
|
||||
"hints": dict(self.hints),
|
||||
"location_checks": dict(self.location_checks),
|
||||
"name_aliases": self.name_aliases,
|
||||
"client_game_state": dict(self.client_game_state),
|
||||
"client_activity_timers": tuple(
|
||||
(key, value.timestamp()) for key, value in self.client_activity_timers.items()),
|
||||
"client_connection_timers": tuple(
|
||||
(key, value.timestamp()) for key, value in self.client_connection_timers.items()),
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
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.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.name_aliases.update(savedata["name_aliases"])
|
||||
self.client_game_state.update(savedata["client_game_state"])
|
||||
self.client_connection_timers.update(
|
||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||
in savedata["client_connection_timers"]})
|
||||
self.client_activity_timers.update(
|
||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||
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 '
|
||||
f'for {len(received_items)} players')
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
|
||||
f'for {len(self.received_items)} players')
|
||||
|
||||
def get_aliased_name(self, team: int, slot: int):
|
||||
if (team, slot) in self.name_aliases:
|
||||
@@ -303,16 +320,20 @@ class Context(Node):
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||
cmd = ctx.dumper([{"cmd": "Hint", "hints" : hints}])
|
||||
commands = ctx.dumper([hint.as_network_message() for hint in hints])
|
||||
|
||||
concerns = collections.defaultdict(list)
|
||||
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):
|
||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||
|
||||
for client in ctx.endpoints:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
||||
asyncio.create_task(ctx.send_encoded_msgs(client, commands))
|
||||
client_hints = concerns[client.slot]
|
||||
if client_hints:
|
||||
asyncio.create_task(ctx.send_msgs(client, client_hints))
|
||||
|
||||
|
||||
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
|
||||
@@ -331,11 +352,14 @@ async def server(websocket, path, ctx: Context):
|
||||
ctx.endpoints.append(client)
|
||||
|
||||
try:
|
||||
logging.info("Incoming")
|
||||
if ctx.log_network:
|
||||
logging.info("Incoming connection")
|
||||
await on_client_connected(ctx, client)
|
||||
logging.info("Sent Room Info")
|
||||
if ctx.log_network:
|
||||
logging.info("Sent Room Info")
|
||||
async for data in websocket:
|
||||
logging.info(data)
|
||||
if ctx.log_network:
|
||||
logging.info(f"Incoming message: {data}")
|
||||
for msg in decode(data):
|
||||
await process_client_cmd(ctx, client, msg)
|
||||
except Exception as e:
|
||||
@@ -350,7 +374,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
await ctx.send_msgs(client, [{
|
||||
'cmd': 'RoomInfo',
|
||||
'password': ctx.password is not None,
|
||||
'players': [(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name)) for client
|
||||
'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client
|
||||
in ctx.endpoints if client.auth],
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
@@ -360,7 +384,8 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'remaining_mode': ctx.remaining_mode,
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_version': network_data_package["version"]
|
||||
'datapackage_version': network_data_package["version"],
|
||||
'seed_name': ctx.seed_name
|
||||
}])
|
||||
|
||||
|
||||
@@ -420,10 +445,6 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
|
||||
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):
|
||||
for client in ctx.endpoints:
|
||||
if client.auth: # can't send to disconnected client
|
||||
@@ -432,22 +453,22 @@ def send_new_items(ctx: Context):
|
||||
asyncio.create_task(ctx.send_msgs(client, [{
|
||||
"cmd": "ReceivedItems",
|
||||
"index": client.send_index,
|
||||
"items": tuplize_received_items(items)[client.send_index:]}]))
|
||||
"items": items[client.send_index:]}]))
|
||||
client.send_index = len(items)
|
||||
|
||||
|
||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||
# 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))
|
||||
register_location_checks(ctx, team, slot, all_locations)
|
||||
|
||||
|
||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
items = []
|
||||
for (location, location_slot) in ctx.locations:
|
||||
if location_slot == slot and location not in ctx.location_checks[team, slot]:
|
||||
items.append(ctx.locations[location, slot][0]) # item ID
|
||||
for location_id in ctx.locations[slot]:
|
||||
if location_id not in ctx.location_checks[team, slot]:
|
||||
items.append(ctx.locations[slot][location_id][0]) # item ID
|
||||
return sorted(items)
|
||||
|
||||
|
||||
@@ -456,15 +477,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
if new_locations:
|
||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
for location in new_locations:
|
||||
if (location, slot) in ctx.locations:
|
||||
item_id, target_player = ctx.locations[(location, slot)]
|
||||
if location in ctx.locations[slot]:
|
||||
item_id, target_player = ctx.locations[slot][location]
|
||||
new_item = NetworkItem(item_id, location, slot)
|
||||
if target_player != slot or slot in ctx.remote_items:
|
||||
get_received_items(ctx, team, target_player).append(new_item)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
|
||||
@@ -487,35 +508,32 @@ def notify_team(ctx: Context, team: int, text: str):
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
|
||||
hints = []
|
||||
seeked_item_id = lookup_any_item_name_to_id[item]
|
||||
for check, result in ctx.locations.items():
|
||||
item_id, receiving_player = result
|
||||
if receiving_player == slot and item_id == seeked_item_id:
|
||||
location_id, finding_player = check
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
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))
|
||||
for finding_player, check_data in ctx.locations.items():
|
||||
for location_id, result in check_data.items():
|
||||
item_id, receiving_player = result
|
||||
if receiving_player == slot and item_id == seeked_item_id:
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
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))
|
||||
|
||||
return hints
|
||||
|
||||
|
||||
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]
|
||||
for check, result in ctx.locations.items():
|
||||
location_id, finding_player = check
|
||||
if finding_player == slot and location_id == seeked_location:
|
||||
item_id, receiving_player = result
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
|
||||
break # each location has 1 item
|
||||
return hints
|
||||
|
||||
seeked_location: int = Regions.lookup_name_to_id[location]
|
||||
item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
|
||||
if item_id:
|
||||
found = seeked_location in ctx.location_checks[team, slot]
|
||||
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance)]
|
||||
return []
|
||||
|
||||
|
||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{lookup_any_item_id_to_name[hint.item]} is " \
|
||||
f"at {get_location_name_from_address(hint.location)} " \
|
||||
f"at {get_location_name_from_id(hint.location)} " \
|
||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||
|
||||
if hint.entrance:
|
||||
@@ -525,10 +543,15 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
parts = []
|
||||
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
|
||||
NetUtils.add_json_text(parts, " sent ")
|
||||
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
||||
NetUtils.add_json_text(parts, " to ")
|
||||
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
|
||||
if net_item.player == receiving_player:
|
||||
NetUtils.add_json_text(parts, " found their ")
|
||||
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
||||
else:
|
||||
NetUtils.add_json_text(parts, " sent ")
|
||||
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
||||
NetUtils.add_json_text(parts, " to ")
|
||||
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
|
||||
|
||||
NetUtils.add_json_text(parts, " (")
|
||||
NetUtils.add_json_text(parts, net_item.location, type=NetUtils.JSONTypes.location_id)
|
||||
NetUtils.add_json_text(parts, ")")
|
||||
@@ -764,7 +787,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if self.ctx.remaining_mode == "enabled":
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
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))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
@@ -777,7 +800,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
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)
|
||||
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))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
@@ -794,7 +817,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_missing_checks(self.ctx, self.client)
|
||||
|
||||
if locations:
|
||||
texts = [f'Missing: {get_item_name_from_id(location)}\n' for location in locations]
|
||||
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
|
||||
texts.append(f"Found {len(locations)} missing location checks")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
@@ -842,7 +865,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
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.")
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
@@ -863,7 +886,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
||||
else: # location 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:
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
@@ -877,8 +900,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif self.ctx.hint_cost:
|
||||
can_pay = points_available // self.ctx.hint_cost
|
||||
elif cost:
|
||||
can_pay = points_available // cost
|
||||
else:
|
||||
can_pay = 1000
|
||||
|
||||
@@ -904,7 +927,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
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)
|
||||
self.ctx.save()
|
||||
return True
|
||||
@@ -919,21 +942,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||
return [location_id for
|
||||
location_id, slot in ctx.locations if
|
||||
slot == client.slot and
|
||||
location_id in ctx.locations[client.slot] if
|
||||
location_id in ctx.location_checks[client.team, client.slot]]
|
||||
|
||||
|
||||
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||
return [location_id for
|
||||
location_id, slot in ctx.locations if
|
||||
slot == client.slot and
|
||||
location_id in ctx.locations[client.slot] if
|
||||
location_id not in ctx.location_checks[client.team, client.slot]]
|
||||
|
||||
|
||||
def get_client_points(ctx: Context, client: Client) -> int:
|
||||
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):
|
||||
@@ -967,6 +988,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
errors.add('InvalidSlot')
|
||||
else:
|
||||
team, slot = ctx.connect_names[args['name']]
|
||||
game = ctx.games[slot]
|
||||
if args['game'] != game:
|
||||
errors.add('InvalidSlot')
|
||||
# this can only ever be 0 or 1 elements
|
||||
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
|
||||
if clients:
|
||||
@@ -982,14 +1006,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
client.name = ctx.player_names[(team, slot)]
|
||||
client.team = team
|
||||
client.slot = slot
|
||||
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
|
||||
minver = ctx.minimum_client_versions[slot]
|
||||
if minver > args['version']:
|
||||
errors.add('IncompatibleVersion')
|
||||
|
||||
if ctx.compatibility == 1 and "AP" not in args['tags']:
|
||||
errors.add('IncompatibleVersion')
|
||||
#only exact version match allowed
|
||||
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||
# only exact version match allowed
|
||||
if ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||
errors.add('IncompatibleVersion')
|
||||
if errors:
|
||||
logging.info(f"A client connection was refused due to: {errors}")
|
||||
@@ -1004,10 +1026,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
"team": client.team, "slot": client.slot,
|
||||
"players": ctx.get_players_package(),
|
||||
"missing_locations": get_missing_checks(ctx, client),
|
||||
"checked_locations": get_checked_checks(ctx, client)}]
|
||||
"checked_locations": get_checked_checks(ctx, client),
|
||||
"slot_data": ctx.slot_data.get(client.slot, {})
|
||||
}]
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
|
||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
|
||||
client.send_index = len(items)
|
||||
|
||||
await ctx.send_msgs(client, reply)
|
||||
@@ -1022,7 +1046,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
if items:
|
||||
client.send_index = len(items)
|
||||
await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0,
|
||||
"items": tuplize_received_items(items)}])
|
||||
"items": items}])
|
||||
|
||||
elif cmd == 'LocationChecks':
|
||||
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
||||
@@ -1030,20 +1054,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
elif cmd == 'LocationScouts':
|
||||
locs = []
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
|
||||
return
|
||||
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
||||
target_item, target_player = ctx.locations[client.slot][location]
|
||||
locs.append(NetworkItem(target_item, location, target_player))
|
||||
|
||||
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
|
||||
item_type = [i[1] for i in Items.item_table.values() if type(i[2]) is int and i[2] == target_item]
|
||||
if item_type:
|
||||
target_item = replacements.get(item_type[0], target_item)
|
||||
|
||||
locs.append([target_item, location, target_player])
|
||||
|
||||
# logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||
|
||||
elif cmd == 'StatusUpdate':
|
||||
@@ -1189,7 +1205,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
if usable:
|
||||
for client in self.ctx.endpoints:
|
||||
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)
|
||||
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
|
||||
self.ctx.get_aliased_name(client.team, client.slot))
|
||||
@@ -1309,6 +1325,7 @@ def parse_args() -> argparse.Namespace:
|
||||
#1 -> recommended for friendly racing, tries to block third party clients
|
||||
#0 -> recommended for tournaments to force a level playing field, only allow an exact version match
|
||||
""")
|
||||
parser.add_argument('--log_network', default=defaults["log_network"], action="store_true")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
@@ -1345,7 +1362,7 @@ async def main(args: argparse.Namespace):
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility)
|
||||
|
||||
ctx.log_network = args.log_network
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
|
||||
155
Mystery.py
155
Mystery.py
@@ -54,13 +54,15 @@ def mystery_argparse():
|
||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||
parser.add_argument('--plando', default="bosses",
|
||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||
|
||||
parser.add_argument('--seed_name')
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||
return args
|
||||
|
||||
def get_seed_name(random):
|
||||
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
|
||||
|
||||
def main(args=None, callback=ERmain):
|
||||
if not args:
|
||||
@@ -68,9 +70,8 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
random.seed(seed)
|
||||
|
||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
||||
seed_name = args.seed_name if args.seed_name else get_seed_name(random)
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
|
||||
|
||||
if args.race:
|
||||
random.seed() # reset to time-based random source
|
||||
@@ -112,7 +113,7 @@ def main(args=None, callback=ERmain):
|
||||
erargs.glitch_triforce = args.glitch_triforce
|
||||
erargs.race = args.race
|
||||
erargs.skip_playthrough = args.skip_playthrough
|
||||
erargs.outputname = seedname
|
||||
erargs.outputname = seed_name
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.teams = args.teams
|
||||
|
||||
@@ -195,14 +196,18 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
pre_rolled = dict()
|
||||
pre_rolled["original_seed_number"] = seed
|
||||
pre_rolled["original_seed_name"] = seedname
|
||||
pre_rolled["original_seed_name"] = seed_name
|
||||
pre_rolled["pre_rolled"] = vars(settings).copy()
|
||||
if "plando_items" in pre_rolled["pre_rolled"]:
|
||||
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in pre_rolled["pre_rolled"]["plando_items"]]
|
||||
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in
|
||||
pre_rolled["pre_rolled"]["plando_items"]]
|
||||
if "plando_connections" in pre_rolled["pre_rolled"]:
|
||||
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in pre_rolled["pre_rolled"]["plando_connections"]]
|
||||
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in
|
||||
pre_rolled["pre_rolled"][
|
||||
"plando_connections"]]
|
||||
|
||||
with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
|
||||
with open(os.path.join(args.outputpath if args.outputpath else ".",
|
||||
f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
|
||||
yaml.dump(pre_rolled, f)
|
||||
for k, v in vars(settings).items():
|
||||
if v is not None:
|
||||
@@ -294,10 +299,14 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
name_counter[name] += 1
|
||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
|
||||
NUMBER=(name_counter[name] if name_counter[name] > 1 else ''),
|
||||
NUMBER=(name_counter[name] if name_counter[
|
||||
name] > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
return new_name.strip().replace(' ', '_')[:16]
|
||||
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]:
|
||||
@@ -315,17 +324,44 @@ available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if leve
|
||||
boss_shuffle_options = {None: 'none',
|
||||
'none': 'none',
|
||||
'basic': 'basic',
|
||||
'normal': 'normal',
|
||||
'full': 'full',
|
||||
'chaos': 'chaos',
|
||||
'singularity': 'singularity'
|
||||
}
|
||||
|
||||
goals = {
|
||||
'ganon': 'ganon',
|
||||
'crystals': 'crystals',
|
||||
'bosses': 'bosses',
|
||||
'pedestal': 'pedestal',
|
||||
'ganon_pedestal': 'ganonpedestal',
|
||||
'triforce_hunt': 'triforcehunt',
|
||||
'local_triforce_hunt': 'localtriforcehunt',
|
||||
'ganon_triforce_hunt': 'ganontriforcehunt',
|
||||
'local_ganon_triforce_hunt': 'localganontriforcehunt',
|
||||
'ice_rod_hunt': 'icerodhunt',
|
||||
}
|
||||
|
||||
# remove sometime before 1.0.0, warn before
|
||||
legacy_boss_shuffle_options = {
|
||||
# legacy, will go away:
|
||||
'simple': 'basic',
|
||||
'random': 'full',
|
||||
'normal': 'full'
|
||||
}
|
||||
|
||||
legacy_goals = {
|
||||
'dungeons': 'bosses',
|
||||
'fast_ganon': 'crystals',
|
||||
}
|
||||
|
||||
|
||||
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
|
||||
logging.debug(f'Applying {new_weights}')
|
||||
new_options = set(new_weights) - set(weights)
|
||||
@@ -337,6 +373,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
||||
f'This is probably in error.')
|
||||
return weights
|
||||
|
||||
|
||||
def roll_linked_options(weights: dict) -> dict:
|
||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||
for option_set in weights["linked_options"]:
|
||||
@@ -349,7 +386,8 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
|
||||
if "rom_options" in option_set:
|
||||
rom_weights = weights.get("rom", dict())
|
||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", option_set["name"])
|
||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
|
||||
option_set["name"])
|
||||
weights["rom"] = rom_weights
|
||||
else:
|
||||
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||
@@ -358,10 +396,11 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
f"Please fix your linked option.") from e
|
||||
return weights
|
||||
|
||||
|
||||
def roll_triggers(weights: dict) -> dict:
|
||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
|
||||
for option_set in weights["triggers"]:
|
||||
for i, option_set in enumerate(weights["triggers"]):
|
||||
try:
|
||||
key = get_choice("option_name", option_set)
|
||||
if key not in weights:
|
||||
@@ -373,18 +412,25 @@ def roll_triggers(weights: dict) -> dict:
|
||||
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
||||
if "options" in option_set:
|
||||
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
|
||||
|
||||
if "rom_options" in option_set:
|
||||
rom_weights = weights.get("rom", dict())
|
||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom", option_set["option_name"])
|
||||
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
|
||||
option_set["option_name"])
|
||||
weights["rom"] = rom_weights
|
||||
weights[key] = result
|
||||
except Exception as e:
|
||||
raise ValueError(f"A trigger is destroyed. "
|
||||
raise ValueError(f"Your trigger number {i+1} is destroyed. "
|
||||
f"Please fix your triggers.") from e
|
||||
return weights
|
||||
|
||||
|
||||
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
||||
if boss_shuffle in legacy_boss_shuffle_options:
|
||||
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
||||
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
|
||||
f"please use {new_boss_shuffle} instead")
|
||||
return new_boss_shuffle
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
elif "bosses" in plando_options:
|
||||
@@ -392,6 +438,10 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
for boss in options:
|
||||
if boss in legacy_boss_shuffle_options:
|
||||
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
|
||||
logging.warning(f"Boss shuffle {boss} is deprecated, "
|
||||
f"please use {remainder_shuffle} instead")
|
||||
if boss in boss_shuffle_options:
|
||||
remainder_shuffle = boss_shuffle_options[boss]
|
||||
elif "-" in boss:
|
||||
@@ -419,7 +469,7 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses", ))):
|
||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
|
||||
if "pre_rolled" in weights:
|
||||
pre_rolled = weights["pre_rolled"]
|
||||
|
||||
@@ -435,7 +485,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
if "plando_connections" in pre_rolled:
|
||||
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
|
||||
connection["exit"],
|
||||
connection["direction"]) for connection in pre_rolled["plando_connections"]]
|
||||
connection["direction"]) for connection in
|
||||
pre_rolled["plando_connections"]]
|
||||
if "connections" not in plando_options and pre_rolled["plando_connections"]:
|
||||
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
|
||||
|
||||
@@ -480,17 +531,44 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
if isinstance(itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
ret.startinventory = startitems
|
||||
ret.start_hints = set(weights.get('start_hints', []))
|
||||
|
||||
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, weights, plando_options)
|
||||
elif ret.game == "Hollow Knight":
|
||||
for option_name, option in Options.hollow_knight_options.items():
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
|
||||
elif ret.game == "Factorio":
|
||||
pass
|
||||
for option_name, option in Options.factorio_options.items():
|
||||
if option_name in weights:
|
||||
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
|
||||
setattr(ret, option_name, option.from_any(weights[option_name]))
|
||||
else:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||
else:
|
||||
setattr(ret, option_name, option(option.default))
|
||||
elif ret.game == "Minecraft":
|
||||
for option_name, option in Options.minecraft_options.items():
|
||||
if option_name in weights:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
|
||||
else:
|
||||
setattr(ret, option_name, option(option.default))
|
||||
else:
|
||||
raise Exception(f"Unsupported game {ret.game}")
|
||||
return ret
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
|
||||
@@ -533,17 +611,11 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goal = get_choice('goals', weights, 'ganon')
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'crystals': 'crystals',
|
||||
'bosses': 'bosses',
|
||||
'pedestal': 'pedestal',
|
||||
'ganon_pedestal': 'ganonpedestal',
|
||||
'triforce_hunt': 'triforcehunt',
|
||||
'local_triforce_hunt': 'localtriforcehunt',
|
||||
'ganon_triforce_hunt': 'ganontriforcehunt',
|
||||
'local_ganon_triforce_hunt': 'localganontriforcehunt',
|
||||
'ice_rod_hunt': 'icerodhunt'
|
||||
}[goal]
|
||||
|
||||
if goal in legacy_goals:
|
||||
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
|
||||
goal = legacy_goals[goal]
|
||||
ret.goal = goals[goal]
|
||||
|
||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||
# fast ganon + ganon at hole
|
||||
@@ -587,11 +659,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.hints = get_choice('hints', weights)
|
||||
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[get_choice('weapons', weights, 'assured')]
|
||||
ret.swordless = get_choice('swordless', weights, False)
|
||||
|
||||
ret.difficulty = get_choice('item_pool', weights)
|
||||
|
||||
@@ -602,6 +670,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
||||
|
||||
|
||||
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
||||
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
||||
@@ -647,23 +716,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if not ret.required_medallions[index]:
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
if item.startswith(('Progressive ', 'Small Key ', 'Rupee', 'Piece of Heart', 'Boss Heart Container',
|
||||
'Sanctuary Heart Container', 'Arrow', 'Bombs ', 'Bomb ', 'Bottle')) and isinstance(
|
||||
itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights, True)
|
||||
|
||||
ret.remote_items = get_choice('remote_items', weights, False)
|
||||
|
||||
if get_choice("local_keys", weights, "l" in dungeon_items):
|
||||
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
|
||||
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
|
||||
@@ -772,5 +826,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.quickswap = True
|
||||
ret.sprite = "Link"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
36
NetUtils.py
36
NetUtils.py
@@ -9,6 +9,7 @@ import websockets
|
||||
|
||||
from Utils import Version
|
||||
|
||||
|
||||
class JSONMessagePart(typing.TypedDict, total=False):
|
||||
text: str
|
||||
# optional
|
||||
@@ -18,7 +19,6 @@ class JSONMessagePart(typing.TypedDict, total=False):
|
||||
found: bool
|
||||
|
||||
|
||||
|
||||
class ClientStatus(enum.IntEnum):
|
||||
CLIENT_UNKNOWN = 0
|
||||
CLIENT_CONNECTED = 5
|
||||
@@ -61,10 +61,12 @@ _encode = JSONEncoder(
|
||||
def encode(obj):
|
||||
return _encode(_scan_for_TypedTuples(obj))
|
||||
|
||||
|
||||
def get_any_version(data: dict) -> Version:
|
||||
data = {key.lower(): value for key, value in data.items()} # .NET version classes have capitalized keys
|
||||
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
|
||||
|
||||
|
||||
whitelist = {"NetworkPlayer": NetworkPlayer,
|
||||
"NetworkItem": NetworkItem,
|
||||
}
|
||||
@@ -73,6 +75,7 @@ custom_hooks = {
|
||||
"Version": get_any_version
|
||||
}
|
||||
|
||||
|
||||
def _object_hook(o: typing.Any) -> typing.Any:
|
||||
if isinstance(o, dict):
|
||||
hook = custom_hooks.get(o.get("class", None), None)
|
||||
@@ -82,7 +85,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
|
||||
if cls:
|
||||
for key in tuple(o):
|
||||
if key not in cls._fields:
|
||||
del(o[key])
|
||||
del (o[key])
|
||||
return cls(**o)
|
||||
|
||||
return o
|
||||
@@ -99,6 +102,7 @@ class Node:
|
||||
def __init__(self):
|
||||
self.endpoints = []
|
||||
super(Node, self).__init__()
|
||||
self.log_network = 0
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
@@ -114,6 +118,9 @@ class Node:
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
|
||||
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
|
||||
@@ -123,6 +130,9 @@ class Node:
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
@@ -144,11 +154,16 @@ class HandlerMeta(type):
|
||||
handlers = attrs["handlers"] = {}
|
||||
trigger: str = "_handle_"
|
||||
for base in bases:
|
||||
handlers.update(base.commands)
|
||||
handlers.update(base.handlers)
|
||||
handlers.update({handler_name[len(trigger):]: method for handler_name, method in attrs.items() if
|
||||
handler_name.startswith(trigger)})
|
||||
|
||||
orig_init = attrs.get('__init__', None)
|
||||
if not orig_init:
|
||||
for base in bases:
|
||||
orig_init = getattr(base, '__init__', None)
|
||||
if orig_init:
|
||||
break
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# turn functions into bound methods
|
||||
@@ -160,6 +175,7 @@ class HandlerMeta(type):
|
||||
attrs['__init__'] = __init__
|
||||
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class JSONTypes(str, enum.Enum):
|
||||
color = "color"
|
||||
text = "text"
|
||||
@@ -171,6 +187,7 @@ class JSONTypes(str, enum.Enum):
|
||||
location_id = "location_id"
|
||||
entrance_name = "entrance_name"
|
||||
|
||||
|
||||
class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
@@ -229,6 +246,11 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
return self._handle_color(node)
|
||||
|
||||
|
||||
class RawJSONtoTextParser(JSONtoTextParser):
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
return self._handle_text(node)
|
||||
|
||||
|
||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||
@@ -274,7 +296,7 @@ class Hint(typing.NamedTuple):
|
||||
add_json_text(parts, " is at ")
|
||||
add_json_text(parts, self.location, type="location_id")
|
||||
add_json_text(parts, " in ")
|
||||
add_json_text(parts, self.finding_player, type ="player_id")
|
||||
add_json_text(parts, self.finding_player, type="player_id")
|
||||
if self.entrance:
|
||||
add_json_text(parts, "'s World at ")
|
||||
add_json_text(parts, self.entrance, type="entrance_name")
|
||||
@@ -285,4 +307,8 @@ class Hint(typing.NamedTuple):
|
||||
else:
|
||||
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
|
||||
|
||||
133
Options.py
133
Options.py
@@ -3,7 +3,7 @@ import typing
|
||||
|
||||
|
||||
class AssembleOptions(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
options = attrs["options"] = {}
|
||||
name_lookup = attrs["name_lookup"] = {}
|
||||
for base in bases:
|
||||
@@ -17,12 +17,13 @@ class AssembleOptions(type):
|
||||
# apply aliases, without name_lookup
|
||||
options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("alias_")})
|
||||
return super(AssembleOptions, cls).__new__(cls, name, bases, attrs)
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class Option(metaclass=AssembleOptions):
|
||||
value: int
|
||||
name_lookup: typing.Dict[int, str]
|
||||
default = 0
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.get_option_name()})"
|
||||
@@ -47,6 +48,7 @@ class Option(metaclass=AssembleOptions):
|
||||
class Toggle(Option):
|
||||
option_false = 0
|
||||
option_true = 1
|
||||
default = 0
|
||||
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
@@ -86,6 +88,7 @@ class Toggle(Option):
|
||||
def get_option_name(self):
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class Choice(Option):
|
||||
def __init__(self, value: int):
|
||||
self.value: int = value
|
||||
@@ -100,8 +103,41 @@ class Choice(Option):
|
||||
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
return cls.from_text(data)
|
||||
def from_any(cls, data: typing.Any) -> Choice:
|
||||
if type(data) == int and data in cls.options.values():
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
|
||||
class OptionNameSet(Option):
|
||||
default = frozenset()
|
||||
|
||||
def __init__(self, value: typing.Set[str]):
|
||||
self.value: typing.Set[str] = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> OptionNameSet:
|
||||
return cls({option.strip() for option in text.split(",")})
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> OptionNameSet:
|
||||
if type(data) == set:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
|
||||
class OptionDict(Option):
|
||||
default = {}
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
self.value: typing.Dict[str, typing.Any] = value
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||
if type(data) == dict:
|
||||
return cls(data)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -231,7 +267,94 @@ hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
|
||||
"SHADESKIPS": Toggle,
|
||||
}
|
||||
|
||||
hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options, **hollow_knight_skip_options}
|
||||
hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options,
|
||||
**hollow_knight_skip_options}
|
||||
|
||||
|
||||
class MaxSciencePack(Choice):
|
||||
option_automation_science_pack = 0
|
||||
option_logistic_science_pack = 1
|
||||
option_military_science_pack = 2
|
||||
option_chemical_science_pack = 3
|
||||
option_production_science_pack = 4
|
||||
option_utility_science_pack = 5
|
||||
option_space_science_pack = 6
|
||||
default = 6
|
||||
|
||||
def get_allowed_packs(self):
|
||||
return {option.replace("_", "-") for option, value in self.options.items()
|
||||
if value <= self.value}
|
||||
|
||||
|
||||
class TechCost(Choice):
|
||||
option_very_easy = 0
|
||||
option_easy = 1
|
||||
option_kind = 2
|
||||
option_normal = 3
|
||||
option_hard = 4
|
||||
option_very_hard = 5
|
||||
option_insane = 6
|
||||
default = 3
|
||||
|
||||
|
||||
class FreeSamples(Choice):
|
||||
option_none = 0
|
||||
option_single_craft = 1
|
||||
option_half_stack = 2
|
||||
option_stack = 3
|
||||
default = 3
|
||||
|
||||
|
||||
class TechTreeLayout(Choice):
|
||||
option_single = 0
|
||||
option_small_diamonds = 1
|
||||
option_medium_diamonds = 2
|
||||
option_pyramid = 3
|
||||
option_funnel = 4
|
||||
default = 0
|
||||
|
||||
|
||||
class Visibility(Choice):
|
||||
option_none = 0
|
||||
option_sending = 1
|
||||
default = 1
|
||||
|
||||
|
||||
class FactorioStartItems(OptionDict):
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"tech_cost": TechCost,
|
||||
"free_samples": FreeSamples,
|
||||
"visibility": Visibility,
|
||||
"random_tech_ingredients": Toggle,
|
||||
"starting_items": FactorioStartItems}
|
||||
|
||||
|
||||
class AdvancementGoal(Choice):
|
||||
option_few = 0
|
||||
option_normal = 1
|
||||
option_many = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class CombatDifficulty(Choice):
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
default = 1
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
"include_hard_advancements": Toggle,
|
||||
"include_insane_advancements": Toggle,
|
||||
"include_postgame_advancements": Toggle,
|
||||
"shuffle_structures": Toggle
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
15
Patch.py
15
Patch.py
@@ -12,7 +12,7 @@ from typing import Tuple, Optional
|
||||
import Utils
|
||||
from worlds.alttp.Rom import JAP10HASH
|
||||
|
||||
current_patch_version = 1
|
||||
current_patch_version = 2
|
||||
|
||||
|
||||
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:
|
||||
patch = yaml.dump({"meta": metadata,
|
||||
"patch": patch,
|
||||
"game": "alttp",
|
||||
"compatible_version": 1,
|
||||
"game": "A Link to the Past",
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 1,
|
||||
"version": current_patch_version,
|
||||
"base_checksum": JAP10HASH})
|
||||
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)
|
||||
|
||||
|
||||
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),
|
||||
{
|
||||
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
meta)
|
||||
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
|
||||
write_lzma(bytes, target)
|
||||
return target
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# [Archipelago](https://archipelago.gg)  | [Install](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
# [Archipelago](https://archipelago.gg)  | [Install](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself.
|
||||
|
||||
Currently, the following games are supported:
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
||||
Downloads can be found at [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases), including compiled
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
windows binaries.
|
||||
|
||||
## History
|
||||
@@ -25,7 +27,7 @@ We recognize that there is a strong community of incredibly smart people that ha
|
||||
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
||||
|
||||
## Running Archipelago
|
||||
For most people all you need to do is head over to the [releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page then download and run the appropriate installer. The installers function on Windows only.
|
||||
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
|
||||
|
||||
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).
|
||||
|
||||
|
||||
25
Utils.py
25
Utils.py
@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
|
||||
minor: int
|
||||
build: int
|
||||
|
||||
__version__ = "0.0.2"
|
||||
__version__ = "0.1.1"
|
||||
_version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
@@ -84,9 +84,13 @@ def local_path(*path):
|
||||
# cx_Freeze
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||
else:
|
||||
# we are running in a normal Python environment
|
||||
import __main__
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
||||
if hasattr(__main__, "__file__"):
|
||||
# we are running in a normal Python environment
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
||||
else:
|
||||
# pray
|
||||
local_path.cached_path = os.path.abspath(".")
|
||||
|
||||
return os.path.join(local_path.cached_path, *path)
|
||||
|
||||
@@ -166,7 +170,6 @@ def get_default_options() -> dict:
|
||||
},
|
||||
"factorio_options": {
|
||||
"executable": "factorio\\bin\\x64\\factorio",
|
||||
"script-output": "factorio\\script-output",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
@@ -190,6 +193,7 @@ def get_default_options() -> dict:
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
"log_network": 0
|
||||
},
|
||||
"multi_mystery_options": {
|
||||
"teams": 1,
|
||||
@@ -267,14 +271,14 @@ def get_options() -> dict:
|
||||
return get_options.options
|
||||
|
||||
|
||||
def get_item_name_from_id(code):
|
||||
def get_item_name_from_id(code: int) -> str:
|
||||
from worlds import lookup_any_item_id_to_name
|
||||
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
|
||||
|
||||
|
||||
def get_location_name_from_address(address):
|
||||
def get_location_name_from_id(code: int) -> str:
|
||||
from worlds import lookup_any_location_id_to_name
|
||||
return lookup_any_location_id_to_name.get(address, f'Unknown location (ID:{address})')
|
||||
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
|
||||
|
||||
|
||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||
@@ -385,9 +389,14 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
def find_class(self, module, name):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus"}:
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
||||
import NetUtils
|
||||
return getattr(NetUtils, name)
|
||||
if module == "Options":
|
||||
import Options
|
||||
obj = getattr(Options, name)
|
||||
if issubclass(obj, Options.Option):
|
||||
return obj
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
(module, name))
|
||||
|
||||
@@ -8,16 +8,14 @@ from waitress import serve
|
||||
from WebHostLib.models import db
|
||||
from WebHostLib.autolauncher import autohost
|
||||
|
||||
configpath = "config.yaml"
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
|
||||
|
||||
def get_app():
|
||||
app = raw_app
|
||||
if os.path.exists(configpath):
|
||||
import yaml
|
||||
with open(configpath) as c:
|
||||
app.config.update(yaml.safe_load(c))
|
||||
|
||||
app.config.from_file(configpath, yaml.safe_load)
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
db.bind(**app.config["PONY"])
|
||||
db.generate_mapping(create_tables=True)
|
||||
|
||||
@@ -6,7 +6,6 @@ import socket
|
||||
from pony.flask import Pony
|
||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||
from flask_caching import Cache
|
||||
from flaskext.autoversion import Autoversion
|
||||
from flask_compress import Compress
|
||||
|
||||
from .models import *
|
||||
@@ -46,10 +45,8 @@ app.config["PONY"] = {
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "simple"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||
|
||||
app.autoversion = True
|
||||
|
||||
av = Autoversion(app)
|
||||
cache = Cache(app)
|
||||
Compress(app)
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import pickle
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from . import api_endpoints
|
||||
@@ -46,7 +48,7 @@ def generate_api():
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
|
||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
||||
@@ -58,6 +60,7 @@ def generate_api():
|
||||
return {"text": "Uncaught Exception:" + str(e)}, 500
|
||||
|
||||
|
||||
|
||||
@api_endpoints.route('/status/<suuid:seed>')
|
||||
def wait_seed_api(seed: UUID):
|
||||
seed_id = seed
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import json
|
||||
import multiprocessing
|
||||
from datetime import timedelta, datetime
|
||||
import concurrent.futures
|
||||
@@ -9,6 +10,8 @@ import time
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
|
||||
from Utils import restricted_loads
|
||||
|
||||
|
||||
class CommonLocker():
|
||||
"""Uses a file lock to signal that something is already running"""
|
||||
@@ -25,6 +28,7 @@ class AlreadyRunningException(Exception):
|
||||
if sys.platform == 'win32':
|
||||
import os
|
||||
|
||||
|
||||
class Locker(CommonLocker):
|
||||
def __enter__(self):
|
||||
try:
|
||||
@@ -43,6 +47,7 @@ if sys.platform == 'win32':
|
||||
else: # unix
|
||||
import fcntl
|
||||
|
||||
|
||||
class Locker(CommonLocker):
|
||||
def __enter__(self):
|
||||
try:
|
||||
@@ -78,14 +83,21 @@ def handle_generation_failure(result: BaseException):
|
||||
|
||||
|
||||
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
options = generation.options
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
|
||||
meta = generation.meta
|
||||
pool.apply_async(gen_game, (options,),
|
||||
{"race": meta["race"], "sid": generation.id, "owner": generation.owner},
|
||||
handle_generation_success, handle_generation_failure)
|
||||
generation.state = STATE_STARTED
|
||||
try:
|
||||
meta = json.loads(generation.meta)
|
||||
options = restricted_loads(generation.options)
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
pool.apply_async(gen_game, (options,),
|
||||
{"race": meta["race"],
|
||||
"sid": generation.id,
|
||||
"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):
|
||||
@@ -138,6 +150,7 @@ multiworlds = {}
|
||||
|
||||
guardians = concurrent.futures.ThreadPoolExecutor(2, thread_name_prefix="Guardian")
|
||||
|
||||
|
||||
class MultiworldInstance():
|
||||
def __init__(self, room: Room, config: dict):
|
||||
self.room_id = room.id
|
||||
@@ -162,7 +175,7 @@ class MultiworldInstance():
|
||||
self.process = None
|
||||
|
||||
def _collect(self):
|
||||
self.process.join() # wait for process to finish
|
||||
self.process.join() # wait for process to finish
|
||||
self.process = None
|
||||
self.guardian = None
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import zlib
|
||||
import pickle
|
||||
|
||||
|
||||
from .models import *
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||
from Utils import get_public_ipv4, get_public_ipv6, parse_yaml
|
||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
|
||||
|
||||
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
@@ -27,7 +27,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
self.ctx.save()
|
||||
self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}")
|
||||
return True
|
||||
elif platform.lower().startswith("y"): # youtube
|
||||
elif platform.lower().startswith("y"): # youtube
|
||||
self.ctx.video[self.client.team, self.client.slot] = "Youtube", user
|
||||
self.ctx.save()
|
||||
self.output(f"Registered Youtube Stream for {user}")
|
||||
@@ -81,16 +81,16 @@ class WebHostContext(Context):
|
||||
def init_save(self, enabled: bool = True):
|
||||
self.saving = enabled
|
||||
if self.saving:
|
||||
existing_savegame = Room.get(id=self.room_id).multisave
|
||||
if existing_savegame:
|
||||
self.set_save(existing_savegame)
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self._start_async_saving()
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
|
||||
@db_session
|
||||
def _save(self, exit_save:bool = False) -> bool:
|
||||
room = Room.get(id=self.room_id)
|
||||
room.multisave = self.get_save()
|
||||
room.multisave = pickle.dumps(self.get_save())
|
||||
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
||||
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
||||
room.last_activity = datetime.utcnow()
|
||||
|
||||
@@ -2,12 +2,12 @@ from flask import send_file, Response
|
||||
from pony.orm import select
|
||||
|
||||
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>")
|
||||
def download_patch(room_id, patch_id):
|
||||
patch = Patch.get(id=patch_id)
|
||||
patch = Slot.get(id=patch_id)
|
||||
if not patch:
|
||||
return "Patch not found"
|
||||
else:
|
||||
@@ -16,7 +16,7 @@ def download_patch(room_id, patch_id):
|
||||
room = Room.get(id=room_id)
|
||||
last_port = room.last_port
|
||||
|
||||
patch_data = update_patch_data(patch.data, server=f"{app.config['HOSTNAME']}:{last_port}")
|
||||
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||
patch_data = io.BytesIO(patch_data)
|
||||
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"
|
||||
@@ -30,8 +30,9 @@ def download_spoiler(seed_id):
|
||||
|
||||
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
|
||||
def download_raw_patch(seed_id, player_id: int):
|
||||
patch = select(patch for patch in Patch if
|
||||
patch.player_id == player_id and patch.seed.id == seed_id).first()
|
||||
seed = Seed.get(id=seed_id)
|
||||
patch = select(patch for patch in seed.slots if
|
||||
patch.player_id == player_id).first()
|
||||
|
||||
if not patch:
|
||||
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"
|
||||
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 tempfile
|
||||
import random
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
@@ -39,7 +40,7 @@ def generate(race=False):
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
|
||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
@@ -79,7 +80,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = target.name
|
||||
erargs.teams = 1
|
||||
erargs.progression_balancing = {}
|
||||
erargs.create_diff = True
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
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)
|
||||
if gen is not None:
|
||||
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
|
||||
|
||||
|
||||
@@ -122,12 +122,12 @@ def wait_seed(seed: UUID):
|
||||
if not generation:
|
||||
return "Generation not found."
|
||||
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)
|
||||
|
||||
|
||||
def upload_to_db(folder, owner, sid, race:bool):
|
||||
patches = set()
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
|
||||
multidata = None
|
||||
@@ -137,8 +137,8 @@ def upload_to_db(folder, owner, sid, race:bool):
|
||||
player_text = file.split("_P", 1)[1]
|
||||
player_name = player_text.split("_", 1)[1].split(".", 1)[0]
|
||||
player_id = int(player_text.split(".", 1)[0].split("_", 1)[0])
|
||||
patches.add(Patch(data=open(file, "rb").read(),
|
||||
player_id=player_id, player_name = player_name))
|
||||
slots.add(Slot(data=open(file, "rb").read(),
|
||||
player_id=player_id, player_name = player_name, game = "A Link to the Past"))
|
||||
elif file.endswith(".txt"):
|
||||
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
||||
elif file.endswith(".archipelago"):
|
||||
@@ -146,12 +146,12 @@ def upload_to_db(folder, owner, sid, race:bool):
|
||||
if multidata:
|
||||
with db_session:
|
||||
if sid:
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner,
|
||||
id=sid, meta={"tags": ["generated"]})
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
|
||||
id=sid, meta=json.dumps({"race": race, "tags": ["generated"]}))
|
||||
else:
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner,
|
||||
meta={"tags": ["generated"]})
|
||||
for patch in patches:
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
|
||||
meta=json.dumps({"race": race, "tags": ["generated"]}))
|
||||
for patch in slots:
|
||||
patch.seed = seed
|
||||
if sid:
|
||||
gen = Generation.get(id=sid)
|
||||
|
||||
@@ -9,12 +9,13 @@ STATE_STARTED = 1
|
||||
STATE_ERROR = -1
|
||||
|
||||
|
||||
class Patch(db.Entity):
|
||||
class Slot(db.Entity):
|
||||
id = PrimaryKey(int, auto=True)
|
||||
player_id = Required(int)
|
||||
player_name = Required(str, 16)
|
||||
data = Required(bytes, lazy=True)
|
||||
data = Optional(bytes, lazy=True)
|
||||
seed = Optional('Seed')
|
||||
game = Required(str)
|
||||
|
||||
|
||||
class Room(db.Entity):
|
||||
@@ -37,9 +38,9 @@ class Seed(db.Entity):
|
||||
multidata = Required(bytes, lazy=True)
|
||||
owner = Required(UUID, index=True)
|
||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||
patches = Set(Patch)
|
||||
slots = Set(Slot)
|
||||
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):
|
||||
@@ -51,6 +52,6 @@ class Command(db.Entity):
|
||||
class Generation(db.Entity):
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
owner = Required(UUID)
|
||||
options = Required(Json, lazy=True)
|
||||
meta = Required(Json, lazy=True)
|
||||
options = Required(buffer, lazy=True)
|
||||
meta = Required(str, default=lambda: "{\"race\": false}")
|
||||
state = Required(int, default=0, index=True)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
flask>=1.1.2
|
||||
flask>=2.0.0
|
||||
pony>=0.7.14
|
||||
waitress>=2.0.0
|
||||
flask-caching>=1.10.1
|
||||
Flask-Autoversion>=0.2.0
|
||||
Flask-Compress>=1.9.0
|
||||
Flask-Limiter>=1.4
|
||||
|
||||
@@ -170,6 +170,12 @@ const generateGame = (raceMode = false) => {
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
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
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.
|
||||
|
||||
136
WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md
Normal file
136
WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Minecraft Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
### Server Host
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Players
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Dedicated Server Setup
|
||||
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
|
||||
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
|
||||
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
|
||||
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
|
||||
|
||||
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
|
||||
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
|
||||
- This will create the appropriate directories for you to place the files in the following step.
|
||||
|
||||
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
|
||||
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!
|
||||
|
||||
### Basic Player Setup
|
||||
- Purchase and install Minecraft from the above link.
|
||||
|
||||
**You're Done**.
|
||||
|
||||
Players only need to have a Vanilla unmodified version of Minecraft to play!
|
||||
|
||||
### Advanced Player Setup
|
||||
***This is not required to play a randomized minecraft game.***
|
||||
however this recommended as it helps make the experience more enjoyable.
|
||||
|
||||
#### Recomended Mods
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Install and run Minecraft from the link above at least once.
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install client**.
|
||||
- Start Minecraft forge at least once to create the directories needed for the next steps.
|
||||
3. Navigate to your minecraft install directory and place desired mods `.jar` file the in the `mods` directory.
|
||||
- The default install directories are as follows.
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how
|
||||
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
|
||||
each player to enjoy an experience customized for their taste, and different players in the same multiworld
|
||||
can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
A basic minecraft yaml will look like this.
|
||||
```yaml
|
||||
description: Template Name
|
||||
# Your name in-game. Spaces will be replaced with underscores and
|
||||
# there is a 16 character limit
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
|
||||
# Shared Options supported by all games:
|
||||
accessibility: locations
|
||||
progression_balancing: on
|
||||
# Minecraft Specific Options
|
||||
|
||||
# Number of advancements required (out of 92 total) to spawn the
|
||||
# Ender Dragon and complete the game.
|
||||
advancement_goal:
|
||||
few: 0 #30
|
||||
normal: 1 #50
|
||||
many: 0 #70
|
||||
|
||||
# Modifies the level of items logically required for exploring
|
||||
# dangerous areas and fighting bosses.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
|
||||
# Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Junk-fills extremely difficult advancements;
|
||||
# this is only How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Some advancements require defeating the Ender Dragon;
|
||||
# this will junk-fill them so you won't have to finish to send some items.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
#enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
shuffle_structures:
|
||||
on: 1
|
||||
off: 0
|
||||
```
|
||||
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Minecraft data file
|
||||
**Only one yaml file needs to be submitted per minecraft world regardless of how many players play on it.**
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your data file, or with a zip file containing
|
||||
everyone's data files. Your data file should have a `.apmc` extension.
|
||||
|
||||
Put your data file in your forge servers `APData` folder. Make sure to remove any previous data file that was in there
|
||||
previously.
|
||||
|
||||
### Connect to the MultiServer
|
||||
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
|
||||
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
|
||||
|
||||
Once in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
|
||||
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.
|
||||
|
||||
### Play the game
|
||||
When the console tells you that you have joined the room, you're ready to begin playing. Congratulations
|
||||
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
|
||||
forge server.
|
||||
|
||||
121
WebHostLib/static/assets/tutorial/minecraft/minecraft_es.md
Normal file
121
WebHostLib/static/assets/tutorial/minecraft/minecraft_es.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Guia instalación de Minecraft Randomizer
|
||||
|
||||
## Software Requerido
|
||||
|
||||
### Servidor
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Jugadores
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Procedimiento de instalación
|
||||
|
||||
### Instalación de servidor dedicado
|
||||
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
|
||||
1. Descarga el instalador de **Minecraft Forge** 1.16.15 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
|
||||
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
|
||||
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
|
||||
|
||||
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
|
||||
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
|
||||
- Esto creara la estructura de directorios apropiada para el siguiente paso
|
||||
|
||||
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
|
||||
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
|
||||
|
||||
### Instalación basica para jugadores
|
||||
- Compra e instala Minecraft a traves del tercer enlace.
|
||||
**Y listo!**.
|
||||
Los jugadores solo necesitan una version no modificada de Minecraft para jugar!
|
||||
|
||||
### Instalación avanzada para jugadores
|
||||
***Esto no es requerido para jugar a minecraft randomizado.***
|
||||
Sin embargo lo recomendamos porque hace la experiencia mas llevadera.
|
||||
|
||||
#### Recomended Mods
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Instala y ejecuta Minecraft al menos una vez.
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elige **install client**.
|
||||
- Ejecuta Minecraft forge al menos una vez para generar los directorios necesarios para el siguiente paso.
|
||||
3. Navega a la carpeta de instalación de Minecraft y colocal los mods que quieras en el directorio `mods`
|
||||
- Los directorios por defecto de instalación son:
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Configura tu fichero YAML
|
||||
|
||||
### Que es un fichero YAML y potque necesito uno?
|
||||
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu juego.
|
||||
Cada jugador de un multiworld entregara u propio fichero YAML.
|
||||
Esto permite que cada jugador disfrute de una experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld
|
||||
pueden tener diferentes opciones
|
||||
|
||||
### Where do I get a YAML file?
|
||||
Un fichero basico yaml para minecraft tendra este aspecto.
|
||||
```yaml
|
||||
# Usado para describir tu yaml. Util si tienes multiples ficheros
|
||||
description: Template Name
|
||||
# Tu nombre en el juego. Los espacios son reemplazados por guiones bajos, limitado a 16 caracteres
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
accessibility: locations
|
||||
# Recomendado no activar esto ya que el pool de objetos de Minecraft es bastante escueto, ademas hay muchas maneras alternativas de obtener los objetivos de Minecraft.
|
||||
progression_balancing: off
|
||||
# Cuantos avances se necesitan para hacer aparecer el Ender Dragon y acabar el juego. few = 30, normal = 50 , many = 70
|
||||
advancement_goal:
|
||||
few: 0
|
||||
normal: 1
|
||||
many: 0
|
||||
# Modifica el nivel de objetos lógicamente requeridos para explorar areas peligrosas y pelear contra jefes.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
# Avances que sean tediosos o basados en suerte tendran simplemente experiencia o cosas no necesarias
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Los avances extremadamente difíciles no seran requeridos; esto afecta a How Did We Get Here? y Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Los avances posteriores a Ender Dragon no tendrán objetos necesarios para que otros jugadores en el caso de un MW acaben su partida.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Actualmente desactivado; permite la mezcla de pueblos, puestos, fortalezas, bastiones y cuidades.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
```
|
||||
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
### Obten tu ficheros de datos Minecraft
|
||||
**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.**
|
||||
|
||||
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld (no confundir con hospedar el mundo minecraft).
|
||||
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
|
||||
Tu fichero de datos tiene una extensión `.apmc`.
|
||||
|
||||
Pon tu fichero de datos en el directorio `APData` de tu forge server. Asegurate de eliminar los que hubiera anteriormente
|
||||
|
||||
|
||||
### Conectar al multiserver
|
||||
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
|
||||
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
|
||||
|
||||
Una vez en juego introduce `/connect <AP-Address> (<Password>)` donde `<AP-Address>` es la dirección del servidor
|
||||
Archipelago. `(<Password>)`
|
||||
solo se necesita si el servidor Archipleago tiene un password activo.
|
||||
|
||||
### Jugar al juego
|
||||
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades
|
||||
por unirte exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor forge.
|
||||
|
||||
114
WebHostLib/static/assets/tutorial/minecraft/minecraft_sv.md
Normal file
114
WebHostLib/static/assets/tutorial/minecraft/minecraft_sv.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Minecraft Randomizer Uppsättningsguide
|
||||
|
||||
## Nödvändig Mjukvara
|
||||
|
||||
### Server Värd
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Spelare
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Installationsprocedurer
|
||||
|
||||
### Tillägnad
|
||||
Bara en person behöver göra denna uppsättning och vara värd för en server för alla andra spelare att koppla till.
|
||||
1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste rekommenderade versionen.
|
||||
|
||||
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera server**.
|
||||
- På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är viktigt för nästa steg.
|
||||
|
||||
3. Navigera till vart du har installerat servern och öppna `forge-1.16.5-xx.x.x-installer.jar`
|
||||
- Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta till `eula=true` för att acceptera Minecrafts EULA.
|
||||
- Detta kommer skapa de lämpliga katalogerna för dig att placera filerna i de följande steget.
|
||||
|
||||
4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge server.
|
||||
- Kör servern igen. Den kommer ladda up och generera den nödvändiga katalogen `APData` för när du är redo att spela!
|
||||
|
||||
### Grundläggande Spelaruppsättning
|
||||
- Köp och installera Minecraft från länken ovanför.
|
||||
|
||||
**Du är klar**.
|
||||
|
||||
Andra spelare behöver endast ha en 'Vanilla' omodifierad version av Minecraft för att kunna spela!
|
||||
|
||||
### Avancerad Spelaruppsättning
|
||||
***Detta är inte nödvändigt för att spela ett slumpmässigt Minecraftspel.***
|
||||
Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer trevligt.
|
||||
|
||||
#### Rekommenderade Moddar
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Installera och Kör Minecraft från länken ovanför minst en gång.
|
||||
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera klient**.
|
||||
- Starta Minecraft Forge minst en gång för att skapa katalogerna som behövs för de nästa stegen.
|
||||
3. Navigera till din Minecraft installationskatalog och placera de önskade moddarna med `.jar` i `mods` -katalogen.
|
||||
- Standardinstallationskatalogerna är som följande;
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Konfigurera Din YAML-fil
|
||||
|
||||
### Vad är en YAML-fil och varför behöver jag en?
|
||||
Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur
|
||||
den borde generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter
|
||||
varje spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld
|
||||
kan ha helt olika alternativ.
|
||||
|
||||
### Vart kan jag få tag i en YAML-fil?
|
||||
En grundläggande Minecraft YAML kommer se ut så här.
|
||||
```yaml
|
||||
description: Template Name
|
||||
# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns.
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
accessibility: locations
|
||||
progression_balancing: off
|
||||
advancement_goal:
|
||||
few: 0
|
||||
normal: 1
|
||||
many: 0
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
shuffle_structures:
|
||||
on: 1
|
||||
off: 0
|
||||
```
|
||||
|
||||
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med Archipelago-installationen.
|
||||
|
||||
## Gå med i ett Multivärld-spel
|
||||
|
||||
### Skaffa din Minecraft data-fil
|
||||
**Endast en YAML-fil behöver användats per Minecraft-värld oavsett hur många spelare det är som spelar.**
|
||||
|
||||
När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När detta
|
||||
är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som innehåller allas data-filer.
|
||||
Din data-fil borde ha en `.apmc` -extension.
|
||||
|
||||
Lägg din data-fil i dina forge-servrar `APData` -mapp. Se till att ta bort alla tidigare data-filer som var i där förut.
|
||||
|
||||
### Koppla till Multiservern
|
||||
Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status
|
||||
genom att skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient.
|
||||
När du är inne i spelet, skriv `/connect <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
|
||||
Archipelago-servern. `(<Lösenord>)` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt lösenord.
|
||||
|
||||
### Spela spelet
|
||||
När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis
|
||||
att du har lykats med att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla
|
||||
in till din forge-server.
|
||||
|
||||
@@ -85,5 +85,59 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Tutorial",
|
||||
"description": "A guide to setting up the Archipelago Minecraft software on your computer. This guide covers single-player, multiworld, and related software.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "minecraft/minecraft_en.md",
|
||||
"link": "minecraft/minecraft/en",
|
||||
"authors": [
|
||||
"Kono Tyran"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "Spanish",
|
||||
"filename": "minecraft/minecraft_es.md",
|
||||
"link": "minecraft/minecraft/es",
|
||||
"authors": [
|
||||
"Edos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "Swedish",
|
||||
"filename": "minecraft/minecraft_sv.md",
|
||||
"link": "minecraft/minecraft/sv",
|
||||
"authors": [
|
||||
"Albinum"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# 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
|
||||
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
|
||||
@@ -473,5 +473,11 @@ const generateGame = (raceMode = false) => {
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
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;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -14,7 +14,7 @@ html{
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#user-warning{
|
||||
#user-warning, #weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
@@ -25,6 +25,10 @@ html{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "tablepage.html" %}
|
||||
{% block head %}
|
||||
{{ 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 %}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Mystery Check Result</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/check.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/check.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/check.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/check.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<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 %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Generate Game</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/generate.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/generate.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/generate.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/generate.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% 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 %}
|
||||
|
||||
{% block header %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% 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 %}
|
||||
|
||||
{% include 'header/baseHeader.html' %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% 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 %}
|
||||
|
||||
{% include 'header/baseHeader.html' %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% 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 %}
|
||||
|
||||
{% include 'header/baseHeader.html' %}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Upload Multidata</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/hostGame.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/hostGame.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostGame.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/hostGame.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% import "macros.html" as macros %}
|
||||
{% block head %}
|
||||
<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 %}
|
||||
|
||||
{% block body %}
|
||||
@@ -21,7 +21,7 @@
|
||||
you can simply refresh this page and the server will be started again.<br>
|
||||
{% if 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) }}
|
||||
{% if room.owner == session["_id"] %}
|
||||
<form method=post>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2020 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2021 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a>
|
||||
-
|
||||
@@ -14,5 +14,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block head %}
|
||||
<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 %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -7,11 +7,19 @@
|
||||
</ul>
|
||||
{%- endmacro %}
|
||||
{% macro list_patches_room(room) %}
|
||||
{% if room.seed.patches %}
|
||||
{% if room.seed.slots %}
|
||||
<ul>
|
||||
{% for patch in patches|list|sort(attribute="player") %}
|
||||
{% 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) }}">
|
||||
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<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 rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tooltip.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/cookieNotice.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/globalStyles.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/styleController.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/cookieNotice.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/styleController.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||
{% block head %}
|
||||
<title>MultiWorld</title>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
{% block head %}
|
||||
<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="{{ static_autoversion("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/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerSettings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="player-settings">
|
||||
<div id="user-message"></div>
|
||||
<h1>Start Game</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/playerTracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/playerTracker.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerTracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerTracker.js") }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -39,7 +39,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
{% block head %}
|
||||
<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 %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
<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"
|
||||
></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 %}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/tracker.js") }}"></script>
|
||||
<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 %}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<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"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
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 %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Archipelago Guides</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tutorialLanding.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/tutorialLanding.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorialLanding.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tutorialLanding.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Generate Game</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/userContent.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/userContent.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/userContent.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/userContent.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
@@ -31,10 +31,7 @@
|
||||
<tr>
|
||||
<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
|
||||
class="center"
|
||||
data-tooltip="{{ room.seed.multidata.names[0]|join(", ")|truncate(256, False, " ...") }}"
|
||||
>{{ room.seed.multidata.names[0]|length }}</td>
|
||||
<td>>={{ room.seed.slots|length }}</td>
|
||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
</tr>
|
||||
@@ -59,11 +56,7 @@
|
||||
{% for seed in seeds %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("viewSeed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
||||
<td class="center"
|
||||
{% 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>{% if seed.multidata %}>={{ seed.slots|length }}{% else %}1{% endif %}
|
||||
</td>
|
||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
{% block head %}
|
||||
<title>View Seed {{ seed.id|suuid }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/viewSeed.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ static_autoversion("assets/viewSeed.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/viewSeed.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/viewSeed.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
@@ -37,17 +37,10 @@
|
||||
<td>Players: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for team in seed.multidata["names"] %}
|
||||
{% set outer_loop = loop %}
|
||||
<li>Team #{{ loop.index }} - {{ team | length }}
|
||||
<ul>
|
||||
{% 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>
|
||||
{% for patch in seed.slots|sort(attribute='player_id') %}
|
||||
<li>
|
||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player_id) }}">{{ patch.player_name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
@@ -64,13 +57,13 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>Patches: </td>
|
||||
<td>Files: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for patch in seed.patches %}
|
||||
{% for slot in seed.slots %}
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block head %}
|
||||
<title>Generation in Progress</title>
|
||||
<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 %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
{% block head %}
|
||||
<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="{{ static_autoversion("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/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedSettings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings">
|
||||
<header id="user-warning"></header>
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Settings</h1>
|
||||
<div id="instructions">
|
||||
This page is used to configure your weighted settings. You have three presets you can control, which
|
||||
|
||||
@@ -7,15 +7,15 @@ from uuid import UUID
|
||||
|
||||
from worlds.alttp import Items, Regions
|
||||
from WebHostLib import app, cache, Room
|
||||
from NetUtils import Hint
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
|
||||
|
||||
def get_id(item_name):
|
||||
def get_alttp_id(item_name):
|
||||
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['item_name'] = lambda id: Items.lookup_id_to_name.get(id, id)
|
||||
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: lookup_any_item_id_to_name.get(id, id)
|
||||
|
||||
icons = {
|
||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
@@ -154,9 +154,9 @@ levels = {"Fighter Sword": 1,
|
||||
"Bow": 1,
|
||||
"Silver Bow": 2}
|
||||
|
||||
multi_items = {get_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()}
|
||||
levels = {get_id(key): value for key, value in levels.items()}
|
||||
multi_items = {get_alttp_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")}
|
||||
links = {get_alttp_id(key): get_alttp_id(value) for key, value in links.items()}
|
||||
levels = {get_alttp_id(key): value for key, value in levels.items()}
|
||||
|
||||
tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer",
|
||||
"Hookshot", "Magic Mirror", "Flute",
|
||||
@@ -236,7 +236,7 @@ ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower',
|
||||
tracking_ids = []
|
||||
|
||||
for item in tracking_names:
|
||||
tracking_ids.append(get_id(item))
|
||||
tracking_ids.append(get_alttp_id(item))
|
||||
|
||||
small_key_ids = {}
|
||||
big_key_ids = {}
|
||||
@@ -253,7 +253,7 @@ for item_name, data in Items.item_table.items():
|
||||
big_key_ids[area] = data[2]
|
||||
ids_big_key[data[2]] = area
|
||||
|
||||
from MultiServer import get_item_name_from_id
|
||||
from MultiServer import get_item_name_from_id, Context
|
||||
|
||||
|
||||
def attribute_item(inventory, team, recipient, item):
|
||||
@@ -265,6 +265,7 @@ def attribute_item(inventory, team, recipient, item):
|
||||
|
||||
|
||||
def attribute_item_solo(inventory, item):
|
||||
"""Adds item to inventory counter, converts everything to progressive."""
|
||||
target_item = links.get(item, item)
|
||||
if item in levels: # non-progressive
|
||||
inventory[target_item] = max(inventory[target_item], levels[item])
|
||||
@@ -295,9 +296,9 @@ def get_static_room_data(room: Room):
|
||||
result = _multidata_cache.get(room.seed.id, None)
|
||||
if result:
|
||||
return result
|
||||
multidata = room.seed.multidata
|
||||
multidata = Context._decompress(room.seed.multidata)
|
||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
|
||||
locations = multidata['locations']
|
||||
names = multidata["names"]
|
||||
seed_checks_in_area = checks_in_area.copy()
|
||||
|
||||
@@ -308,36 +309,31 @@ def get_static_room_data(room: Room):
|
||||
for area, checks in key_only_locations.items():
|
||||
seed_checks_in_area[area] += len(checks)
|
||||
seed_checks_in_area["Total"] = 249
|
||||
if "checks_in_area" not in multidata:
|
||||
player_checks_in_area = {playernumber: (seed_checks_in_area if use_door_tracker and
|
||||
(0x140031, playernumber) in locations else checks_in_area)
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: location_to_area
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
|
||||
else:
|
||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][f'{playernumber}'][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
|
||||
for areaname in ordered_areas}
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||
for areaname in ordered_areas}
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
|
||||
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||
for _, (item_id, item_player) in multidata["locations"]:
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
||||
if item_id in ids_small_key:
|
||||
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
||||
for loc_data in locations.values():
|
||||
for item_id, item_player in loc_data.values():
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations[item_player].add(ids_big_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, player_big_key_locations, player_small_key_locations
|
||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||
player_big_key_locations, player_small_key_locations, multidata["precollected_items"]
|
||||
_multidata_cache[room.seed.id] = result
|
||||
return result
|
||||
|
||||
|
||||
@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):
|
||||
# Team and player must be positive and greater than zero
|
||||
if tracked_team < 0 or tracked_player < 1:
|
||||
@@ -348,7 +344,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
abort(404)
|
||||
|
||||
# Collect seed information and pare it down to a single player
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, precollected_items = get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
seed_checks_in_area = seed_checks_in_area[tracked_player]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
@@ -356,41 +352,36 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
|
||||
# Add starting items to inventory
|
||||
starting_items = room.seed.multidata.get("precollected_items", None)[tracked_player - 1]
|
||||
starting_items = precollected_items[tracked_player]
|
||||
if starting_items:
|
||||
for item_id in starting_items:
|
||||
attribute_item_solo(inventory, item_id)
|
||||
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
|
||||
# Add items to player inventory
|
||||
for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}):
|
||||
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
|
||||
# Skip teams and players not matching the request
|
||||
|
||||
player_locations = locations[ms_player]
|
||||
if ms_team == tracked_team:
|
||||
# If the player does not have the item, do nothing
|
||||
for location in locations_checked:
|
||||
if (location, ms_player) not in locations:
|
||||
continue
|
||||
|
||||
item, recipient = locations[location, ms_player]
|
||||
if recipient == tracked_player: # a check done for the tracked player
|
||||
attribute_item_solo(inventory, item)
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
if location in player_locations:
|
||||
item, recipient = player_locations[location]
|
||||
if recipient == tracked_player: # a check done for the tracked player
|
||||
attribute_item_solo(inventory, item)
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
|
||||
# Note the presence of the triforce item
|
||||
for (ms_team, ms_player), game_state in room.multisave.get("client_game_state", []):
|
||||
# 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
|
||||
|
||||
acquired_items = []
|
||||
for itm in inventory:
|
||||
acquired_items.append(get_item_name_from_id(itm))
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
if game_state == 30:
|
||||
inventory[106] = 1 # Triforce
|
||||
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
@@ -400,91 +391,48 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
}
|
||||
|
||||
# Determine which icon to use for the sword
|
||||
sword_url = icons["Fighter Sword"]
|
||||
sword_acquired = False
|
||||
sword_names = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']
|
||||
if "Progressive Sword" in acquired_items:
|
||||
sword_url = icons[sword_names[min(inventory[progressive_items["Progressive Sword"]], 4) - 1]]
|
||||
sword_acquired = True
|
||||
else:
|
||||
for sword in reversed(sword_names):
|
||||
if sword in acquired_items:
|
||||
sword_url = icons[sword]
|
||||
sword_acquired = True
|
||||
break
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]))
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level+1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name+"_acquired"] = acquired
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
|
||||
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,
|
||||
checks_in_area=seed_checks_in_area, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
mail_url=mail_url, shield_url=shield_url, shield_acquired=shield_acquired)
|
||||
**display_data)
|
||||
|
||||
|
||||
@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):
|
||||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
|
||||
player_small_key_locations, precollected_items = get_static_room_data(room)
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
@@ -492,29 +440,34 @@ def getTracker(tracker: UUID):
|
||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||
for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
precollected_items = room.seed.multidata.get("precollected_items", None)
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if "hints" in room.multisave:
|
||||
for key, hintdata in room.multisave["hints"]:
|
||||
for hint in hintdata:
|
||||
hints[key[0]].add(Hint(*hint))
|
||||
|
||||
for (team, player), locations_checked in room.multisave.get("location_checks", {}):
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
if "hints" in multisave:
|
||||
|
||||
for (team, slot), slot_hints in multisave["hints"].items():
|
||||
hints[team] |= set(slot_hints)
|
||||
|
||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
player_locations = locations[player]
|
||||
if precollected_items:
|
||||
precollected = precollected_items[player - 1]
|
||||
precollected = precollected_items[player]
|
||||
for item_id in precollected:
|
||||
attribute_item(inventory, team, player, item_id)
|
||||
for location in locations_checked:
|
||||
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
|
||||
|
||||
item, recipient = locations[location, player]
|
||||
item, recipient = player_locations[location]
|
||||
attribute_item(inventory, team, recipient, item)
|
||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
|
||||
for (team, player), game_state in room.multisave.get("client_game_state", []):
|
||||
if game_state:
|
||||
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
||||
if game_state == 30:
|
||||
inventory[team][player][106] = 1 # Triforce
|
||||
|
||||
group_big_key_locations = set()
|
||||
@@ -525,7 +478,7 @@ def getTracker(tracker: UUID):
|
||||
|
||||
activity_timers = {}
|
||||
now = datetime.datetime.utcnow()
|
||||
for (team, player), timestamp in room.multisave.get("client_activity_timers", []):
|
||||
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
player_names = {}
|
||||
@@ -533,12 +486,12 @@ def getTracker(tracker: UUID):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[(team, player)] = name
|
||||
long_player_names = player_names.copy()
|
||||
for (team, player), alias in room.multisave.get("name_aliases", []):
|
||||
for (team, player), alias in multisave.get("name_aliases", []):
|
||||
player_names[(team, player)] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||
|
||||
video = {}
|
||||
for (team, player), data in room.multisave.get("video", []):
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import json
|
||||
import zlib
|
||||
import zipfile
|
||||
import logging
|
||||
import lzma
|
||||
import json
|
||||
import base64
|
||||
import MultiServer
|
||||
|
||||
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",
|
||||
"spoiler": ".txt",
|
||||
@@ -29,7 +31,7 @@ def uploads():
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
patches = set()
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
multidata = None
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
@@ -39,34 +41,58 @@ def uploads():
|
||||
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."
|
||||
elif file.filename.endswith(".apbp"):
|
||||
splitted = file.filename.split("/")[-1][3:].split("P", 1)
|
||||
player = int(splitted[1].split(".")[0].split("_")[0])
|
||||
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
|
||||
data = zfile.open(file, "r").read()
|
||||
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||
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"):
|
||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||
elif file.filename.endswith(".archipelago"):
|
||||
try:
|
||||
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
|
||||
multidata = zfile.open(file).read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
else:
|
||||
multidata = zfile.open(file).read()
|
||||
if multidata:
|
||||
commit() # commit patches
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
||||
commit() # create seed
|
||||
for patch in patches:
|
||||
patch.seed = seed
|
||||
flush() # commit slots
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=session["_id"])
|
||||
flush() # create seed
|
||||
for slot in slots:
|
||||
slot.seed = seed
|
||||
|
||||
return redirect(url_for("viewSeed", seed=seed.id))
|
||||
else:
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
else:
|
||||
try:
|
||||
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
|
||||
multidata = file.read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
raise
|
||||
else:
|
||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||
commit() # place into DB and generate ids
|
||||
flush() # place into DB and generate ids
|
||||
return redirect(url_for("viewSeed", seed=seed.id))
|
||||
else:
|
||||
flash("Not recognized file format. Awaiting a .multidata file.")
|
||||
|
||||
159
WebUI.py
159
WebUI.py
@@ -1,159 +0,0 @@
|
||||
import http.server
|
||||
import logging
|
||||
import json
|
||||
import typing
|
||||
import socket
|
||||
import socketserver
|
||||
import threading
|
||||
import webbrowser
|
||||
import asyncio
|
||||
from functools import partial
|
||||
|
||||
from NetUtils import Node
|
||||
from LttPClient import Context
|
||||
import Utils
|
||||
|
||||
|
||||
class WebUiClient(Node, logging.Handler):
|
||||
loader = staticmethod(json.loads)
|
||||
dumper = staticmethod(json.dumps)
|
||||
def __init__(self):
|
||||
super(WebUiClient, self).__init__()
|
||||
self.manual_snes = None
|
||||
|
||||
@staticmethod
|
||||
def build_message(msg_type: str, content: typing.Union[str, dict]) -> dict:
|
||||
return {'type': msg_type, 'content': content}
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
self.broadcast_all({"type": record.levelname.lower(), "content": str(record.msg)})
|
||||
|
||||
def send_chat_message(self, message):
|
||||
self.broadcast_all(self.build_message('chat', message))
|
||||
|
||||
def send_connection_status(self, ctx: Context):
|
||||
asyncio.create_task(self._send_connection_status(ctx))
|
||||
|
||||
async def _send_connection_status(self, ctx: Context):
|
||||
cache = Utils.persistent_load()
|
||||
cached_address = cache.get("servers", {}).get("default", None)
|
||||
server_address = ctx.server_address if ctx.server_address else cached_address if cached_address else None
|
||||
|
||||
self.broadcast_all(self.build_message('connections', {
|
||||
'snesDevice': ctx.snes_attached_device[1] if ctx.snes_attached_device else None,
|
||||
'snes': ctx.snes_state,
|
||||
'serverAddress': server_address,
|
||||
'server': 1 if ctx.server is not None and not ctx.server.socket.closed else 0,
|
||||
}))
|
||||
|
||||
def send_device_list(self, devices):
|
||||
self.broadcast_all(self.build_message('availableDevices', {
|
||||
'devices': devices,
|
||||
}))
|
||||
|
||||
def poll_for_server_ip(self):
|
||||
self.broadcast_all(self.build_message('serverAddress', {}))
|
||||
|
||||
def notify_item_sent(self, finder, recipient, item, location, i_am_finder: bool, i_am_recipient: bool,
|
||||
item_is_unique: bool = False):
|
||||
self.broadcast_all(self.build_message('itemSent', {
|
||||
'finder': finder,
|
||||
'recipient': recipient,
|
||||
'item': item,
|
||||
'location': location,
|
||||
'iAmFinder': int(i_am_finder),
|
||||
'iAmRecipient': int(i_am_recipient),
|
||||
'itemIsUnique': int(item_is_unique),
|
||||
}))
|
||||
|
||||
def notify_item_found(self, finder: str, item: str, location: str, i_am_finder: bool, item_is_unique: bool = False):
|
||||
self.broadcast_all(self.build_message('itemFound', {
|
||||
'finder': finder,
|
||||
'item': item,
|
||||
'location': location,
|
||||
'iAmFinder': int(i_am_finder),
|
||||
'itemIsUnique': int(item_is_unique),
|
||||
}))
|
||||
|
||||
def notify_item_received(self, finder: str, item: str, location: str, item_index: int, queue_length: int,
|
||||
item_is_unique: bool = False):
|
||||
self.broadcast_all(self.build_message('itemReceived', {
|
||||
'finder': finder,
|
||||
'item': item,
|
||||
'location': location,
|
||||
'itemIndex': item_index,
|
||||
'queueLength': queue_length,
|
||||
'itemIsUnique': int(item_is_unique),
|
||||
}))
|
||||
|
||||
def send_hint(self, finder, recipient, item, location, found, i_am_finder: bool, i_am_recipient: bool,
|
||||
entrance_location: str = None):
|
||||
self.broadcast_all(self.build_message('hint', {
|
||||
'finder': finder,
|
||||
'recipient': recipient,
|
||||
'item': item,
|
||||
'location': location,
|
||||
'found': int(found),
|
||||
'iAmFinder': int(i_am_finder),
|
||||
'iAmRecipient': int(i_am_recipient),
|
||||
'entranceLocation': entrance_location,
|
||||
}))
|
||||
|
||||
def send_game_info(self, ctx: Context):
|
||||
self.broadcast_all(self.build_message('gameInfo', {
|
||||
'clientVersion': Utils.__version__,
|
||||
'hintCost': ctx.hint_cost,
|
||||
'checkPoints': ctx.check_points,
|
||||
'forfeitMode': ctx.forfeit_mode,
|
||||
'remainingMode': ctx.remaining_mode,
|
||||
}))
|
||||
|
||||
def send_location_check(self, ctx: Context, last_check: str):
|
||||
self.broadcast_all(self.build_message('locationCheck', {
|
||||
'totalChecks': len(ctx.locations_checked),
|
||||
'hintPoints': ctx.hint_points,
|
||||
'lastCheck': last_check,
|
||||
}))
|
||||
|
||||
|
||||
web_thread = None
|
||||
PORT = 5050
|
||||
|
||||
|
||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_request(self, code='-', size='-'):
|
||||
pass
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def log_date_time_string(self):
|
||||
pass
|
||||
|
||||
|
||||
Handler = partial(RequestHandler,
|
||||
directory=Utils.local_path("data", "web", "public"))
|
||||
|
||||
|
||||
def start_server(socket_port: int, on_start=lambda: None):
|
||||
global web_thread
|
||||
try:
|
||||
server = socketserver.TCPServer(("", PORT), Handler)
|
||||
except OSError:
|
||||
# In most cases "Only one usage of each socket address (protocol/network address/port) is normally permitted"
|
||||
import logging
|
||||
|
||||
# If the exception is caused by our desired port being unavailable, assume the web server is already running
|
||||
# from another client instance
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
if sock.connect_ex(('localhost', PORT)) == 0:
|
||||
logging.info("Web server is already running in another client window.")
|
||||
webbrowser.open(f'http://localhost:{PORT}?port={socket_port}')
|
||||
return
|
||||
|
||||
# If the exception is caused by something else, report on it
|
||||
logging.exception("Unable to bind port for local web server. The CLI client should work in all cases.")
|
||||
else:
|
||||
print("serving at port", PORT)
|
||||
on_start()
|
||||
web_thread = threading.Thread(target=server.serve_forever).start()
|
||||
7
data/default.apsprite
Normal file
7
data/default.apsprite
Normal file
@@ -0,0 +1,7 @@
|
||||
author: Nintendo
|
||||
data: null
|
||||
game: A Link to the Past
|
||||
min_format_version: 1
|
||||
name: Link
|
||||
format_version: 1
|
||||
sprite_version: 1
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Berserker55
|
||||
Copyright (c) 2021 Berserker55 and Dewiniaid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
-- for testing
|
||||
script.on_event(defines.events.on_tick, function(event)
|
||||
if event.tick%600 == 0 then
|
||||
dumpTech()
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
game.print("Research done")
|
||||
dumpTech()
|
||||
end)
|
||||
|
||||
function dumpTech()
|
||||
|
||||
local force = game.forces["player"]
|
||||
local data_collection = {}
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.researched and string.find(tech_name, "ap-") == 1 then
|
||||
data_collection[tech_name] = tech.researched
|
||||
end
|
||||
end
|
||||
game.write_file("research_done.json", game.table_to_json(data_collection), false)
|
||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.print("Sent progress to Archipelago.")
|
||||
end
|
||||
|
||||
function dumpGameInfo()
|
||||
-- dump Game Information that the Archipelago Randomizer needs.
|
||||
local data_collection = {}
|
||||
local force = game.forces["player"]
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.enabled then
|
||||
local tech_data = {}
|
||||
local unlocks = {}
|
||||
tech_data["unlocks"] = unlocks
|
||||
local requires = {}
|
||||
tech_data["requires"] = requires
|
||||
local ingredients = {}
|
||||
tech_data["ingredients"] = ingredients
|
||||
for tech_requirement, _ in pairs(tech.prerequisites) do
|
||||
table.insert(requires, tech_requirement)
|
||||
end
|
||||
for _, modifier in pairs(tech.effects) do
|
||||
if modifier.type == "unlock-recipe" then
|
||||
table.insert(unlocks, modifier.recipe)
|
||||
end
|
||||
end
|
||||
for _, ingredient in pairs(tech.research_unit_ingredients) do
|
||||
table.insert(ingredients, ingredient.name)
|
||||
end
|
||||
data_collection[tech_name] = tech_data
|
||||
|
||||
end
|
||||
game.write_file("techs.json", game.table_to_json(data_collection), false)
|
||||
game.print("Exported Tech Data")
|
||||
end
|
||||
data_collection = {}
|
||||
for recipe_name, recipe in pairs(force.recipes) do
|
||||
local recipe_data = {}
|
||||
recipe_data["ingredients"] = {}
|
||||
recipe_data["products"] = {}
|
||||
recipe_data["category"] = recipe.category
|
||||
for _, ingredient in pairs(recipe.ingredients) do
|
||||
table.insert(recipe_data["ingredients"], ingredient.name)
|
||||
end
|
||||
for _, product in pairs(recipe.products) do
|
||||
table.insert(recipe_data["products"], product.name)
|
||||
end
|
||||
data_collection[recipe_name] = recipe_data
|
||||
end
|
||||
game.write_file("recipes.json", game.table_to_json(data_collection), false)
|
||||
game.print("Exported Recipe Data")
|
||||
-- data.raw can't be accessed from control.lua, need to find a better method
|
||||
-- data_collection = {}
|
||||
-- for machine_name, machine in pairs(data.raw["assembling_machine"]) do
|
||||
-- local machine_data = {}
|
||||
-- machine_data["categories"] = table.deepcopy(machine.crafting_categories)
|
||||
-- data_collection[machine.name] = machine_data
|
||||
-- end
|
||||
-- game.write_file("machines.json", game.table_to_json(data_collection), false)
|
||||
-- game.print("Exported Machine Data")
|
||||
end
|
||||
|
||||
-- add / commands
|
||||
|
||||
commands.add_command("ap-get-info-dump", "Dump Game Info, used by Archipelago.", function(call)
|
||||
dumpGameInfo()
|
||||
end)
|
||||
|
||||
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
||||
dumpTech()
|
||||
end)
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
local force = game.forces["player"]
|
||||
local tech_name = call.parameter
|
||||
local tech = force.technologies[tech_name]
|
||||
if tech ~= nil then
|
||||
if tech.researched ~= true then
|
||||
tech.researched = true
|
||||
game.print({"", "Received ", tech.localised_name, " from Archipelago"})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
end
|
||||
else
|
||||
game.print("Unknown Technology " .. tech_name)
|
||||
end
|
||||
end)
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "archipelago-client",
|
||||
"version": "0.0.1",
|
||||
"title": "Archipelago",
|
||||
"author": "Berserker",
|
||||
"author": "Berserker and Dewiniaid",
|
||||
"homepage": "https://archipelago.gg",
|
||||
"description": "Integration client for the Archipelago Randomizer",
|
||||
"factorio_version": "1.1"
|
||||
|
||||
23
data/factorio/mod/lib.lua
Normal file
23
data/factorio/mod/lib.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
function filter_ingredients(ingredients, ingredient_filter)
|
||||
local new_ingredient_list = {}
|
||||
for _, ingredient_table in pairs(ingredients) do
|
||||
if ingredient_filter[ingredient_table[1]] then -- name of ingredient_table
|
||||
table.insert(new_ingredient_list, ingredient_table)
|
||||
end
|
||||
end
|
||||
|
||||
return new_ingredient_list
|
||||
end
|
||||
|
||||
function get_any_stack_size(name)
|
||||
local item = game.item_prototypes[name]
|
||||
if item ~= nil then
|
||||
return item.stack_size
|
||||
end
|
||||
item = game.equipment_prototypes[name]
|
||||
if item ~= nil then
|
||||
return item.stack_size
|
||||
end
|
||||
-- failsafe
|
||||
return 1
|
||||
end
|
||||
BIN
data/factorio/mod/thumbnail.png
Normal file
BIN
data/factorio/mod/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
239
data/factorio/mod_template/control.lua
Normal file
239
data/factorio/mod_template/control.lua
Normal file
@@ -0,0 +1,239 @@
|
||||
{% macro dict_to_lua(dict) -%}
|
||||
{
|
||||
{% for key, value in dict.items() %}
|
||||
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
{%- endmacro %}
|
||||
require "lib"
|
||||
require "util"
|
||||
|
||||
FREE_SAMPLES = {{ free_samples }}
|
||||
SLOT_NAME = "{{ slot_name }}"
|
||||
SEED_NAME = "{{ seed_name }}"
|
||||
--SUPPRESS_INVENTORY_EVENTS = false
|
||||
|
||||
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
|
||||
function on_force_created(event)
|
||||
--event.force appears to be LuaForce.name, not LuaForce
|
||||
game.forces[event.force].research_queue_enabled = true
|
||||
local data = {}
|
||||
data['earned_samples'] = {{ dict_to_lua(starting_items) }}
|
||||
data["victory"] = 0
|
||||
global.forcedata[event.force] = data
|
||||
end
|
||||
script.on_event(defines.events.on_force_created, on_force_created)
|
||||
|
||||
-- Destroy force data. This doesn't appear to be currently possible with the Factorio API, but here for completeness.
|
||||
function on_force_destroyed(event)
|
||||
global.forcedata[event.force.name] = nil
|
||||
end
|
||||
|
||||
-- Initialize player data, either from them joining the game or them already being part of the game when the mod was
|
||||
-- added.`
|
||||
function on_player_created(event)
|
||||
local player = game.players[event.player_index]
|
||||
-- FIXME: This (probably) fires before any other mod has a chance to change the player's force
|
||||
-- For now, they will (probably) always be on the 'player' force when this event fires.
|
||||
local data = {}
|
||||
data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples'])
|
||||
global.playerdata[player.index] = data
|
||||
update_player(player.index) -- Attempt to send pending free samples, if relevant.
|
||||
end
|
||||
script.on_event(defines.events.on_player_created, on_player_created)
|
||||
|
||||
function on_player_removed(event)
|
||||
global.playerdata[event.player_index] = nil
|
||||
end
|
||||
script.on_event(defines.events.on_player_removed, on_player_removed)
|
||||
|
||||
function on_rocket_launched(event)
|
||||
global.forcedata[event.rocket.force.name]['victory'] = 1
|
||||
dumpInfo(event.rocket.force)
|
||||
end
|
||||
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
|
||||
|
||||
-- Updates a player, attempting to send them any pending samples (if relevant)
|
||||
function update_player(index)
|
||||
local player = game.players[index]
|
||||
if not player or not player.valid then -- Do nothing if we reference an invalid player somehow
|
||||
return
|
||||
end
|
||||
local character = player.character or player.cutscene_character
|
||||
if not character or not character.valid then
|
||||
return
|
||||
end
|
||||
local data = global.playerdata[index]
|
||||
local samples = data['pending_samples']
|
||||
local sent
|
||||
--player.print(serpent.block(data['pending_samples']))
|
||||
local stack = {}
|
||||
--SUPPRESS_INVENTORY_EVENTS = true
|
||||
for name, count in pairs(samples) do
|
||||
stack.name = name
|
||||
stack.count = count
|
||||
if character.can_insert(stack) then
|
||||
sent = character.insert(stack)
|
||||
else
|
||||
sent = 0
|
||||
end
|
||||
if sent > 0 then
|
||||
player.print("Received " .. sent .. "x [item=" .. name .. "]")
|
||||
data.suppress_full_inventory_message = false
|
||||
end
|
||||
if sent ~= count then -- Couldn't full send.
|
||||
if not data.suppress_full_inventory_message then
|
||||
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
|
||||
end
|
||||
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
|
||||
samples[name] = count - sent -- Buffer the remaining items
|
||||
break -- Stop trying to send other things
|
||||
else
|
||||
samples[name] = nil -- Remove from the list
|
||||
end
|
||||
end
|
||||
--SUPPRESS_INVENTORY_EVENTS = false
|
||||
end
|
||||
|
||||
-- Update players upon them connecting, since updates while they're offline are suppressed.
|
||||
script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end)
|
||||
|
||||
function update_player_event(event)
|
||||
--if not SUPPRESS_INVENTORY_EVENTS then
|
||||
update_player(event.player_index)
|
||||
--end
|
||||
end
|
||||
|
||||
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
|
||||
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
t[name] = (t[name] or 0) + count
|
||||
end
|
||||
-- Add to global table of earned samples for future new players
|
||||
add_to_table(global.forcedata[force.name]['earned_samples'])
|
||||
-- Add to existing players
|
||||
for _, player in pairs(force.players) do
|
||||
add_to_table(global.playerdata[player.index]['pending_samples'])
|
||||
update_player(player.index)
|
||||
end
|
||||
end
|
||||
|
||||
script.on_init(function()
|
||||
global.forcedata = {}
|
||||
global.playerdata = {}
|
||||
-- Fire dummy events for all currently existing forces.
|
||||
local e = {}
|
||||
for name, _ in pairs(game.forces) do
|
||||
e.force = name
|
||||
on_force_created(e)
|
||||
end
|
||||
e.force = nil
|
||||
|
||||
-- Fire dummy events for all currently existing players.
|
||||
for index, _ in pairs(game.players) do
|
||||
e.player_index = index
|
||||
on_player_created(e)
|
||||
end
|
||||
end)
|
||||
|
||||
-- for testing
|
||||
script.on_event(defines.events.on_tick, function(event)
|
||||
if event.tick%600 == 300 then
|
||||
dumpInfo(game.forces["player"])
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
local technology = event.research
|
||||
dumpInfo(technology.force)
|
||||
if FREE_SAMPLES == 0 then
|
||||
return -- Nothing else to do
|
||||
end
|
||||
if not technology.effects then
|
||||
return -- No technology effects, so nothing to do.
|
||||
end
|
||||
for _, effect in pairs(technology.effects) do
|
||||
if effect.type == "unlock-recipe" then
|
||||
local recipe = game.recipe_prototypes[effect.recipe]
|
||||
for _, result in pairs(recipe.products) do
|
||||
if result.type == "item" and result.amount then
|
||||
local name = result.name
|
||||
local count
|
||||
if FREE_SAMPLES == 1 then
|
||||
count = result.amount
|
||||
else
|
||||
count = get_any_stack_size(result.name)
|
||||
if FREE_SAMPLES == 2 then
|
||||
count = math.ceil(count / 2)
|
||||
end
|
||||
end
|
||||
add_samples(technology.force, name, count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function dumpInfo(force)
|
||||
local research_done = {}
|
||||
local data_collection = {
|
||||
["research_done"] = research_done,
|
||||
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
||||
["slot_name"] = SLOT_NAME,
|
||||
["seed_name"] = SEED_NAME
|
||||
}
|
||||
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
if tech.researched and string.find(tech_name, "ap%-") == 1 then
|
||||
research_done[tech_name] = tech.researched
|
||||
end
|
||||
end
|
||||
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
|
||||
-- game.print("Sent progress to Archipelago.")
|
||||
end
|
||||
|
||||
|
||||
|
||||
function chain_lookup(table, ...)
|
||||
for _, k in ipairs{...} do
|
||||
table = table[k]
|
||||
if not table then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
-- add / commands
|
||||
|
||||
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
|
||||
if call.player_index == nil then
|
||||
dumpInfo(game.forces.player)
|
||||
else
|
||||
dumpInfo(game.players[call.player_index].force)
|
||||
end
|
||||
game.print("Wrote bridge file.")
|
||||
end)
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
local force = game.forces["player"]
|
||||
chunks = {}
|
||||
for substring in call.parameter:gmatch("%S+") do -- split on " "
|
||||
table.insert(chunks, substring)
|
||||
end
|
||||
local tech_name = chunks[1]
|
||||
local source = chunks[2] or "Archipelago"
|
||||
local tech = force.technologies[tech_name]
|
||||
if tech ~= nil then
|
||||
if tech.researched ~= true then
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
end
|
||||
else
|
||||
game.print("Unknown Technology " .. tech_name)
|
||||
end
|
||||
end)
|
||||
@@ -1,23 +1,50 @@
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
|
||||
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }}
|
||||
|
||||
local technologies = data.raw["technology"]
|
||||
local original_tech
|
||||
local new_tree_copy
|
||||
allowed_ingredients = {}
|
||||
{%- for tech_name, technology in custom_data["custom_technologies"].items() %}
|
||||
allowed_ingredients["{{ tech_name }}"] = {
|
||||
{%- for ingredient in technology.ingredients %}
|
||||
["{{ingredient}}"] = 1,
|
||||
{%- endfor %}
|
||||
}
|
||||
{% endfor %}
|
||||
local template_tech = table.deepcopy(technologies["automation"])
|
||||
{#- ensure the copy unlocks nothing #}
|
||||
template_tech.unlocks = {}
|
||||
template_tech.upgrade = false
|
||||
template_tech.effects = {}
|
||||
template_tech.prerequisites = {}
|
||||
|
||||
function prep_copy(new_copy, old_tech)
|
||||
old_tech.enabled = false
|
||||
new_copy.unit = table.deepcopy(old_tech.unit)
|
||||
local ingredient_filter = allowed_ingredients[old_tech.name]
|
||||
if ingredient_filter ~= nil then
|
||||
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
|
||||
|
||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||
{%- for original_tech_name, item_name, receiving_player in locations %}
|
||||
original_tech = technologies["{{original_tech_name}}"]
|
||||
{#- the tech researched by the local player #}
|
||||
new_tree_copy = table.deepcopy(template_tech)
|
||||
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
||||
{#- hide and disable original tech; which will be shown, unlocked and enabled by AP Client #}
|
||||
original_tech.enabled = false
|
||||
{#- copy original tech costs #}
|
||||
new_tree_copy.unit = table.deepcopy(original_tech.unit)
|
||||
{% if item_name in tech_table %}
|
||||
prep_copy(new_tree_copy, original_tech)
|
||||
{% if tech_cost != 1 %}
|
||||
if new_tree_copy.unit.count then
|
||||
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
|
||||
end
|
||||
{% endif %}
|
||||
{% if item_name in tech_table and visibility %}
|
||||
{#- copy Factorio Technology Icon #}
|
||||
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
|
||||
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
|
||||
@@ -28,7 +55,13 @@ new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
|
||||
new_tree_copy.icons = nil
|
||||
new_tree_copy.icon_size = 512
|
||||
{% endif %}
|
||||
{#- add new technology to game #}
|
||||
{#- connect Technology #}
|
||||
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
||||
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
||||
{% endfor %}
|
||||
{% endif -%}
|
||||
{#- add new Technology to game #}
|
||||
data:extend{new_tree_copy}
|
||||
|
||||
{% endfor %}
|
||||
@@ -1,8 +1,18 @@
|
||||
|
||||
[technology-name]
|
||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
||||
{%- if visibility -%}
|
||||
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
|
||||
{% else %}
|
||||
ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
|
||||
[technology-description]
|
||||
{% for original_tech_name, item_name, receiving_player in locations %}
|
||||
"ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
|
||||
{%- if visibility -%}
|
||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
|
||||
{% else %}
|
||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
File diff suppressed because one or more lines are too long
7
data/sprites/custom/link.apsprite
Normal file
7
data/sprites/custom/link.apsprite
Normal file
@@ -0,0 +1,7 @@
|
||||
author: Nintendo
|
||||
data: null
|
||||
game: A Link to the Past
|
||||
min_format_version: 1
|
||||
name: Link
|
||||
format_version: 1
|
||||
sprite_version: 1
|
||||
@@ -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
2
data/web/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
*.map
|
||||
14170
data/web/package-lock.json
generated
14170
data/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
Binary file not shown.
Binary file not shown.
|
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);
|
||||
@@ -1,8 +0,0 @@
|
||||
const SET_AVAILABLE_DEVICES = 'SET_AVAILABLE_DEVICES';
|
||||
|
||||
const setAvailableDevices = (devices) => ({
|
||||
type: SET_AVAILABLE_DEVICES,
|
||||
devices,
|
||||
});
|
||||
|
||||
export default setAvailableDevices;
|
||||
@@ -1,8 +0,0 @@
|
||||
const SET_WEBSOCKET = 'SET_WEBSOCKET';
|
||||
|
||||
const setWebSocket = (webSocket) => ({
|
||||
type: SET_WEBSOCKET,
|
||||
webSocket,
|
||||
});
|
||||
|
||||
export default setWebSocket;
|
||||
@@ -1,25 +0,0 @@
|
||||
import _assign from 'lodash-es/assign';
|
||||
|
||||
const initialState = {
|
||||
webSocket: null,
|
||||
availableDevices: [],
|
||||
};
|
||||
|
||||
const webUIReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_WEBSOCKET':
|
||||
return _assign({}, state, {
|
||||
webSocket: action.webSocket,
|
||||
});
|
||||
|
||||
case 'SET_AVAILABLE_DEVICES':
|
||||
return _assign({}, state, {
|
||||
availableDevices: action.devices,
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default webUIReducer;
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import HeaderBar from '../../HeaderBar/components/HeaderBar';
|
||||
import Monitor from '../../Monitor/components/Monitor';
|
||||
import WidgetArea from '../../WidgetArea/containers/WidgetArea';
|
||||
import MonitorTools from '../../global/MonitorTools';
|
||||
import '../../../styles/WebUI/containers/WebUI.scss';
|
||||
|
||||
// Redux actions
|
||||
import setWebSocket from '../Redux/actions/setWebSocket';
|
||||
import WebSocketUtils from '../../global/WebSocketUtils';
|
||||
import updateGameState from '../../global/Redux/actions/updateGameState';
|
||||
import appendMessage from '../../Monitor/Redux/actions/appendMessage';
|
||||
|
||||
const mapReduxStateToProps = (reduxState) => ({
|
||||
connections: reduxState.gameState.connections,
|
||||
simpleFont: reduxState.monitor.simpleFont,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
doSetWebSocket: (webSocket) => dispatch(setWebSocket(webSocket)),
|
||||
handleIncomingMessage: (message) => dispatch(WebSocketUtils.handleIncomingMessage(message)),
|
||||
doUpdateGameState: (gameState) => dispatch(updateGameState(gameState)),
|
||||
appendMonitorMessage: (message) => dispatch(appendMessage(message)),
|
||||
});
|
||||
|
||||
class WebUI extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.webSocket = null;
|
||||
this.maxConnectionAttempts = 20;
|
||||
this.webUiRef = React.createRef();
|
||||
this.state = {
|
||||
connectionAttempts: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.webSocketConnect();
|
||||
}
|
||||
|
||||
webSocketConnect = () => {
|
||||
this.props.appendMonitorMessage(MonitorTools.createTextDiv(
|
||||
`Attempting to connect to MultiClient (attempt ${this.state.connectionAttempts + 1})...`,
|
||||
));
|
||||
this.setState({ connectionAttempts: this.state.connectionAttempts + 1 }, () => {
|
||||
if (this.state.connectionAttempts >= 20) {
|
||||
this.props.appendMonitorMessage(MonitorTools.createTextDiv(
|
||||
'Unable to connect to MultiClient. Maximum of 20 attempts exceeded.',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const getParams = new URLSearchParams(document.location.search.substring(1));
|
||||
const port = getParams.get('port');
|
||||
if (!port) { throw new Error('Unable to determine socket port from GET parameters'); }
|
||||
|
||||
const webSocketAddress = `ws://localhost:${port}`;
|
||||
try {
|
||||
this.props.webSocket.close();
|
||||
this.props.doSetWebSocket(null);
|
||||
} catch (error) {
|
||||
// Ignore errors caused by attempting to close an invalid WebSocket object
|
||||
}
|
||||
|
||||
const webSocket = new WebSocket(webSocketAddress);
|
||||
webSocket.onerror = () => {
|
||||
this.props.doUpdateGameState({
|
||||
connections: {
|
||||
snesDevice: this.props.connections.snesDevice,
|
||||
snesConnected: false,
|
||||
serverAddress: this.props.connections.serverAddress,
|
||||
serverConnected: false,
|
||||
},
|
||||
});
|
||||
if (this.state.connectionAttempts < this.maxConnectionAttempts) {
|
||||
setTimeout(this.webSocketConnect, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
// Dispatch a custom event when websocket messages are received
|
||||
webSocket.onmessage = (message) => {
|
||||
this.props.handleIncomingMessage(message);
|
||||
};
|
||||
|
||||
// Store the webSocket object in the Redux store so other components can access it
|
||||
webSocket.onopen = () => {
|
||||
this.props.doSetWebSocket(webSocket);
|
||||
webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'connections'));
|
||||
this.props.appendMonitorMessage(MonitorTools.createTextDiv('Connected to MultiClient.'));
|
||||
this.setState({ connectionAttempts: 0 });
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="web-ui" ref={ this.webUiRef } className={ this.props.simpleFont ? 'simple-font' : null }>
|
||||
<HeaderBar />
|
||||
<div id="content-middle">
|
||||
<Monitor />
|
||||
<WidgetArea />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapReduxStateToProps, mapDispatchToProps)(WebUI);
|
||||
@@ -1,117 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import '../../../styles/WidgetArea/containers/WidgetArea.scss';
|
||||
|
||||
const mapReduxStateToProps = (reduxState) => ({
|
||||
clientVersion: reduxState.gameState.clientVersion,
|
||||
forfeitMode: reduxState.gameState.forfeitMode,
|
||||
remainingMode: reduxState.gameState.remainingMode,
|
||||
hintCost: reduxState.gameState.hintCost,
|
||||
checkPoints: reduxState.gameState.checkPoints,
|
||||
hintPoints: reduxState.gameState.hintPoints,
|
||||
totalChecks: reduxState.gameState.totalChecks,
|
||||
lastCheck: reduxState.gameState.lastCheck,
|
||||
});
|
||||
|
||||
class WidgetArea extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
saveNotes = (event) => {
|
||||
localStorage.setItem('notes', event.target.value);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
||||
toggleCollapse = () => this.setState({ collapsed: !this.state.collapsed });
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="widget-area" className={ `${this.state.collapsed ? 'collapsed' : null}` }>
|
||||
{
|
||||
this.state.collapsed ? (
|
||||
<div id="widget-button-row">
|
||||
<button className="collapse-button" onClick={ this.toggleCollapse }>↩</button>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.collapsed ? null : (
|
||||
<div id="widget-area-contents">
|
||||
<div id="game-info">
|
||||
<div id="game-info-title">
|
||||
Game Info:
|
||||
<button className="collapse-button" onClick={ this.toggleCollapse }>↪</button>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Client Version:</th>
|
||||
<td>{this.props.clientVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Forfeit Mode:</th>
|
||||
<td>{this.props.forfeitMode}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Remaining Mode:</th>
|
||||
<td>{this.props.remainingMode}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="check-data">
|
||||
<div id="check-data-title">Checks:</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Total Checks:</th>
|
||||
<td>{this.props.totalChecks}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Last Check:</th>
|
||||
<td>{this.props.lastCheck}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="hint-data">
|
||||
<div id="hint-data-title">
|
||||
Hint Data:
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Hint Cost:</th>
|
||||
<td>{this.props.hintCost}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Check Points:</th>
|
||||
<td>{this.props.checkPoints}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Current Points:</th>
|
||||
<td>{this.props.hintPoints}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="notes">
|
||||
<div id="notes-title">
|
||||
<div>Notes:</div>
|
||||
</div>
|
||||
<textarea defaultValue={ localStorage.getItem('notes') } onKeyUp={ this.saveNotes } />
|
||||
</div>
|
||||
More tools Coming Soon™
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapReduxStateToProps)(WidgetArea);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user