mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 23:25:51 -08:00
Compare commits
508 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f6003582 | ||
|
|
bd8e1f6531 | ||
|
|
3658c9f8e3 | ||
|
|
6a912c128d | ||
|
|
71f30b72f4 | ||
|
|
2dc8b77ddc | ||
|
|
16cd2760a4 | ||
|
|
55bfc71269 | ||
|
|
d623cd5ce0 | ||
|
|
4bbf8858b0 | ||
|
|
5626ff1582 | ||
|
|
28f5236719 | ||
|
|
4cd9711de3 | ||
|
|
2ffa0d0e7f | ||
|
|
586af0de1d | ||
|
|
fc3b8c40be | ||
|
|
c178006acc | ||
|
|
4e43166e1f | ||
|
|
452026165f | ||
|
|
82b8b313f0 | ||
|
|
b529f95798 | ||
|
|
2d55cf4bbf | ||
|
|
62e0e0bb55 | ||
|
|
83a40d4394 | ||
|
|
4937156021 | ||
|
|
24596899c9 | ||
|
|
cd3f0eabfb | ||
|
|
34af785e87 | ||
|
|
34cfe7d1df | ||
|
|
ca8f6c2439 | ||
|
|
4a8ba0575f | ||
|
|
77ec8d4141 | ||
|
|
61ae51b30c | ||
|
|
f26d2d5f20 | ||
|
|
fd07bc3f2c | ||
|
|
8316a1902d | ||
|
|
650fd5d792 | ||
|
|
82d3e4bc92 | ||
|
|
8eb1f0258c | ||
|
|
80c86f34a4 | ||
|
|
3ed7b9f60c | ||
|
|
77c18ac819 | ||
|
|
0d6c23e4f2 | ||
|
|
ec9ef21cc0 | ||
|
|
43323e59ce | ||
|
|
9ada4df151 | ||
|
|
d42d77d3d3 | ||
|
|
2007549e01 | ||
|
|
987bbc761a | ||
|
|
0b096528d4 | ||
|
|
fa56541b3a | ||
|
|
beb15aa99a | ||
|
|
ca9bf48ffa | ||
|
|
b9941e40c1 | ||
|
|
e8639988ce | ||
|
|
c32f3d6e96 | ||
|
|
60697cc8ba | ||
|
|
c0d3f140f3 | ||
|
|
d5934a88a7 | ||
|
|
db2731dfb7 | ||
|
|
97ee73d79f | ||
|
|
48ce19a923 | ||
|
|
4f28c3fa46 | ||
|
|
449f4ee92f | ||
|
|
79041bdf21 | ||
|
|
655d14ed6e | ||
|
|
5d0d9c2890 | ||
|
|
f10163e7d2 | ||
|
|
666e3b5333 | ||
|
|
2b124aaff4 | ||
|
|
00d62fc23f | ||
|
|
aa87b78dde | ||
|
|
6c71bd40fb | ||
|
|
ed40043448 | ||
|
|
5cf7e6e24b | ||
|
|
720ef936da | ||
|
|
30755b2067 | ||
|
|
04f67c114e | ||
|
|
ea707a0bc5 | ||
|
|
f43475f33b | ||
|
|
739d4d0038 | ||
|
|
e756a77c70 | ||
|
|
bcfa5d0a7e | ||
|
|
45f92536a6 | ||
|
|
6b0b78d8e0 | ||
|
|
c336cdc5df | ||
|
|
6ea8d07c8f | ||
|
|
5c25a08dc1 | ||
|
|
fe7f109127 | ||
|
|
583819c4ae | ||
|
|
cb8da2e757 | ||
|
|
fdc96115e4 | ||
|
|
e019ec5ff7 | ||
|
|
e4838f6d2b | ||
|
|
10837e75b2 | ||
|
|
46590c3163 | ||
|
|
e64d5c5f17 | ||
|
|
0e0cc0ad16 | ||
|
|
8ff01ca979 | ||
|
|
9508a9afc6 | ||
|
|
704a0e3078 | ||
|
|
9bf9f2c611 | ||
|
|
71c869e65b | ||
|
|
2897fa4003 | ||
|
|
7f020857d1 | ||
|
|
2217a9304d | ||
|
|
5a389b4855 | ||
|
|
bdb9b7803c | ||
|
|
4622b3fe36 | ||
|
|
402afd15db | ||
|
|
82aca3bce4 | ||
|
|
756c6554c9 | ||
|
|
3b9753aaf4 | ||
|
|
4472ef20fe | ||
|
|
c152790011 | ||
|
|
4e3b8a5178 | ||
|
|
375a0ff208 | ||
|
|
57831f0eba | ||
|
|
c9a3f67121 | ||
|
|
6af1f98c88 | ||
|
|
8e35372aad | ||
|
|
0f4d285223 | ||
|
|
192e592cda | ||
|
|
1c2c1f286f | ||
|
|
6e25af9493 | ||
|
|
050927008a | ||
|
|
2fe5459c56 | ||
|
|
8fbbaf7fcb | ||
|
|
2f5bdc5cf9 | ||
|
|
17833a0bfc | ||
|
|
f4e71df946 | ||
|
|
be070b79af | ||
|
|
ef8eefd3b4 | ||
|
|
83f46f6b2b | ||
|
|
6b4bdf569c | ||
|
|
7a9f6e2a8e | ||
|
|
ce95ff65bd | ||
|
|
28e724da98 | ||
|
|
a43b027cde | ||
|
|
4b5e36ebf2 | ||
|
|
89c05cfcae | ||
|
|
f8569db21b | ||
|
|
34eba2655e | ||
|
|
1625860bd9 | ||
|
|
f3ddfb96f3 | ||
|
|
66e198cbb6 | ||
|
|
33c747a881 | ||
|
|
20d61d14e0 | ||
|
|
833de94ab0 | ||
|
|
c8d6250ada | ||
|
|
d38e1185bb | ||
|
|
fdb8ae0cb5 | ||
|
|
b57306beac | ||
|
|
af6e159644 | ||
|
|
54e50f69e1 | ||
|
|
3f415b8265 | ||
|
|
8ccdb56bf1 | ||
|
|
17ed957c6b | ||
|
|
e4564abe41 | ||
|
|
f16b29b16b | ||
|
|
ef8af7d618 | ||
|
|
79e33899a8 | ||
|
|
11fc220d4d | ||
|
|
a94a30168c | ||
|
|
19704920a4 | ||
|
|
e4f4c1f1be | ||
|
|
065931cae7 | ||
|
|
78443bffac | ||
|
|
a8b105267c | ||
|
|
f7bd637073 | ||
|
|
3e6f7f0fad | ||
|
|
e301b67e49 | ||
|
|
952d878442 | ||
|
|
8f66f94ffa | ||
|
|
d79acef59e | ||
|
|
e66a2a7c30 | ||
|
|
2f04b93fdb | ||
|
|
818e99b39d | ||
|
|
96ffe95404 | ||
|
|
438e53d25e | ||
|
|
ca4b0acd92 | ||
|
|
f8deb1bd7f | ||
|
|
d8de84e417 | ||
|
|
eb602aedc3 | ||
|
|
b539892cc0 | ||
|
|
ba13d2179d | ||
|
|
c7a315ac97 | ||
|
|
b1fb793ea4 | ||
|
|
62db9ad982 | ||
|
|
652c9943c2 | ||
|
|
9f62575abe | ||
|
|
2fd87f703e | ||
|
|
d3780cd9d5 | ||
|
|
0376705e47 | ||
|
|
f1fddac655 | ||
|
|
6acd08431e | ||
|
|
317f7116c4 | ||
|
|
bf8e99140e | ||
|
|
6c949c3a52 | ||
|
|
76d591bab5 | ||
|
|
d10cab824a | ||
|
|
a93d633d25 | ||
|
|
9ebab4a382 | ||
|
|
cd53dcfe43 | ||
|
|
87ceef230f | ||
|
|
a06e81a0ba | ||
|
|
59e87e0d27 | ||
|
|
76d1460d0f | ||
|
|
1985423a97 | ||
|
|
f5afc84cd2 | ||
|
|
1217179f8a | ||
|
|
29a207b73e | ||
|
|
f7ecf02beb | ||
|
|
c5193ffdd9 | ||
|
|
916ba2ea41 | ||
|
|
3348dce122 | ||
|
|
53e6ca6e34 | ||
|
|
0fed7f1295 | ||
|
|
6ade832029 | ||
|
|
50ba9a56f7 | ||
|
|
990141df47 | ||
|
|
50f7541ef7 | ||
|
|
6a6962b3b9 | ||
|
|
3314ad0315 | ||
|
|
9a4a96eedd | ||
|
|
ff4a9d1761 | ||
|
|
df2d4a557e | ||
|
|
d831923a54 | ||
|
|
594183d751 | ||
|
|
bddaa954ab | ||
|
|
f4a7777018 | ||
|
|
8fc9a9c55e | ||
|
|
8e457d9b8f | ||
|
|
aa37c9bf81 | ||
|
|
89cbd05600 | ||
|
|
a5e9c4af03 | ||
|
|
ea753cd8bf | ||
|
|
46e9fd7ae3 | ||
|
|
96d7277a22 | ||
|
|
c937167a11 | ||
|
|
0c59ad7e22 | ||
|
|
fa1b93252c | ||
|
|
0d9e186e18 | ||
|
|
b7aa5a17b7 | ||
|
|
d55a057a4d | ||
|
|
72976da3a4 | ||
|
|
81afbb55cf | ||
|
|
d1709764ef | ||
|
|
4f7e3d7a45 | ||
|
|
4ca53a6ee0 | ||
|
|
efe02e2591 | ||
|
|
391f42b4f2 | ||
|
|
cff5db446d | ||
|
|
858d4c74ce | ||
|
|
f56bf0db73 | ||
|
|
4801bb1178 | ||
|
|
8b2433584d | ||
|
|
bde02f696b | ||
|
|
0afbe7988e | ||
|
|
345d4c58f3 | ||
|
|
6c44ffaf7a | ||
|
|
16454dbc33 | ||
|
|
89c6fd6ac4 | ||
|
|
ea8b6e6438 | ||
|
|
c0b25e1f6e | ||
|
|
df0335f739 | ||
|
|
1ffe5fc7bb | ||
|
|
cf070e6dd9 | ||
|
|
f9a9189687 | ||
|
|
9daf1abcd9 | ||
|
|
8c525a5e33 | ||
|
|
952a155003 | ||
|
|
7f35f6f8f4 | ||
|
|
8b9e278593 | ||
|
|
655ebcdb07 | ||
|
|
ac534a6881 | ||
|
|
59529eba4e | ||
|
|
1cef10b309 | ||
|
|
c3070be14a | ||
|
|
5570440eb1 | ||
|
|
ec0a5df5a1 | ||
|
|
8411b76ee5 | ||
|
|
c0ff90fc86 | ||
|
|
f9c8816c43 | ||
|
|
822e8941ed | ||
|
|
7ac9bd8591 | ||
|
|
68a5784650 | ||
|
|
67f324b939 | ||
|
|
8db8c60e75 | ||
|
|
8e569a1d1f | ||
|
|
3caf8bc82b | ||
|
|
3da028415f | ||
|
|
104df1915d | ||
|
|
bfb6d44195 | ||
|
|
df0e8bc027 | ||
|
|
442b6ced35 | ||
|
|
111e11924f | ||
|
|
061cc69a6a | ||
|
|
f9950e1f01 | ||
|
|
895d259589 | ||
|
|
4ea80f34fa | ||
|
|
77878bf714 | ||
|
|
f85dde6323 | ||
|
|
6441f92c9f | ||
|
|
25b9fc8b6a | ||
|
|
090678776e | ||
|
|
9be6d443d7 | ||
|
|
678253d037 | ||
|
|
bd561fd191 | ||
|
|
38b5ee7314 | ||
|
|
11245462f0 | ||
|
|
351a5b87bf | ||
|
|
b780257098 | ||
|
|
4e1f1551ea | ||
|
|
b82e3f2a8a | ||
|
|
a82bf1bb32 | ||
|
|
abc0220cfa | ||
|
|
f17e6f9afd | ||
|
|
16e6b9eed7 | ||
|
|
323415ba9c | ||
|
|
ae97b5e704 | ||
|
|
6b8b30c3c7 | ||
|
|
0df2b2221d | ||
|
|
e2b36dfa7d | ||
|
|
4e18f24f3b | ||
|
|
b0d5a51768 | ||
|
|
b3d2c22373 | ||
|
|
cace88e8fa | ||
|
|
9c09d84c71 | ||
|
|
2d27665369 | ||
|
|
45266caa8d | ||
|
|
feb1a59902 | ||
|
|
fdec4157da | ||
|
|
4e84b20925 | ||
|
|
f952ad5913 | ||
|
|
be27586203 | ||
|
|
9dc3f3f38b | ||
|
|
f39defbe06 | ||
|
|
890f71a477 | ||
|
|
bc8e8c5daf | ||
|
|
37f12809a1 | ||
|
|
f5c0b847a9 | ||
|
|
44d6c3c07e | ||
|
|
da1a2b2957 | ||
|
|
9f6fa2bd05 | ||
|
|
5d68dc568f | ||
|
|
ee1ea881e8 | ||
|
|
87add88436 | ||
|
|
7643609e09 | ||
|
|
73727ab0d1 | ||
|
|
007a393ab5 | ||
|
|
4ed185a155 | ||
|
|
fbb220ce85 | ||
|
|
0c57d35402 | ||
|
|
8cc045f370 | ||
|
|
80c90c0a00 | ||
|
|
c1c92647ca | ||
|
|
033adceb6f | ||
|
|
e57e92bfee | ||
|
|
4d68000692 | ||
|
|
44b5423afc | ||
|
|
a1a7729c3b | ||
|
|
071b0eeb77 | ||
|
|
fafc17c7d3 | ||
|
|
7599302920 | ||
|
|
7f8d7231a4 | ||
|
|
b1196885d7 | ||
|
|
494cfb3c04 | ||
|
|
6a65981103 | ||
|
|
f508f93d69 | ||
|
|
411d4434a3 | ||
|
|
d41fce6f91 | ||
|
|
282e7b4006 | ||
|
|
b4c3c5deea | ||
|
|
683514d891 | ||
|
|
e9beb21a98 | ||
|
|
bc47f78264 | ||
|
|
b002f7f862 | ||
|
|
05dac999a8 | ||
|
|
242595b725 | ||
|
|
48dd1a1aa6 | ||
|
|
3aacaffe6b | ||
|
|
61b875256f | ||
|
|
14dc450631 | ||
|
|
6352056528 | ||
|
|
bd4f24844b | ||
|
|
062615b6b1 | ||
|
|
6c9293e4f6 | ||
|
|
24802d64c7 | ||
|
|
5e8a686bb6 | ||
|
|
279ab89a61 | ||
|
|
29ed40051d | ||
|
|
8d05aa6262 | ||
|
|
694f942c06 | ||
|
|
105a2d4e13 | ||
|
|
1ee62912fd | ||
|
|
abacca34ee | ||
|
|
3e4e69735e | ||
|
|
4afc351933 | ||
|
|
23b8070b9d | ||
|
|
e53b5324f5 | ||
|
|
25bbbdbecd | ||
|
|
d739d04380 | ||
|
|
f7da0265c4 | ||
|
|
82ae21420d | ||
|
|
89984a0d09 | ||
|
|
fc62b4e0bd | ||
|
|
2e2ca1665b | ||
|
|
1b27fc495f | ||
|
|
51c38fc628 | ||
|
|
74c30ce09a | ||
|
|
859316353e | ||
|
|
63c9bea724 | ||
|
|
df435eb693 | ||
|
|
c73b994305 | ||
|
|
88451d4239 | ||
|
|
f74db254f6 | ||
|
|
3cb0a22e17 | ||
|
|
ca3e01b15e | ||
|
|
e9d1dcc46c | ||
|
|
7fd0f1a5bf | ||
|
|
2d65fbf798 | ||
|
|
ac915d00fc | ||
|
|
fbb8d6b132 | ||
|
|
fb0f70b3e3 | ||
|
|
17929415ee | ||
|
|
631b6788c6 | ||
|
|
7972aa6320 | ||
|
|
138c884684 | ||
|
|
f5ef98287a | ||
|
|
5188b41ab0 | ||
|
|
f83ba6e615 | ||
|
|
cc2a72eb82 | ||
|
|
4fcce66505 | ||
|
|
66627d8a66 | ||
|
|
adfd68f83c | ||
|
|
ddc619f2e7 | ||
|
|
ff2e57705e | ||
|
|
a6a859b272 | ||
|
|
88c5ebdd2f | ||
|
|
3d578bcc98 | ||
|
|
c3290af2bd | ||
|
|
01f1545b3e | ||
|
|
fc8e849db5 | ||
|
|
9115e59f15 | ||
|
|
2f4b248a45 | ||
|
|
2f28afb46e | ||
|
|
e960d7b58c | ||
|
|
321569c542 | ||
|
|
df037c54ff | ||
|
|
d859cecffb | ||
|
|
fd6e009c4b | ||
|
|
4520051ec9 | ||
|
|
b90b73859a | ||
|
|
6c357b61cc | ||
|
|
12957db90f | ||
|
|
3c74f561d5 | ||
|
|
cc70a6fa26 | ||
|
|
1c42564d90 | ||
|
|
e76c870c09 | ||
|
|
5daadcb2d5 | ||
|
|
a124a7a82a | ||
|
|
a65bf60cea | ||
|
|
3fa28a3fdb | ||
|
|
baa7992a7a | ||
|
|
7ba4bfc0d5 | ||
|
|
11fedef2f5 | ||
|
|
944347a2b3 | ||
|
|
8c72b0a6c4 | ||
|
|
5d62d4e063 | ||
|
|
9b05537a0e | ||
|
|
fd0a87626e | ||
|
|
9402d82405 | ||
|
|
da6674760c | ||
|
|
ee03371dd0 | ||
|
|
a975c8fd00 | ||
|
|
60840da740 | ||
|
|
de567cc701 | ||
|
|
de4775b0c8 | ||
|
|
104cc0ea83 | ||
|
|
5bb8de500a | ||
|
|
21255b3b46 | ||
|
|
e8da9924c6 | ||
|
|
96b38aba04 | ||
|
|
b8b51965d2 | ||
|
|
be46d128bc | ||
|
|
c05f1ed24f | ||
|
|
99775ec1bd | ||
|
|
f4f043ac87 | ||
|
|
acbca78e2d | ||
|
|
30ac7baa2c | ||
|
|
21a5170337 | ||
|
|
3a5a6a096b | ||
|
|
578ae70150 | ||
|
|
57282e76a4 | ||
|
|
7aaa652ef5 | ||
|
|
81da0d2ba4 | ||
|
|
ce6cdcaf92 | ||
|
|
4730a928b5 | ||
|
|
4c0f0a16c9 | ||
|
|
b07fc80f3f | ||
|
|
6a3d1fcaf4 | ||
|
|
4aeb3cd3dc | ||
|
|
6dc2000638 | ||
|
|
72610d8c2f | ||
|
|
0f55fa4f45 | ||
|
|
aec39c919c | ||
|
|
a0849f9416 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,9 +4,14 @@
|
||||
*_Spoiler.txt
|
||||
*.bmbp
|
||||
*.apbp
|
||||
*.apm3
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.sfc
|
||||
*.z64
|
||||
*.n64
|
||||
*.wixobj
|
||||
*.lck
|
||||
*.db3
|
||||
@@ -37,6 +42,7 @@ success.txt
|
||||
output/
|
||||
Output Logs/
|
||||
/factorio/
|
||||
/Minecraft Forge Server/
|
||||
/WebHostLib/static/generated
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
539
BaseClasses.py
539
BaseClasses.py
@@ -6,10 +6,13 @@ import logging
|
||||
import json
|
||||
import functools
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
|
||||
import secrets
|
||||
import random
|
||||
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
@@ -22,9 +25,9 @@ class MultiWorld():
|
||||
plando_texts: List[Dict[str, str]]
|
||||
plando_items: List
|
||||
plando_connections: List
|
||||
er_seeds: Dict[int, str]
|
||||
worlds: Dict[int, Any]
|
||||
is_race: bool = False
|
||||
precollected_items: Dict[int, List[Item]]
|
||||
|
||||
class AttributeProxy():
|
||||
def __init__(self, rule):
|
||||
@@ -38,13 +41,13 @@ class MultiWorld():
|
||||
self.players = players
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.dungeons = []
|
||||
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
|
||||
self.regions = []
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
self.seed = None
|
||||
self.seed_name: str = "Unavailable"
|
||||
self.precollected_items = []
|
||||
self.precollected_items = {player: [] for player in self.player_ids}
|
||||
self.state = CollectionState(self)
|
||||
self._cached_entrances = None
|
||||
self._cached_locations = None
|
||||
@@ -63,28 +66,29 @@ 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.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])
|
||||
|
||||
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('swordless', False)
|
||||
set_player_attr('difficulty', 'normal')
|
||||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
set_player_attr('goal', 'ganon')
|
||||
set_player_attr('accessibility', 'items')
|
||||
set_player_attr('retro', False)
|
||||
set_player_attr('hints', True)
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
set_player_attr('powder_patch_required', False)
|
||||
@@ -95,19 +99,12 @@ class MultiWorld():
|
||||
set_player_attr('can_access_trock_big_chest', None)
|
||||
set_player_attr('can_access_trock_middle', None)
|
||||
set_player_attr('fix_fake_world', True)
|
||||
set_player_attr('mapshuffle', False)
|
||||
set_player_attr('compassshuffle', False)
|
||||
set_player_attr('keyshuffle', False)
|
||||
set_player_attr('bigkeyshuffle', False)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
set_player_attr('enemy_shuffle', False)
|
||||
set_player_attr('enemy_health', 'default')
|
||||
set_player_attr('enemy_damage', 'default')
|
||||
set_player_attr('killable_thieves', False)
|
||||
set_player_attr('tile_shuffle', False)
|
||||
set_player_attr('bush_shuffle', False)
|
||||
set_player_attr('beemizer', 0)
|
||||
set_player_attr('beemizer_total_chance', 0)
|
||||
set_player_attr('beemizer_trap_chance', 0)
|
||||
set_player_attr('escape_assist', [])
|
||||
set_player_attr('open_pyramid', False)
|
||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||
@@ -118,17 +115,12 @@ class MultiWorld():
|
||||
set_player_attr('blue_clock_time', 2)
|
||||
set_player_attr('green_clock_time', 4)
|
||||
set_player_attr('can_take_damage', True)
|
||||
set_player_attr('glitch_boots', True)
|
||||
set_player_attr('progression_balancing', True)
|
||||
set_player_attr('local_items', set())
|
||||
set_player_attr('non_local_items', set())
|
||||
set_player_attr('triforce_pieces_available', 30)
|
||||
set_player_attr('triforce_pieces_required', 20)
|
||||
set_player_attr('shop_shuffle', 'off')
|
||||
set_player_attr('shuffle_prizes', "g")
|
||||
set_player_attr('sprite_pool', [])
|
||||
set_player_attr('dark_room_logic', "lamp")
|
||||
set_player_attr('restrict_dungeon_item_on_boss', False)
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
set_player_attr('plando_connections', [])
|
||||
@@ -136,16 +128,38 @@ class MultiWorld():
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.custom_data = {}
|
||||
self.worlds = {}
|
||||
self.slot_seeds = {}
|
||||
|
||||
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||
self.seed = get_seed(seed)
|
||||
if secure:
|
||||
self.secure()
|
||||
else:
|
||||
self.random.seed(self.seed)
|
||||
self.seed_name = name if name else str(self.seed)
|
||||
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in
|
||||
range(1, self.players + 1)}
|
||||
|
||||
def set_options(self, args):
|
||||
from worlds import AutoWorld
|
||||
for player in self.player_ids:
|
||||
self.custom_data[player] = {}
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
for option in world_type.options:
|
||||
setattr(self, option, getattr(args, option, {}))
|
||||
for option_key in world_type.options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.per_game_common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
self.worlds[player] = world_type(self, player)
|
||||
|
||||
# intended for unittests
|
||||
def set_default_common_options(self):
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
self.is_race = True
|
||||
@@ -158,6 +172,10 @@ class MultiWorld():
|
||||
def get_game_players(self, game_name: str):
|
||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_game_worlds(self, game_name: str):
|
||||
return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
@@ -209,42 +227,29 @@ class MultiWorld():
|
||||
return self._location_cache[location, player]
|
||||
|
||||
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
||||
for dungeon in self.dungeons:
|
||||
if dungeon.name == dungeonname and dungeon.player == player:
|
||||
return dungeon
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player))
|
||||
try:
|
||||
return self.dungeons[dungeonname, player]
|
||||
except KeyError as e:
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
|
||||
|
||||
def get_all_state(self, keys=False) -> CollectionState:
|
||||
key = f"_all_state_{keys}"
|
||||
cached = getattr(self, key, None)
|
||||
if cached:
|
||||
def get_all_state(self, use_cache: bool) -> CollectionState:
|
||||
cached = getattr(self, "_all_state", None)
|
||||
if use_cache and cached:
|
||||
return cached.copy()
|
||||
|
||||
ret = CollectionState(self)
|
||||
|
||||
for item in self.itempool:
|
||||
self.worlds[item.player].collect(ret, item)
|
||||
|
||||
if keys:
|
||||
for p in self.get_game_players("A Link to the Past"):
|
||||
world = self.worlds[p]
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
for item in ItemFactory(
|
||||
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
||||
'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)',
|
||||
'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
|
||||
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [
|
||||
'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [
|
||||
'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
|
||||
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [
|
||||
'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)',
|
||||
'Big Key (Ganons Tower)'] + [
|
||||
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
||||
'Small Key (Ganons Tower)'] * 4,
|
||||
p):
|
||||
world.collect(ret, item)
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
for item in get_dungeon_item_pool(self):
|
||||
subworld = self.worlds[item.player]
|
||||
if item.name in subworld.dungeon_local_item_names:
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
setattr(self, key, ret)
|
||||
|
||||
if use_cache:
|
||||
self._all_state = ret
|
||||
return ret
|
||||
|
||||
def get_items(self) -> list:
|
||||
@@ -263,7 +268,7 @@ class MultiWorld():
|
||||
|
||||
def push_precollected(self, item: Item):
|
||||
item.world = self
|
||||
self.precollected_items.append(item)
|
||||
self.precollected_items[item.player].append(item)
|
||||
self.state.collect(item, True)
|
||||
|
||||
def push_item(self, location: Location, item: Item, collect: bool = True):
|
||||
@@ -282,7 +287,7 @@ class MultiWorld():
|
||||
else:
|
||||
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
|
||||
|
||||
def get_entrances(self) -> list:
|
||||
def get_entrances(self) -> List[Entrance]:
|
||||
if self._cached_entrances is None:
|
||||
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
|
||||
return self._cached_entrances
|
||||
@@ -290,7 +295,7 @@ class MultiWorld():
|
||||
def clear_entrance_cache(self):
|
||||
self._cached_entrances = None
|
||||
|
||||
def get_locations(self) -> list:
|
||||
def get_locations(self) -> List[Location]:
|
||||
if self._cached_locations is None:
|
||||
self._cached_locations = [location for region in self.regions for location in region.locations]
|
||||
return self._cached_locations
|
||||
@@ -298,7 +303,7 @@ class MultiWorld():
|
||||
def clear_location_cache(self):
|
||||
self._cached_locations = None
|
||||
|
||||
def get_unfilled_locations(self, player=None) -> list:
|
||||
def get_unfilled_locations(self, player=None) -> List[Location]:
|
||||
if player is not None:
|
||||
return [location for location in self.get_locations() if
|
||||
location.player == player and not location.item]
|
||||
@@ -307,19 +312,19 @@ class MultiWorld():
|
||||
def get_unfilled_dungeon_locations(self):
|
||||
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
|
||||
|
||||
def get_filled_locations(self, player=None) -> list:
|
||||
def get_filled_locations(self, player=None) -> List[Location]:
|
||||
if player is not None:
|
||||
return [location for location in self.get_locations() if
|
||||
location.player == player and location.item is not None]
|
||||
return [location for location in self.get_locations() if location.item is not None]
|
||||
|
||||
def get_reachable_locations(self, state=None, player=None) -> list:
|
||||
def get_reachable_locations(self, state=None, player=None) -> List[Location]:
|
||||
if state is None:
|
||||
state = self.state
|
||||
return [location for location in self.get_locations() if
|
||||
(player is None or location.player == player) and location.can_reach(state)]
|
||||
|
||||
def get_placeable_locations(self, state=None, player=None) -> list:
|
||||
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
|
||||
if state is None:
|
||||
state = self.state
|
||||
return [location for location in self.get_locations() if
|
||||
@@ -347,7 +352,7 @@ class MultiWorld():
|
||||
else:
|
||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||
|
||||
def can_beat_game(self, starting_state : Optional[CollectionState]=None):
|
||||
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
|
||||
if starting_state:
|
||||
if self.has_beaten_game(starting_state):
|
||||
return True
|
||||
@@ -404,21 +409,21 @@ class MultiWorld():
|
||||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||
if not state:
|
||||
state = CollectionState(self)
|
||||
players = {"none" : set(),
|
||||
players = {"minimal": set(),
|
||||
"items": set(),
|
||||
"locations": set()}
|
||||
for player, access in self.accessibility.items():
|
||||
players[access].add(player)
|
||||
players[access.current_key].add(player)
|
||||
|
||||
beatable_fulfilled = False
|
||||
|
||||
def location_conditition(location : Location):
|
||||
def location_conditition(location: Location):
|
||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||
if location.player in players["none"]:
|
||||
if location.player in players["minimal"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def location_relevant(location : Location):
|
||||
def location_relevant(location: Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
if location.player in players["locations"] or location.event or \
|
||||
(location.item and location.item.advancement):
|
||||
@@ -470,8 +475,9 @@ class CollectionState(object):
|
||||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||
for item in parent.precollected_items:
|
||||
self.collect(item, True)
|
||||
for items in parent.precollected_items.values():
|
||||
for item in items:
|
||||
self.collect(item, True)
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
from worlds.alttp.EntranceShuffle import indirect_connections
|
||||
@@ -511,7 +517,8 @@ class CollectionState(object):
|
||||
ret.prog_items = self.prog_items.copy()
|
||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.events = copy.copy(self.events)
|
||||
ret.path = copy.copy(self.path)
|
||||
ret.locations_checked = copy.copy(self.locations_checked)
|
||||
@@ -537,9 +544,7 @@ class CollectionState(object):
|
||||
locations = {location for location in locations if location.event}
|
||||
while new_locations:
|
||||
reachable_events = {location for location in locations if
|
||||
(not key_only or
|
||||
(not self.world.keyshuffle[location.item.player] and location.item.smallkey)
|
||||
or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
(not key_only or getattr(location.item, "locked_dungeon_item", False))
|
||||
and location.can_reach(self)}
|
||||
new_locations = reachable_events - self.events
|
||||
for event in new_locations:
|
||||
@@ -549,10 +554,10 @@ class CollectionState(object):
|
||||
def has(self, item, player: int, count: int = 1):
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def has_all(self, items: Set[str], player:int):
|
||||
def has_all(self, items: Set[str], player: int):
|
||||
return all(self.prog_items[item, player] for item in items)
|
||||
|
||||
def has_any(self, items: Set[str], player:int):
|
||||
def has_any(self, items: Set[str], player: int):
|
||||
return any(self.prog_items[item, player] for item in items)
|
||||
|
||||
def has_group(self, item_name_group: str, player: int, count: int = 1):
|
||||
@@ -569,13 +574,6 @@ class CollectionState(object):
|
||||
found += self.prog_items[item_name, player]
|
||||
return found
|
||||
|
||||
def has_key(self, item, player, count: int = 1):
|
||||
if self.world.logic[player] == 'nologic':
|
||||
return True
|
||||
if self.world.keyshuffle[player] == "universal":
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
||||
shop in self.world.shops)
|
||||
@@ -659,10 +657,10 @@ class CollectionState(object):
|
||||
self.is_not_bunny(cave, player)
|
||||
)
|
||||
|
||||
def can_retrieve_tablet(self, player:int) -> bool:
|
||||
def can_retrieve_tablet(self, player: int) -> bool:
|
||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
||||
(self.world.swordless[player] and
|
||||
self.has("Hammer", player)))
|
||||
(self.world.swordless[player] and
|
||||
self.has("Hammer", player)))
|
||||
|
||||
def has_sword(self, player: int) -> bool:
|
||||
return self.has('Fighter Sword', player) \
|
||||
@@ -671,7 +669,8 @@ class CollectionState(object):
|
||||
or self.has('Golden Sword', player)
|
||||
|
||||
def has_beam_sword(self, player: int) -> bool:
|
||||
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
|
||||
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
|
||||
player)
|
||||
|
||||
def has_melee_weapon(self, player: int) -> bool:
|
||||
return self.has_sword(player) or self.has('Hammer', player)
|
||||
@@ -735,7 +734,7 @@ class CollectionState(object):
|
||||
rules.append(self.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
|
||||
def can_bomb_clip(self, region: Region, player: int) -> bool:
|
||||
def can_bomb_clip(self, region: Region, player: int) -> bool:
|
||||
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
|
||||
|
||||
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
||||
@@ -763,12 +762,13 @@ class CollectionState(object):
|
||||
self.blocked_connections[item.player] = set()
|
||||
self.stale[item.player] = True
|
||||
|
||||
|
||||
@unique
|
||||
class RegionType(int, Enum):
|
||||
Generic = 0
|
||||
LightWorld = 1
|
||||
DarkWorld = 2
|
||||
Cave = 3 # Also includes Houses
|
||||
Cave = 3 # Also includes Houses
|
||||
Dungeon = 4
|
||||
|
||||
@property
|
||||
@@ -807,13 +807,6 @@ class Region(object):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_fill(self, item: Item):
|
||||
inside_dungeon_item = item.locked_dungeon_item
|
||||
if inside_dungeon_item:
|
||||
return self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -855,8 +848,8 @@ class Entrance(object):
|
||||
world = self.parent_region.world if self.parent_region else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
|
||||
class Dungeon(object):
|
||||
|
||||
class Dungeon(object):
|
||||
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
|
||||
self.name = name
|
||||
self.regions = regions
|
||||
@@ -897,6 +890,7 @@ class Dungeon(object):
|
||||
def __str__(self):
|
||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
class Boss():
|
||||
def __init__(self, name, enemizer_name, defeat_rule, player: int):
|
||||
self.name = name
|
||||
@@ -910,19 +904,23 @@ class Boss():
|
||||
def __repr__(self):
|
||||
return f"Boss({self.name})"
|
||||
|
||||
|
||||
class Location():
|
||||
shop_slot: bool = False
|
||||
# If given as integer, then this is the shop's inventory index
|
||||
shop_slot: Optional[int] = None
|
||||
shop_slot_disabled: bool = False
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
spot_type = 'Location'
|
||||
game: str = "Generic"
|
||||
show_in_spoiler: bool = True
|
||||
excluded: bool = False
|
||||
crystal: bool = False
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
access_rule = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
|
||||
def __init__(self, player: int, name: str = '', address:int = None, parent=None):
|
||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
||||
self.name: str = name
|
||||
self.address: Optional[int] = address
|
||||
self.parent_region: Region = parent
|
||||
@@ -930,7 +928,7 @@ class Location():
|
||||
self.item: Optional[Item] = None
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# self.access_rule computes faster on average, so placing it first for faster abort
|
||||
@@ -966,21 +964,36 @@ class Location():
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
hint_text = getattr(self, "_hint_text", None)
|
||||
if hint_text:
|
||||
return hint_text
|
||||
return "at " + self.name.replace("_", " ").replace("-", " ")
|
||||
|
||||
|
||||
class Item():
|
||||
location: Optional[Location] = None
|
||||
world: Optional[MultiWorld] = None
|
||||
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
name: str
|
||||
game: str = "Generic"
|
||||
type: str = None
|
||||
never_exclude = False # change manually to ensure that a specific nonprogression item never goes on an excluded location
|
||||
# indicates if this is a negative impact item. Causes these to be handled differently by various games.
|
||||
trap: bool = False
|
||||
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
||||
never_exclude = False
|
||||
|
||||
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
||||
pedestal_credit_text: str = "and the Unknown Item"
|
||||
sickkid_credit_text: Optional[str] = None
|
||||
magicshop_credit_text: Optional[str] = None
|
||||
zora_credit_text: Optional[str] = None
|
||||
fluteboy_credit_text: Optional[str] = None
|
||||
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
|
||||
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
|
||||
smallkey: bool = False
|
||||
bigkey: bool = False
|
||||
map: bool = False
|
||||
compass: bool = False
|
||||
|
||||
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
|
||||
self.name = name
|
||||
@@ -1007,51 +1020,6 @@ class Item():
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
def smallkey(self) -> bool:
|
||||
return self.type == 'SmallKey'
|
||||
|
||||
@property
|
||||
def bigkey(self) -> bool:
|
||||
return self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def map(self) -> bool:
|
||||
return self.type == 'Map'
|
||||
|
||||
@property
|
||||
def compass(self) -> bool:
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def dungeon_item(self) -> Optional[str]:
|
||||
if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def shuffled_dungeon_item(self) -> bool:
|
||||
dungeon_item_type = self.dungeon_item
|
||||
if dungeon_item_type:
|
||||
return {"SmallKey" : self.world.keyshuffle,
|
||||
"BigKey": self.world.bigkeyshuffle,
|
||||
"Map": self.world.mapshuffle,
|
||||
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
|
||||
return False
|
||||
|
||||
@property
|
||||
def locked_dungeon_item(self) -> bool:
|
||||
dungeon_item_type = self.dungeon_item
|
||||
if dungeon_item_type:
|
||||
return not {"SmallKey" : self.world.keyshuffle,
|
||||
"BigKey": self.world.bigkeyshuffle,
|
||||
"Map": self.world.mapshuffle,
|
||||
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -1069,67 +1037,87 @@ class Spoiler():
|
||||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = []
|
||||
self.startinventory = []
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
self.metadata = {}
|
||||
self.shops = []
|
||||
self.bosses = OrderedDict()
|
||||
|
||||
def set_entrance(self, entrance, exit, direction, player):
|
||||
if self.world.players == 1:
|
||||
self.entrances[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
|
||||
self.entrances[(entrance, direction, player)] = OrderedDict(
|
||||
[('entrance', entrance), ('exit', exit), ('direction', direction)])
|
||||
else:
|
||||
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
||||
self.entrances[(entrance, direction, player)] = OrderedDict(
|
||||
[('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
||||
|
||||
def parse_data(self):
|
||||
self.medallions = OrderedDict()
|
||||
for player in self.world.get_game_players("A Link to the Past"):
|
||||
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
|
||||
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
|
||||
|
||||
self.startinventory = list(map(str, self.world.precollected_items))
|
||||
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = \
|
||||
self.world.required_medallions[player][0]
|
||||
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = \
|
||||
self.world.required_medallions[player][1]
|
||||
|
||||
self.locations = OrderedDict()
|
||||
listed_locations = set()
|
||||
|
||||
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld]
|
||||
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
|
||||
lw_locations = [loc for loc in self.world.get_locations() if
|
||||
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
|
||||
self.locations['Light World'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
lw_locations])
|
||||
listed_locations.update(lw_locations)
|
||||
|
||||
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld]
|
||||
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
|
||||
dw_locations = [loc for loc in self.world.get_locations() if
|
||||
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
|
||||
self.locations['Dark World'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
dw_locations])
|
||||
listed_locations.update(dw_locations)
|
||||
|
||||
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave]
|
||||
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
|
||||
cave_locations = [loc for loc in self.world.get_locations() if
|
||||
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
|
||||
self.locations['Caves'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
cave_locations])
|
||||
listed_locations.update(cave_locations)
|
||||
|
||||
for dungeon in self.world.dungeons:
|
||||
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon]
|
||||
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
|
||||
for dungeon in self.world.dungeons.values():
|
||||
dungeon_locations = [loc for loc in self.world.get_locations() if
|
||||
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
|
||||
self.locations[str(dungeon)] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
dungeon_locations])
|
||||
listed_locations.update(dungeon_locations)
|
||||
|
||||
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations]
|
||||
other_locations = [loc for loc in self.world.get_locations() if
|
||||
loc not in listed_locations and loc.show_in_spoiler]
|
||||
if other_locations:
|
||||
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
|
||||
self.locations['Other Locations'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
other_locations])
|
||||
listed_locations.update(other_locations)
|
||||
|
||||
self.shops = []
|
||||
from worlds.alttp.Shops import ShopType
|
||||
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
|
||||
for shop in self.world.shops:
|
||||
if not shop.custom:
|
||||
continue
|
||||
shopdata = {'location': str(shop.region),
|
||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||
}
|
||||
shopdata = {
|
||||
'location': str(shop.region),
|
||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||
}
|
||||
for index, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
shopdata['item_{}'.format(index)] = "{} — {}".format(item['item'], item['price']) if item['price'] else item['item']
|
||||
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
|
||||
shopdata['item_{}'.format(
|
||||
index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}"
|
||||
|
||||
if item['player'] > 0:
|
||||
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—', '(Player {}) — '.format(item['player']))
|
||||
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—',
|
||||
'(Player {}) — '.format(
|
||||
item['player']))
|
||||
|
||||
if item['max'] == 0:
|
||||
continue
|
||||
@@ -1137,7 +1125,8 @@ class Spoiler():
|
||||
|
||||
if item['replacement'] is None:
|
||||
continue
|
||||
shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
|
||||
shopdata['item_{}'.format(
|
||||
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
|
||||
self.shops.append(shopdata)
|
||||
|
||||
for player in self.world.get_game_players("A Link to the Past"):
|
||||
@@ -1146,7 +1135,8 @@ class Spoiler():
|
||||
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
|
||||
self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name
|
||||
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
||||
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name
|
||||
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness",
|
||||
player).boss.name
|
||||
self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name
|
||||
self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name
|
||||
self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name
|
||||
@@ -1154,62 +1144,28 @@ class Spoiler():
|
||||
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
|
||||
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
|
||||
if self.world.mode[player] != 'inverted':
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
||||
self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses[
|
||||
'middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses[
|
||||
'top'].name
|
||||
else:
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
||||
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = \
|
||||
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = \
|
||||
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
||||
|
||||
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
||||
self.bosses[str(player)]["Ganon"] = "Ganon"
|
||||
|
||||
from Utils import __version__ as APVersion
|
||||
self.metadata = {'version': APVersion,
|
||||
'logic': self.world.logic,
|
||||
'dark_room_logic': self.world.dark_room_logic,
|
||||
'mode': self.world.mode,
|
||||
'retro': self.world.retro,
|
||||
'swordless': self.world.swordless,
|
||||
'goal': self.world.goal,
|
||||
'shuffle': self.world.shuffle,
|
||||
'item_pool': self.world.difficulty,
|
||||
'item_functionality': self.world.item_functionality,
|
||||
'open_pyramid': self.world.open_pyramid,
|
||||
'accessibility': self.world.accessibility,
|
||||
'hints': self.world.hints,
|
||||
'mapshuffle': self.world.mapshuffle,
|
||||
'compassshuffle': self.world.compassshuffle,
|
||||
'keyshuffle': self.world.keyshuffle,
|
||||
'bigkeyshuffle': self.world.bigkeyshuffle,
|
||||
'boss_shuffle': self.world.boss_shuffle,
|
||||
'enemy_shuffle': self.world.enemy_shuffle,
|
||||
'enemy_health': self.world.enemy_health,
|
||||
'enemy_damage': self.world.enemy_damage,
|
||||
'killable_thieves': self.world.killable_thieves,
|
||||
'tile_shuffle': self.world.tile_shuffle,
|
||||
'bush_shuffle': self.world.bush_shuffle,
|
||||
'beemizer': self.world.beemizer,
|
||||
'shufflepots': self.world.shufflepots,
|
||||
'players': self.world.players,
|
||||
'progression_balancing': self.world.progression_balancing,
|
||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||
'shop_shuffle': self.world.shop_shuffle,
|
||||
'shuffle_prizes': self.world.shuffle_prizes,
|
||||
'sprite_pool': self.world.sprite_pool,
|
||||
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
||||
'game': self.world.game,
|
||||
'er_seeds': self.world.er_seeds
|
||||
}
|
||||
|
||||
def to_json(self):
|
||||
self.parse_data()
|
||||
out = OrderedDict()
|
||||
out['Entrances'] = list(self.entrances.values())
|
||||
out.update(self.locations)
|
||||
out['Starting Inventory'] = self.startinventory
|
||||
out['Special'] = self.medallions
|
||||
if self.hashes:
|
||||
out['Hashes'] = self.hashes
|
||||
@@ -1218,11 +1174,11 @@ class Spoiler():
|
||||
out['playthrough'] = self.playthrough
|
||||
out['paths'] = self.paths
|
||||
out['Bosses'] = self.bosses
|
||||
out['meta'] = self.metadata
|
||||
|
||||
return json.dumps(out)
|
||||
|
||||
def to_file(self, filename):
|
||||
from worlds.AutoWorld import call_all, call_single, call_stage
|
||||
self.parse_data()
|
||||
|
||||
def bool_to_text(variable: Union[bool, str]) -> str:
|
||||
@@ -1230,88 +1186,71 @@ class Spoiler():
|
||||
return variable
|
||||
return 'Yes' if variable else 'No'
|
||||
|
||||
def write_option(option_key: str, option_obj: type(Options.Option)):
|
||||
res = getattr(self.world, option_key)[player]
|
||||
displayname = getattr(option_obj, "displayname", option_key)
|
||||
try:
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
except:
|
||||
raise Exception
|
||||
|
||||
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
||||
outfile.write(
|
||||
'Archipelago Version %s - Seed: %s\n\n' % (
|
||||
self.metadata['version'], self.world.seed))
|
||||
Utils.__version__, self.world.seed))
|
||||
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
||||
outfile.write('Players: %d\n' % self.world.players)
|
||||
call_stage(self.world, "write_spoiler_header", outfile)
|
||||
|
||||
for player in range(1, self.world.players + 1):
|
||||
if self.world.players > 1:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.world.game[player])
|
||||
if self.world.players > 1:
|
||||
outfile.write('Progression Balanced: %s\n' % (
|
||||
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||
for f_option, option in Options.common_options.items():
|
||||
write_option(f_option, option)
|
||||
for f_option, option in Options.per_game_common_options.items():
|
||||
write_option(f_option, option)
|
||||
options = self.world.worlds[player].options
|
||||
if options:
|
||||
for f_option, option in options.items():
|
||||
res = getattr(self.world, f_option)[player]
|
||||
displayname = getattr(option, "displayname", f_option)
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
write_option(f_option, option)
|
||||
call_single(self.world, "write_spoiler_header", player, outfile)
|
||||
|
||||
if player in self.world.get_game_players("A Link to the Past"):
|
||||
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
|
||||
|
||||
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
|
||||
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
|
||||
outfile.write('Restricted Boss Drops: %s\n' %
|
||||
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player]))
|
||||
|
||||
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||
outfile.write('Retro: %s\n' %
|
||||
('Yes' if self.metadata['retro'][player] else 'No'))
|
||||
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('Logic: %s\n' % self.world.logic[player])
|
||||
outfile.write('Dark Room Logic: %s\n' % self.world.dark_room_logic[player])
|
||||
outfile.write('Mode: %s\n' % self.world.mode[player])
|
||||
outfile.write('Goal: %s\n' % self.world.goal[player])
|
||||
if "triforce" in self.world.goal[player]: # triforce hunt
|
||||
outfile.write("Pieces available for Triforce: %s\n" %
|
||||
self.metadata['triforce_pieces_available'][player])
|
||||
self.world.triforce_pieces_available[player])
|
||||
outfile.write("Pieces required for Triforce: %s\n" %
|
||||
self.metadata["triforce_pieces_required"][player])
|
||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||
if self.metadata['shuffle'][player] != "vanilla":
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||
self.world.triforce_pieces_required[player])
|
||||
outfile.write('Difficulty: %s\n' % self.world.difficulty[player])
|
||||
outfile.write('Item Functionality: %s\n' % self.world.item_functionality[player])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
|
||||
if self.world.shuffle[player] != "vanilla":
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
|
||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
|
||||
outfile.write('Map shuffle: %s\n' %
|
||||
('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
||||
outfile.write('Compass shuffle: %s\n' %
|
||||
('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
||||
outfile.write(
|
||||
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
|
||||
outfile.write('Big Key shuffle: %s\n' % (
|
||||
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
||||
'Yes' if self.world.open_pyramid[player] else 'No'))
|
||||
outfile.write('Shop inventory shuffle: %s\n' %
|
||||
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("i" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Shop price shuffle: %s\n' %
|
||||
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("p" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Shop upgrade shuffle: %s\n' %
|
||||
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("u" in self.world.shop_shuffle[player]))
|
||||
outfile.write('New Shop inventory: %s\n' %
|
||||
bool_to_text("g" in self.metadata["shop_shuffle"][player] or
|
||||
"f" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("g" in self.world.shop_shuffle[player] or
|
||||
"f" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||
outfile.write(
|
||||
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
||||
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
||||
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
||||
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"][player])}\n')
|
||||
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"][player])}\n')
|
||||
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"][player])}\n')
|
||||
outfile.write(
|
||||
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
|
||||
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
|
||||
outfile.write('Pot shuffle %s\n'
|
||||
% ('Yes' if self.metadata['shufflepots'][player] else 'No'))
|
||||
bool_to_text("w" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
|
||||
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
|
||||
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
|
||||
outfile.write('Prize shuffle %s\n' %
|
||||
self.metadata['shuffle_prizes'][player])
|
||||
self.world.shuffle_prizes[player])
|
||||
if self.entrances:
|
||||
outfile.write('\n\nEntrances:\n\n')
|
||||
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
|
||||
@@ -1324,35 +1263,34 @@ class Spoiler():
|
||||
outfile.write('\n\nMedallions:\n')
|
||||
for dungeon, medallion in self.medallions.items():
|
||||
outfile.write(f'\n{dungeon}: {medallion}')
|
||||
factorio_players = self.world.get_game_players("Factorio")
|
||||
if factorio_players:
|
||||
outfile.write('\n\nRecipes:\n')
|
||||
for player in factorio_players:
|
||||
name = self.world.get_player_name(player)
|
||||
for recipe in self.world.worlds[player].custom_recipes.values():
|
||||
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
|
||||
|
||||
if self.startinventory:
|
||||
outfile.write('\n\nStarting Inventory:\n\n')
|
||||
outfile.write('\n'.join(self.startinventory))
|
||||
call_all(self.world, "write_spoiler", outfile)
|
||||
|
||||
outfile.write('\n\nLocations:\n\n')
|
||||
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
||||
outfile.write('\n'.join(
|
||||
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in
|
||||
grouping.items()]))
|
||||
|
||||
if self.shops:
|
||||
outfile.write('\n\nShops:\n\n')
|
||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
|
||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(
|
||||
item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if
|
||||
item)) for shop in self.shops))
|
||||
|
||||
for player in self.world.get_game_players("A Link to the Past"):
|
||||
if self.world.boss_shuffle[player] != 'none':
|
||||
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
||||
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
|
||||
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
||||
outfile.write(
|
||||
f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
|
||||
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
||||
outfile.write('\n\nPlaythrough:\n\n')
|
||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
||||
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
|
||||
f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||
if self.unreachables:
|
||||
outfile.write('\n\nUnreachable Items:\n\n')
|
||||
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||
outfile.write(
|
||||
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||
|
||||
if self.paths:
|
||||
outfile.write('\n\nPaths:\n\n')
|
||||
@@ -1367,3 +1305,14 @@ class Spoiler():
|
||||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
||||
|
||||
outfile.write('\n'.join(path_listings))
|
||||
call_all(self.world, "write_spoiler_end", outfile)
|
||||
|
||||
|
||||
seeddigits = 20
|
||||
|
||||
|
||||
def get_seed(seed=None):
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
return random.randint(0, pow(10, seeddigits) - 1)
|
||||
return seed
|
||||
|
||||
213
CommonClient.py
213
CommonClient.py
@@ -1,19 +1,28 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import typing
|
||||
import asyncio
|
||||
import urllib.parse
|
||||
import sys
|
||||
import typing
|
||||
import time
|
||||
|
||||
import websockets
|
||||
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
|
||||
from Utils import Version
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
# without terminal we have to use gui mode
|
||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||
|
||||
|
||||
class ClientCommandProcessor(CommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
@@ -48,6 +57,9 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_missing(self) -> bool:
|
||||
"""List all missing location checks, from your local game state"""
|
||||
if not self.ctx.game:
|
||||
self.output("No game set, cannot determine missing checks.")
|
||||
return False
|
||||
count = 0
|
||||
checked_count = 0
|
||||
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
|
||||
@@ -84,11 +96,13 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
|
||||
class CommonContext():
|
||||
starting_reconnect_delay = 5
|
||||
current_reconnect_delay = starting_reconnect_delay
|
||||
command_processor = ClientCommandProcessor
|
||||
game: None
|
||||
ui: None
|
||||
tags: typing.Set[str] = {"AP"}
|
||||
starting_reconnect_delay: int = 5
|
||||
current_reconnect_delay: int = starting_reconnect_delay
|
||||
command_processor: int = ClientCommandProcessor
|
||||
game = None
|
||||
ui = None
|
||||
keep_alive_task = None
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
# server state
|
||||
@@ -97,6 +111,13 @@ class CommonContext():
|
||||
self.server_task = None
|
||||
self.server: typing.Optional[Endpoint] = None
|
||||
self.server_version = Version(0, 0, 0)
|
||||
self.hint_cost: typing.Optional[int] = None
|
||||
self.games: typing.Dict[int, str] = {}
|
||||
self.permissions = {
|
||||
"forfeit": "disabled",
|
||||
"collect": "disabled",
|
||||
"remaining": "disabled",
|
||||
}
|
||||
|
||||
# own state
|
||||
self.finished_game = False
|
||||
@@ -106,16 +127,18 @@ class CommonContext():
|
||||
self.auth = None
|
||||
self.seed_name = None
|
||||
|
||||
self.locations_checked: typing.Set[int] = set()
|
||||
self.locations_checked: typing.Set[int] = set() # local state
|
||||
self.locations_scouted: typing.Set[int] = set()
|
||||
self.items_received = []
|
||||
self.missing_locations: typing.List[int] = []
|
||||
self.checked_locations: typing.List[int] = []
|
||||
self.missing_locations: typing.Set[int] = set()
|
||||
self.checked_locations: typing.Set[int] = set() # server state
|
||||
self.locations_info = {}
|
||||
|
||||
self.input_queue = asyncio.Queue()
|
||||
self.input_requests = 0
|
||||
|
||||
self.last_death_link: float = time.time() # last send/received death link on AP layer
|
||||
|
||||
# game state
|
||||
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
|
||||
self.exit_event = asyncio.Event()
|
||||
@@ -125,6 +148,15 @@ class CommonContext():
|
||||
self.jsontotextparser = JSONtoTextParser(self)
|
||||
self.set_getters(network_data_package)
|
||||
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self))
|
||||
|
||||
@property
|
||||
def total_locations(self) -> typing.Optional[int]:
|
||||
"""Will return None until connected."""
|
||||
if self.checked_locations or self.missing_locations:
|
||||
return len(self.checked_locations | self.missing_locations)
|
||||
|
||||
async def connection_closed(self):
|
||||
self.auth = None
|
||||
self.items_received = []
|
||||
@@ -135,6 +167,7 @@ class CommonContext():
|
||||
self.server = None
|
||||
self.server_task = None
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
def set_getters(self, data_package: dict, network=False):
|
||||
if not network: # local data; check if newer data was already downloaded
|
||||
local_package = Utils.persistent_load().get("datapackage", {}).get("latest", {})
|
||||
@@ -188,7 +221,10 @@ class CommonContext():
|
||||
def event_invalid_slot(self):
|
||||
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
def event_invalid_game(self):
|
||||
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
logger.info('Enter the password required to join this game:')
|
||||
self.password = await self.console_input()
|
||||
@@ -216,6 +252,49 @@ class CommonContext():
|
||||
"""For custom package handling in subclasses."""
|
||||
pass
|
||||
|
||||
def update_permissions(self, permissions: typing.Dict[str, int]):
|
||||
for permission_name, permission_flag in permissions.items():
|
||||
try:
|
||||
flag = Permission(permission_flag)
|
||||
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
|
||||
self.permissions[permission_name] = flag.name
|
||||
except Exception as e: # safeguard against permissions that may be implemented in the future
|
||||
logger.exception(e)
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
|
||||
self.last_death_link = max(data["time"], self.last_death_link)
|
||||
text = data.get("cause", "")
|
||||
if text:
|
||||
logger.info(f"DeathLink: {text}")
|
||||
else:
|
||||
logger.info(f"DeathLink: Received from {data['source']}")
|
||||
|
||||
async def send_death(self, death_text: str = ""):
|
||||
logger.info("Sending death to your friends...")
|
||||
self.last_death_link = time.time()
|
||||
await self.send_msgs([{
|
||||
"cmd": "Bounce", "tags": ["DeathLink"],
|
||||
"data": {
|
||||
"time": self.last_death_link,
|
||||
"source": self.player_names[self.slot],
|
||||
"cause": death_text
|
||||
}
|
||||
}])
|
||||
|
||||
|
||||
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
|
||||
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
|
||||
so we send a payload to prevent drop and if we were dropped anyway this will cause an auto-reconnect."""
|
||||
seconds_elapsed = 0
|
||||
while not ctx.exit_event.is_set():
|
||||
await asyncio.sleep(1) # short sleep to not block program shutdown
|
||||
if ctx.server and ctx.slot:
|
||||
seconds_elapsed += 1
|
||||
if seconds_elapsed > seconds_between_checks:
|
||||
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot]}])
|
||||
seconds_elapsed = 0
|
||||
|
||||
|
||||
async def server_loop(ctx: CommonContext, address=None):
|
||||
cached_address = None
|
||||
@@ -250,13 +329,13 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||
logger.error('Unable to connect to multiworld server at cached address. '
|
||||
'Please use the connect button above.')
|
||||
else:
|
||||
logger.error('Connection refused by the multiworld server')
|
||||
logger.exception('Connection refused by the multiworld server')
|
||||
except websockets.InvalidURI:
|
||||
logger.exception('Failed to connect to the multiworld server (invalid URI)')
|
||||
except (OSError, websockets.InvalidURI):
|
||||
logger.error('Failed to connect to the multiworld server')
|
||||
logger.exception('Failed to connect to the multiworld server')
|
||||
except Exception as e:
|
||||
logger.error('Lost connection to the multiworld server, type /connect to reconnect')
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logger.exception(e)
|
||||
logger.exception('Lost connection to the multiworld server, type /connect to reconnect')
|
||||
finally:
|
||||
await ctx.connection_closed()
|
||||
if ctx.server_address:
|
||||
@@ -292,16 +371,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
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']}")
|
||||
ctx.update_permissions(args.get("permissions", {}))
|
||||
if "games" in args:
|
||||
ctx.games = {x: game for x, game in enumerate(args["games"], start=1)}
|
||||
logger.info(
|
||||
f"A !hint costs {args['hint_cost']}% of your total location count as points"
|
||||
f" 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:
|
||||
@@ -325,7 +404,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
errors = args["errors"]
|
||||
if 'InvalidSlot' in errors:
|
||||
ctx.event_invalid_slot()
|
||||
|
||||
elif 'InvalidGame' in errors:
|
||||
ctx.event_invalid_game()
|
||||
elif 'SlotAlreadyTaken' in errors:
|
||||
raise Exception('Player slot already in use for that team')
|
||||
elif 'IncompatibleVersion' in errors:
|
||||
@@ -360,8 +440,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
# This list is used to only send to the server what is reported as ACTUALLY Missing.
|
||||
# This also serves to allow an easy visual of what locations were already checked previously
|
||||
# when /missing is used for the client side view of what is missing.
|
||||
ctx.missing_locations = args["missing_locations"]
|
||||
ctx.checked_locations = args["checked_locations"]
|
||||
ctx.missing_locations = set(args["missing_locations"])
|
||||
ctx.checked_locations = set(args["checked_locations"])
|
||||
|
||||
elif cmd == 'ReceivedItems':
|
||||
start_index = args["index"]
|
||||
@@ -390,6 +470,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
ctx.consume_players_package(args["players"])
|
||||
if "hint_points" in args:
|
||||
ctx.hint_points = args['hint_points']
|
||||
if "checked_locations" in args:
|
||||
checked = set(args["checked_locations"])
|
||||
ctx.checked_locations |= checked
|
||||
ctx.missing_locations -= checked
|
||||
if "permissions" in args:
|
||||
ctx.update_permissions(args["permissions"])
|
||||
|
||||
elif cmd == 'Print':
|
||||
ctx.on_print(args)
|
||||
@@ -401,7 +487,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
|
||||
|
||||
elif cmd == "Bounced":
|
||||
pass
|
||||
tags = args.get("tags", [])
|
||||
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
|
||||
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
|
||||
ctx.on_deathlink(args["data"])
|
||||
|
||||
else:
|
||||
logger.debug(f"unknown command {cmd}")
|
||||
@@ -428,3 +517,79 @@ async def console_loop(ctx: CommonContext):
|
||||
commandprocessor(input_text)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
def get_base_parser(description=None):
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||
if sys.stdout: # If terminal output exists, offer gui-less mode
|
||||
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
|
||||
class TextContext(CommonContext):
|
||||
tags = {"AP", "IgnoreGame"}
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(TextContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
'tags': self.tags,
|
||||
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
||||
}])
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
self.game = self.games.get(self.slot, None)
|
||||
|
||||
|
||||
async def main(args):
|
||||
ctx = TextContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
input_task = None
|
||||
from kvui import TextManager
|
||||
ctx.ui = TextManager(ctx)
|
||||
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
|
||||
else:
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ui_task = None
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
if ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
if ctx.server_task:
|
||||
await ctx.server_task
|
||||
|
||||
while ctx.input_requests > 0:
|
||||
ctx.input_queue.put_nowait(None)
|
||||
ctx.input_requests -= 1
|
||||
|
||||
if ui_task:
|
||||
await ui_task
|
||||
|
||||
if input_task:
|
||||
input_task.cancel()
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfaction.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main(args))
|
||||
loop.close()
|
||||
colorama.deinit()
|
||||
|
||||
@@ -4,34 +4,26 @@ import logging
|
||||
import json
|
||||
import string
|
||||
import copy
|
||||
import sys
|
||||
import subprocess
|
||||
import factorio_rcon
|
||||
import time
|
||||
import random
|
||||
|
||||
import factorio_rcon
|
||||
import colorama
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
||||
from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
import random
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("FactorioClient")
|
||||
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
||||
get_base_parser
|
||||
from MultiServer import mark_raw
|
||||
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
||||
|
||||
from worlds.factorio import Factorio
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
|
||||
|
||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
ctx: FactorioContext
|
||||
@@ -62,12 +54,14 @@ class FactorioContext(CommonContext):
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(FactorioContext, self).__init__(server_address, password)
|
||||
self.send_index = 0
|
||||
self.send_index: int = 0
|
||||
self.rcon_client = None
|
||||
self.awaiting_bridge = False
|
||||
self.write_data_path = None
|
||||
self.death_link_tick: int = 0 # last send death link on Factorio layer
|
||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
|
||||
@@ -78,14 +72,18 @@ class FactorioContext(CommonContext):
|
||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
||||
"bridge to Factorio first.")
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
'tags': ['AP'],
|
||||
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
||||
}])
|
||||
await self.send_msgs([{
|
||||
"cmd": 'Connect',
|
||||
'password': self.password,
|
||||
'name': self.auth,
|
||||
'version': Utils.version_tuple,
|
||||
'tags': self.tags,
|
||||
'uuid': Utils.get_unique_identifier(),
|
||||
'game': "Factorio"
|
||||
}])
|
||||
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
super(FactorioContext, self).on_print(args)
|
||||
if self.rcon_client:
|
||||
self.print_to_game(args['text'])
|
||||
|
||||
@@ -97,31 +95,33 @@ class FactorioContext(CommonContext):
|
||||
|
||||
@property
|
||||
def savegame_name(self) -> str:
|
||||
return f"AP_{self.seed_name}_{self.auth}.zip"
|
||||
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
|
||||
|
||||
def print_to_game(self, text):
|
||||
# TODO: remove around version 0.2
|
||||
if self.mod_version < Utils.Version(0, 1, 6):
|
||||
text = text.replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}\")")
|
||||
else:
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}")
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}")
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
if self.rcon_client:
|
||||
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
|
||||
super(FactorioContext, self).on_deathlink(data)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
if cmd in {"Connected", "RoomUpdate"}:
|
||||
# catch up sync anything that is already cleared.
|
||||
for tech in args["checked_locations"]:
|
||||
item_name = f"ap-{tech}-"
|
||||
self.rcon_client.send_command(f'/ap-get-technology {item_name}\t-1')
|
||||
if "checked_locations" in args and args["checked_locations"]:
|
||||
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
|
||||
item_name in args["checked_locations"]})
|
||||
|
||||
|
||||
async def game_watcher(ctx: FactorioContext):
|
||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
next_bridge = time.perf_counter() + 1
|
||||
try:
|
||||
while not ctx.exit_event.is_set():
|
||||
if ctx.awaiting_bridge and ctx.rcon_client:
|
||||
if ctx.awaiting_bridge and ctx.rcon_client and time.perf_counter() > next_bridge:
|
||||
next_bridge = time.perf_counter() + 1
|
||||
ctx.awaiting_bridge = False
|
||||
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
|
||||
if data["slot_name"] != ctx.auth:
|
||||
@@ -145,7 +145,12 @@ async def game_watcher(ctx: FactorioContext):
|
||||
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)
|
||||
death_link_tick = data.get("death_link_tick", 0)
|
||||
if death_link_tick != ctx.death_link_tick:
|
||||
ctx.death_link_tick = death_link_tick
|
||||
await ctx.send_death()
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
@@ -194,15 +199,15 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
factorio_server_logger.info(msg)
|
||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
# TODO: remove around version 0.2
|
||||
if ctx.mod_version < Utils.Version(0, 1, 6):
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
if not ctx.server:
|
||||
logger.info("Established bridge to Factorio Server. "
|
||||
"Ready to connect to Archipelago via /connect")
|
||||
|
||||
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||
ctx.awaiting_bridge = True
|
||||
|
||||
if ctx.rcon_client:
|
||||
commands = {}
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
item_id = transfer_item.item
|
||||
@@ -212,8 +217,10 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
else:
|
||||
item_name = Factorio.item_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}\t{ctx.send_index}\t{player_name}')
|
||||
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
|
||||
ctx.send_index += 1
|
||||
if commands:
|
||||
ctx.rcon_client.send_commands(commands)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
@@ -231,9 +238,13 @@ def get_info(ctx, rcon_client):
|
||||
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
|
||||
ctx.auth = info["slot_name"]
|
||||
ctx.seed_name = info["seed_name"]
|
||||
# 0.2.0 addition, not present earlier
|
||||
death_link = bool(info.get("death_link", False))
|
||||
if death_link:
|
||||
ctx.tags.add("DeathLink")
|
||||
|
||||
|
||||
async def factorio_spinup_server(ctx: FactorioContext):
|
||||
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
savegame_name = os.path.abspath("Archipelago.zip")
|
||||
if not os.path.exists(savegame_name):
|
||||
logger.info(f"Creating savegame {savegame_name}")
|
||||
@@ -259,25 +270,32 @@ async def factorio_spinup_server(ctx: FactorioContext):
|
||||
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
|
||||
parts = msg.split()
|
||||
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
||||
elif "Write data path: " in msg:
|
||||
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
|
||||
if "AppData" in ctx.write_data_path:
|
||||
logger.warning("It appears your mods are loaded from Appdata, "
|
||||
"this can lead to problems with multiple Factorio instances. "
|
||||
"If this is the case, you will get a file locked error running Factorio.")
|
||||
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
get_info(ctx, rcon_client)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
logger.exception(e)
|
||||
logger.error("Aborted Factorio Server Bridge")
|
||||
ctx.exit_event.set()
|
||||
|
||||
else:
|
||||
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
|
||||
logger.info(
|
||||
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
return True
|
||||
finally:
|
||||
factorio_process.terminate()
|
||||
factorio_process.wait(5)
|
||||
return False
|
||||
|
||||
|
||||
async def main(args):
|
||||
@@ -292,16 +310,17 @@ async def main(args):
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ui_task = None
|
||||
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
|
||||
await factorio_server_task
|
||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
succesful_launch = await factorio_server_task
|
||||
if succesful_launch:
|
||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await progression_watcher
|
||||
await factorio_server_task
|
||||
await progression_watcher
|
||||
await factorio_server_task
|
||||
|
||||
if ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
@@ -334,33 +353,28 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
|
||||
"Remaining arguments get passed into bound Factorio instance."
|
||||
"Refer to Factorio --help for those.")
|
||||
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
|
||||
"Remaining arguments get passed into bound Factorio instance."
|
||||
"Refer to Factorio --help for those.")
|
||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
|
||||
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
rcon_port = args.rcon_port
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(random.choice(string.ascii_letters) for x in range(32))
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||
random.choice(string.ascii_letters) for x in range(32))
|
||||
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
options = Utils.get_options()
|
||||
executable = options["factorio_options"]["executable"]
|
||||
bin_dir = os.path.dirname(executable)
|
||||
if not os.path.exists(bin_dir):
|
||||
raise FileNotFoundError(f"Path {bin_dir} does not exist or could not be accessed.")
|
||||
if not os.path.isdir(bin_dir):
|
||||
raise NotADirectoryError(f"Path {bin_dir} is not a directory.")
|
||||
if not os.path.exists(executable):
|
||||
if os.path.exists(executable + ".exe"):
|
||||
|
||||
if not os.path.exists(os.path.dirname(executable)):
|
||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
||||
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
|
||||
executable = os.path.join(executable, "factorio")
|
||||
if not os.path.isfile(executable):
|
||||
if os.path.isfile(executable + ".exe"):
|
||||
executable = executable + ".exe"
|
||||
else:
|
||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||
|
||||
30
Fill.py
30
Fill.py
@@ -4,8 +4,6 @@ import collections
|
||||
import itertools
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import key_drop_data
|
||||
from worlds.generic import PlandoItem
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
@@ -38,7 +36,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
if world.accessibility[item_to_place.player] == 'none':
|
||||
if world.accessibility[item_to_place.player] == 'minimal':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) if single_player_placement else not has_beaten_game
|
||||
else:
|
||||
@@ -54,11 +52,10 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
else:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.append(item_to_place)
|
||||
if world.accessibility[item_to_place.player] != 'none' and world.can_beat_game():
|
||||
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
|
||||
continue
|
||||
|
||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
@@ -81,6 +78,7 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
progitempool = []
|
||||
nonexcludeditempool = []
|
||||
localrestitempool = {player: [] for player in range(1, world.players + 1)}
|
||||
nonlocalrestitempool = []
|
||||
restitempool = []
|
||||
|
||||
for item in world.itempool:
|
||||
@@ -88,13 +86,15 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
progitempool.append(item)
|
||||
elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
|
||||
nonexcludeditempool.append(item)
|
||||
elif item.name in world.local_items[item.player]:
|
||||
elif item.name in world.local_items[item.player].value:
|
||||
localrestitempool[item.player].append(item)
|
||||
elif item.name in world.non_local_items[item.player].value:
|
||||
nonlocalrestitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
@@ -120,14 +120,22 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
|
||||
for item_to_place in nonlocalrestitempool:
|
||||
for i, location in enumerate(fill_locations):
|
||||
if location.player != item_to_place.player:
|
||||
world.push_item(fill_locations.pop(i), item_to_place, False)
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.")
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
|
||||
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
|
||||
unplaced = [item for item in progitempool + restitempool]
|
||||
unplaced = progitempool + restitempool
|
||||
unfilled = [location.name for location in fill_locations]
|
||||
|
||||
if unplaced or unfilled:
|
||||
raise FillError(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
|
||||
@@ -328,6 +336,8 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
|
||||
|
||||
|
||||
def distribute_planned(world: MultiWorld):
|
||||
# TODO: remove. Preferably by implementing key drop
|
||||
from worlds.alttp.Regions import key_drop_data
|
||||
world_name_lookup = world.world_name_lookup
|
||||
|
||||
for player in world.player_ids:
|
||||
@@ -338,7 +348,7 @@ def distribute_planned(world: MultiWorld):
|
||||
placement.warn(
|
||||
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||
continue
|
||||
item = ItemFactory(placement.item, player)
|
||||
item = world.worlds[player].create_item(placement.item)
|
||||
target_world: int = placement.world
|
||||
if target_world is False or world.players == 1:
|
||||
target_world = player # in own world
|
||||
|
||||
356
Generate.py
356
Generate.py
@@ -12,17 +12,16 @@ import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.generic import PlandoItem, PlandoConnection
|
||||
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from Main import get_seed, seeddigits
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
import Options
|
||||
from worlds.alttp.Items import item_name_groups, item_table
|
||||
from worlds.alttp import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.alttp.Regions import location_table, key_drop_data
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
categories = set(AutoWorldRegister.world_types)
|
||||
@@ -39,16 +38,16 @@ def mystery_argparse():
|
||||
parser.add_argument('--player_files_path', default=defaults["player_files_path"],
|
||||
help="Input directory for player files.")
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
|
||||
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
|
||||
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
|
||||
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], help="Path to the 1.0 JP SM Baserom.")
|
||||
parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
|
||||
parser.add_argument('--outputpath', default=options["general_options"]["output_path"])
|
||||
parser.add_argument('--race', action='store_true', default=defaults["race"])
|
||||
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
|
||||
parser.add_argument('--log_output_path', help='Path to store output log')
|
||||
parser.add_argument('--log_level', default='info', help='Sets log level')
|
||||
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
|
||||
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
|
||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||
parser.add_argument('--plando', default=defaults["plando_options"],
|
||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||
@@ -113,35 +112,24 @@ def main(args=None, callback=ERmain):
|
||||
player_id += 1
|
||||
|
||||
args.multi = max(player_id-1, args.multi)
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
||||
f"{', '.join(args.plando)}")
|
||||
|
||||
if not weights_cache:
|
||||
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||
f"A mix is also permitted.")
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.create_spoiler = args.spoiler > 0
|
||||
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
|
||||
erargs.spoiler = args.spoiler
|
||||
erargs.race = args.race
|
||||
erargs.skip_playthrough = args.spoiler < 2
|
||||
erargs.outputname = seed_name
|
||||
erargs.outputpath = args.outputpath
|
||||
|
||||
# set up logger
|
||||
if args.log_level:
|
||||
erargs.loglevel = args.log_level
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
||||
erargs.loglevel]
|
||||
Utils.init_logging(f"Generate_{seed}.txt", loglevel=args.log_level)
|
||||
|
||||
if args.log_output_path:
|
||||
os.makedirs(args.log_output_path, exist_ok=True)
|
||||
logging.basicConfig(format='%(message)s', level=loglevel, force=True,
|
||||
filename=os.path.join(args.log_output_path, f"{seed}.log"))
|
||||
else:
|
||||
logging.basicConfig(format='%(message)s', level=loglevel, force=True)
|
||||
|
||||
erargs.rom = args.rom
|
||||
erargs.lttp_rom = args.lttp_rom
|
||||
erargs.sm_rom = args.sm_rom
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
|
||||
@@ -189,6 +177,9 @@ def main(args=None, callback=ERmain):
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
if len(set(erargs.name.values())) != len(erargs.name):
|
||||
raise Exception(f"Names have to be unique. Names: {erargs.name}")
|
||||
|
||||
if args.yaml_output:
|
||||
import yaml
|
||||
important = {}
|
||||
@@ -237,7 +228,7 @@ def convert_to_on_off(value):
|
||||
return {True: "on", False: "off"}.get(value, value)
|
||||
|
||||
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
def get_choice_legacy(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
@@ -252,6 +243,20 @@ def get_choice(option, root, value=None) -> typing.Any:
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
return random.choices(root[option])[0]
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return value
|
||||
if any(root[option].values()):
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
@@ -345,10 +350,10 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def roll_triggers(weights: dict) -> dict:
|
||||
def roll_triggers(weights: dict, triggers: list) -> 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 i, option_set in enumerate(weights["triggers"]):
|
||||
for i, option_set in enumerate(triggers):
|
||||
try:
|
||||
currently_targeted_weights = weights
|
||||
category = option_set.get("option_category", None)
|
||||
@@ -410,12 +415,38 @@ 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 handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
|
||||
if option_key in game_weights:
|
||||
try:
|
||||
if not option.supports_weighting:
|
||||
player_option = option.from_any(game_weights[option_key])
|
||||
else:
|
||||
player_option = option.from_any(get_choice(option_key, game_weights))
|
||||
setattr(ret, option_key, player_option)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
|
||||
else:
|
||||
# verify item names existing
|
||||
if getattr(player_option, "verify_item_name", False):
|
||||
for item_name in player_option.value:
|
||||
if item_name not in AutoWorldRegister.world_types[ret.game].item_names:
|
||||
raise Exception(f"Item {item_name} from option {player_option} "
|
||||
f"is not a valid item name from {ret.game}")
|
||||
elif getattr(player_option, "verify_location_name", False):
|
||||
for location_name in player_option.value:
|
||||
if location_name not in AutoWorldRegister.world_types[ret.game].location_names:
|
||||
raise Exception(f"Location {location_name} from option {player_option} "
|
||||
f"is not a valid location name from {ret.game}")
|
||||
else:
|
||||
setattr(ret, option_key, option(option.default))
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
if "triggers" in weights:
|
||||
weights = roll_triggers(weights)
|
||||
weights = roll_triggers(weights, weights["triggers"])
|
||||
|
||||
requirements = weights.get("requires", {})
|
||||
if requirements:
|
||||
@@ -436,63 +467,32 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
f"which are not enabled.")
|
||||
|
||||
ret = argparse.Namespace()
|
||||
ret.name = get_choice('name', weights)
|
||||
ret.accessibility = get_choice('accessibility', weights)
|
||||
ret.progression_balancing = get_choice('progression_balancing', weights, True)
|
||||
for option_key in Options.per_game_common_options:
|
||||
if option_key in weights:
|
||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in weights:
|
||||
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
||||
|
||||
world_type = AutoWorldRegister.world_types[ret.game]
|
||||
game_weights = weights[ret.game]
|
||||
ret.local_items = set()
|
||||
for item_name in game_weights.get('local_items', []):
|
||||
items = world_type.item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in world_type.item_names:
|
||||
ret.local_items.add(item)
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
|
||||
|
||||
ret.non_local_items = set()
|
||||
for item_name in game_weights.get('non_local_items', []):
|
||||
items = world_type.item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in world_type.item_names:
|
||||
ret.non_local_items.add(item)
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
||||
if "triggers" in game_weights:
|
||||
weights = roll_triggers(weights, game_weights["triggers"])
|
||||
game_weights = weights[ret.game]
|
||||
|
||||
inventoryweights = game_weights.get('start_inventory', {})
|
||||
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(game_weights.get('start_hints', []))
|
||||
|
||||
ret.excluded_locations = set()
|
||||
for location in game_weights.get('exclude_locations', []):
|
||||
if location in world_type.location_names:
|
||||
ret.excluded_locations.add(location)
|
||||
else:
|
||||
raise Exception(f"Could not exclude location {location}, as it was not recognized.")
|
||||
ret.name = get_choice('name', weights)
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
||||
|
||||
if ret.game in AutoWorldRegister.world_types:
|
||||
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
|
||||
if option_name in game_weights:
|
||||
try:
|
||||
if issubclass(option, Options.OptionDict):
|
||||
setattr(ret, option_name, option.from_any(game_weights[option_name]))
|
||||
else:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_name} in {ret.game}") from e
|
||||
else:
|
||||
setattr(ret, option_name, option(option.default))
|
||||
for option_key, option in world_type.options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
if "items" in plando_options:
|
||||
ret.plando_items = roll_item_plando(world_type, game_weights)
|
||||
if ret.game == "Minecraft":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
@@ -512,8 +512,48 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
return ret
|
||||
|
||||
|
||||
def roll_item_plando(world_type, weights):
|
||||
plando_items = []
|
||||
|
||||
def add_plando_item(item: str, location: str):
|
||||
if item not in world_type.item_name_to_id:
|
||||
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
||||
if location not in world_type.location_name_to_id:
|
||||
raise Exception(
|
||||
f"Could not plando item {item} at location {location} as the location was not recognized")
|
||||
plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
|
||||
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
from_pool = get_choice_legacy("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||
location_world = get_choice_legacy("world", placement, PlandoItem._field_defaults["world"])
|
||||
force = str(get_choice_legacy("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||
if "items" in placement and "locations" in placement:
|
||||
items = placement["items"]
|
||||
locations = placement["locations"]
|
||||
if isinstance(items, dict):
|
||||
item_list = []
|
||||
for key, value in items.items():
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if not items or not locations:
|
||||
raise Exception("You must specify at least one item and one location to place items.")
|
||||
random.shuffle(items)
|
||||
random.shuffle(locations)
|
||||
for item, location in zip(items, locations):
|
||||
add_plando_item(item, location)
|
||||
else:
|
||||
item = get_choice_legacy("item", placement, get_choice_legacy("items", placement))
|
||||
location = get_choice_legacy("location", placement)
|
||||
add_plando_item(item, location)
|
||||
return plando_items
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
|
||||
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
|
||||
glitches_required = get_choice_legacy('glitches_required', weights)
|
||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
||||
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
@@ -521,7 +561,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
|
||||
glitches_required]
|
||||
|
||||
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
|
||||
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
|
||||
if not ret.dark_room_logic: # None/False
|
||||
ret.dark_room_logic = "none"
|
||||
if ret.dark_room_logic == "sconces":
|
||||
@@ -529,94 +569,61 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
|
||||
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
|
||||
|
||||
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False)
|
||||
|
||||
dungeon_items = get_choice('dungeon_items', weights)
|
||||
if dungeon_items == 'full' or dungeon_items == True:
|
||||
dungeon_items = 'mcsb'
|
||||
elif dungeon_items == 'standard':
|
||||
dungeon_items = ""
|
||||
elif not dungeon_items:
|
||||
dungeon_items = ""
|
||||
if "u" in dungeon_items:
|
||||
dungeon_items.replace("s", "")
|
||||
|
||||
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
|
||||
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
|
||||
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla')
|
||||
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
|
||||
if entrance_shuffle.startswith('none-'):
|
||||
ret.shuffle = 'vanilla'
|
||||
else:
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goal = get_choice('goals', weights, 'ganon')
|
||||
goal = get_choice_legacy('goals', weights, 'ganon')
|
||||
|
||||
ret.goal = goals[goal]
|
||||
|
||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||
# fast ganon + ganon at hole
|
||||
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
|
||||
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
|
||||
|
||||
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
|
||||
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
|
||||
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
|
||||
|
||||
# sum a percentage to required
|
||||
if extra_pieces == 'percentage':
|
||||
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100
|
||||
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
|
||||
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||
# vanilla mode (specify how many pieces are)
|
||||
elif extra_pieces == 'available':
|
||||
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
|
||||
get_choice('triforce_pieces_available', weights, 30))
|
||||
get_choice_legacy('triforce_pieces_available', weights, 30))
|
||||
# required pieces + fixed extra
|
||||
elif extra_pieces == 'extra':
|
||||
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
|
||||
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
|
||||
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
|
||||
|
||||
# change minimum to required pieces to avoid problems
|
||||
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
|
||||
|
||||
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
|
||||
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
|
||||
if not ret.shop_shuffle:
|
||||
ret.shop_shuffle = ''
|
||||
|
||||
ret.mode = get_choice("mode", weights)
|
||||
ret.retro = get_choice("retro", weights)
|
||||
ret.mode = get_choice_legacy("mode", weights)
|
||||
|
||||
ret.hints = get_choice('hints', weights)
|
||||
ret.difficulty = get_choice_legacy('item_pool', weights)
|
||||
|
||||
ret.swordless = get_choice('swordless', weights, False)
|
||||
ret.item_functionality = get_choice_legacy('item_functionality', weights)
|
||||
|
||||
ret.difficulty = get_choice('item_pool', weights)
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality', weights)
|
||||
|
||||
boss_shuffle = get_choice('boss_shuffle', weights)
|
||||
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, 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)
|
||||
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos', # to be removed
|
||||
'chaos': 'chaos',
|
||||
}[get_choice('enemy_damage', weights)]
|
||||
}[get_choice_legacy('enemy_damage', weights)]
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health', weights)
|
||||
|
||||
ret.shufflepots = get_choice('pot_shuffle', weights)
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer', weights, 0))
|
||||
ret.enemy_health = get_choice_legacy('enemy_health', weights)
|
||||
|
||||
ret.timer = {'none': False,
|
||||
None: False,
|
||||
@@ -625,19 +632,19 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
'timed_ohko': 'timed-ohko',
|
||||
'ohko': 'ohko',
|
||||
'timed_countdown': 'timed-countdown',
|
||||
'display': 'display'}[get_choice('timer', weights, False)]
|
||||
'display': 'display'}[get_choice_legacy('timer', weights, False)]
|
||||
|
||||
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
|
||||
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
||||
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
|
||||
|
||||
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
|
||||
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
|
||||
|
||||
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
|
||||
get_choice("turtle_rock_medallion", weights, "random")]
|
||||
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
|
||||
get_choice_legacy("turtle_rock_medallion", weights, "random")]
|
||||
|
||||
for index, medallion in enumerate(ret.required_medallions):
|
||||
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
|
||||
@@ -645,88 +652,45 @@ 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'}")
|
||||
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights, True)
|
||||
|
||||
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()
|
||||
ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set()
|
||||
|
||||
ret.plando_items = []
|
||||
if "items" in plando_options:
|
||||
|
||||
def add_plando_item(item: str, location: str):
|
||||
if item not in item_table:
|
||||
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
||||
if location not in location_table and location not in key_drop_data:
|
||||
raise Exception(
|
||||
f"Could not plando item {item} at location {location} as the location was not recognized")
|
||||
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
|
||||
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
|
||||
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||
if "items" in placement and "locations" in placement:
|
||||
items = placement["items"]
|
||||
locations = placement["locations"]
|
||||
if isinstance(items, dict):
|
||||
item_list = []
|
||||
for key, value in items.items():
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if not items or not locations:
|
||||
raise Exception("You must specify at least one item and one location to place items.")
|
||||
random.shuffle(items)
|
||||
random.shuffle(locations)
|
||||
for item, location in zip(items, locations):
|
||||
add_plando_item(item, location)
|
||||
else:
|
||||
item = get_choice("item", placement, get_choice("items", placement))
|
||||
location = get_choice("location", placement)
|
||||
add_plando_item(item, location)
|
||||
|
||||
ret.plando_texts = {}
|
||||
if "texts" in plando_options:
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
at = str(get_choice("at", placement))
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
at = str(get_choice_legacy("at", placement))
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
ret.plando_texts[at] = str(get_choice("text", placement))
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
ret.plando_connections = []
|
||||
if "connections" in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement, "both")
|
||||
get_choice_legacy("entrance", placement),
|
||||
get_choice_legacy("exit", placement),
|
||||
get_choice_legacy("direction", placement, "both")
|
||||
))
|
||||
|
||||
ret.sprite_pool = weights.get('sprite_pool', [])
|
||||
ret.sprite = get_choice('sprite', weights, "Link")
|
||||
ret.sprite = get_choice_legacy('sprite', weights, "Link")
|
||||
if 'random_sprite_on_event' in weights:
|
||||
randomoneventweights = weights['random_sprite_on_event']
|
||||
if get_choice('enabled', randomoneventweights, False):
|
||||
if get_choice_legacy('enabled', randomoneventweights, False):
|
||||
ret.sprite = 'randomon'
|
||||
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
|
||||
|
||||
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
|
||||
for key, value in weights['sprite'].items():
|
||||
if key.startswith('random'):
|
||||
@@ -740,4 +704,4 @@ if __name__ == '__main__':
|
||||
confirmation = atexit.register(input, "Press enter to close.")
|
||||
main()
|
||||
# in case of error-free exit should not need confirmation
|
||||
atexit.unregister(confirmation)
|
||||
atexit.unregister(confirmation)
|
||||
|
||||
@@ -15,7 +15,7 @@ from argparse import Namespace
|
||||
from concurrent.futures import as_completed, ThreadPoolExecutor
|
||||
from glob import glob
|
||||
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, LEFT, X, TOP, LabelFrame, \
|
||||
IntVar, Checkbutton, E, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
|
||||
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
@@ -51,6 +51,7 @@ def main():
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--deathlink', help='Enable DeathLink system.', 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'],
|
||||
@@ -152,7 +153,8 @@ def adjust(args):
|
||||
world = getattr(args, "world")
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
|
||||
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
|
||||
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
|
||||
deathlink=args.deathlink)
|
||||
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
|
||||
rom.write_to_file(path)
|
||||
|
||||
@@ -205,6 +207,7 @@ def adjustGUI():
|
||||
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
|
||||
guiargs.music = bool(rom_vars.MusicVar.get())
|
||||
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
|
||||
guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
|
||||
guiargs.rom = romVar2.get()
|
||||
guiargs.baserom = romVar.get()
|
||||
guiargs.sprite = rom_vars.sprite
|
||||
@@ -272,17 +275,18 @@ def update_sprites(task, on_finish=None):
|
||||
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 if sprite["author"] != "Nintendo"]
|
||||
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
|
||||
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]
|
||||
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
|
||||
except Exception as e:
|
||||
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
||||
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
task.queue_event(finished)
|
||||
return
|
||||
|
||||
|
||||
def dl(sprite_url, filename):
|
||||
target = os.path.join(sprite_dir, filename)
|
||||
with urlopen(sprite_url) as response, open(target, 'wb') as out:
|
||||
@@ -291,7 +295,6 @@ def update_sprites(task, on_finish=None):
|
||||
def rem(sprite):
|
||||
os.remove(os.path.join(sprite_dir, sprite))
|
||||
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
dl_tasks = []
|
||||
rem_tasks = []
|
||||
@@ -313,7 +316,7 @@ def update_sprites(task, on_finish=None):
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
|
||||
for rem_task in as_completed(rem_tasks):
|
||||
@@ -324,7 +327,7 @@ def update_sprites(task, on_finish=None):
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
|
||||
if successful:
|
||||
@@ -362,7 +365,7 @@ class BackgroundTask(object):
|
||||
event = self.queue.get_nowait()
|
||||
event()
|
||||
if self.running:
|
||||
#if self is no longer running self.window may no longer be valid
|
||||
# if self is no longer running self.window may no longer be valid
|
||||
self.window.update_idletasks()
|
||||
except queue.Empty:
|
||||
pass
|
||||
@@ -420,6 +423,7 @@ def get_rom_frame(parent=None):
|
||||
romVar.set(rom)
|
||||
romSelectButton['state'] = "disabled"
|
||||
romSelectButton["text"] = "ROM verified"
|
||||
|
||||
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
@@ -444,17 +448,21 @@ def get_rom_options_frame(parent=None):
|
||||
MusicCheckbutton.grid(row=0, column=0, sticky=E)
|
||||
|
||||
vars.disableFlashingVar = IntVar(value=1)
|
||||
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
|
||||
disableFlashingCheckbutton.grid(row=6, column=0, sticky=E)
|
||||
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
|
||||
variable=vars.disableFlashingVar)
|
||||
disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
|
||||
|
||||
vars.DeathLinkVar = IntVar(value=0)
|
||||
DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
|
||||
DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
|
||||
|
||||
spriteDialogFrame = Frame(romOptionsFrame)
|
||||
spriteDialogFrame.grid(row=0, column=1)
|
||||
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
|
||||
|
||||
|
||||
|
||||
vars.spriteNameVar = StringVar()
|
||||
vars.sprite = None
|
||||
|
||||
def set_sprite(sprite_param):
|
||||
nonlocal vars
|
||||
if isinstance(sprite_param, str):
|
||||
@@ -491,7 +499,8 @@ def get_rom_options_frame(parent=None):
|
||||
menuspeedLabel.pack(side=LEFT)
|
||||
vars.menuspeedVar = StringVar()
|
||||
vars.menuspeedVar.set('normal')
|
||||
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple',
|
||||
'quadruple', 'half')
|
||||
menuspeedOptionMenu.pack(side=LEFT)
|
||||
|
||||
heartcolorFrame = Frame(romOptionsFrame)
|
||||
@@ -518,7 +527,8 @@ def get_rom_options_frame(parent=None):
|
||||
owPalettesLabel.pack(side=LEFT)
|
||||
vars.owPalettesVar = StringVar()
|
||||
vars.owPalettesVar.set('default')
|
||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale',
|
||||
'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
owPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
uwPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -527,7 +537,8 @@ def get_rom_options_frame(parent=None):
|
||||
uwPalettesLabel.pack(side=LEFT)
|
||||
vars.uwPalettesVar = StringVar()
|
||||
vars.uwPalettesVar.set('default')
|
||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale',
|
||||
'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
uwPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
hudPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -536,7 +547,8 @@ def get_rom_options_frame(parent=None):
|
||||
hudPalettesLabel.pack(side=LEFT)
|
||||
vars.hudPalettesVar = StringVar()
|
||||
vars.hudPalettesVar.set('default')
|
||||
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout',
|
||||
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
hudPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
swordPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -545,7 +557,8 @@ def get_rom_options_frame(parent=None):
|
||||
swordPalettesLabel.pack(side=LEFT)
|
||||
vars.swordPalettesVar = StringVar()
|
||||
vars.swordPalettesVar.set('default')
|
||||
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout',
|
||||
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
swordPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
shieldPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -554,7 +567,8 @@ def get_rom_options_frame(parent=None):
|
||||
shieldPalettesLabel.pack(side=LEFT)
|
||||
vars.shieldPalettesVar = StringVar()
|
||||
vars.shieldPalettesVar.set('default')
|
||||
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout',
|
||||
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
shieldPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
spritePoolFrame = Frame(romOptionsFrame)
|
||||
@@ -563,6 +577,7 @@ def get_rom_options_frame(parent=None):
|
||||
|
||||
vars.spritePoolCountVar = StringVar()
|
||||
vars.sprite_pool = []
|
||||
|
||||
def set_sprite_pool(sprite_param):
|
||||
nonlocal vars
|
||||
operation = "add"
|
||||
@@ -632,8 +647,10 @@ class SpriteSelector():
|
||||
title_link.pack(side=LEFT)
|
||||
title_link.bind("<Button-1>", open_custom_sprite_dir)
|
||||
|
||||
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir, 'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
|
||||
self.icon_section(custom_frametitle, self.custom_sprite_dir, 'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
|
||||
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir,
|
||||
'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
|
||||
self.icon_section(custom_frametitle, self.custom_sprite_dir,
|
||||
'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
|
||||
if not randomOnEvent:
|
||||
self.sprite_pool_section(spritePool)
|
||||
|
||||
@@ -683,7 +700,8 @@ class SpriteSelector():
|
||||
button = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
|
||||
button.pack(side=LEFT, padx=(0, 5))
|
||||
|
||||
button = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
|
||||
button = Checkbutton(frame, text="Random", command=self.update_random_button,
|
||||
variable=self.randomOnRandomVar)
|
||||
button.pack(side=LEFT, padx=(0, 5))
|
||||
|
||||
if adjuster:
|
||||
@@ -805,7 +823,6 @@ class SpriteSelector():
|
||||
|
||||
BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
|
||||
|
||||
|
||||
def browse_for_sprite(self):
|
||||
sprite = filedialog.askopenfilename(
|
||||
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
|
||||
@@ -819,7 +836,6 @@ class SpriteSelector():
|
||||
self.callback(None)
|
||||
self.window.destroy()
|
||||
|
||||
|
||||
def use_default_sprite(self):
|
||||
self.callback(None)
|
||||
self.window.destroy()
|
||||
@@ -923,7 +939,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||
gif_lsd = bytearray(7)
|
||||
gif_lsd[0] = width
|
||||
gif_lsd[2] = height
|
||||
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
|
||||
gif_lsd[
|
||||
4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
|
||||
gif_lsd[5] = 0 # background color is zero
|
||||
gif_lsd[6] = 0 # aspect raio not specified
|
||||
gif_gct = bytearray(3 * 32)
|
||||
@@ -943,7 +960,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||
gif_id[7] = height
|
||||
gif_id[9] = 0 # no local color table
|
||||
|
||||
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
|
||||
gif_img_minimum_code_size = bytes(
|
||||
[7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
|
||||
|
||||
clear = 0x80
|
||||
stop = 0x81
|
||||
@@ -1100,5 +1118,6 @@ class ToolTips(object):
|
||||
widget.after_cancel(cls.after_id)
|
||||
cls.after_id = None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
461
Main.py
461
Main.py
@@ -1,189 +1,123 @@
|
||||
from itertools import zip_longest
|
||||
from itertools import zip_longest, chain
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
import zlib
|
||||
import concurrent.futures
|
||||
import pickle
|
||||
import tempfile
|
||||
import zipfile
|
||||
from typing import Dict, Tuple
|
||||
from typing import Dict, Tuple, Optional
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from Utils import output_path, get_options, __version__, version_tuple
|
||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||
from worlds import AutoWorld
|
||||
|
||||
seeddigits = 20
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
def get_seed(seed=None):
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
return random.randint(0, pow(10, seeddigits) - 1)
|
||||
return seed
|
||||
|
||||
|
||||
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
|
||||
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
if seed_def in seeds:
|
||||
return seeds[seed_def]
|
||||
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
||||
world.__named_seeds = seeds
|
||||
return seeds[seed_def]
|
||||
|
||||
|
||||
def main(args, seed=None):
|
||||
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
||||
if not baked_server_options:
|
||||
baked_server_options = get_options()["server_options"]
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
# initialize the world
|
||||
world = MultiWorld(args.multi)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
world.seed = get_seed(seed)
|
||||
if args.race:
|
||||
world.secure()
|
||||
else:
|
||||
world.random.seed(world.seed)
|
||||
world.seed_name = str(args.outputname if args.outputname else world.seed)
|
||||
logger = logging.getLogger()
|
||||
world.set_seed(seed, args.race, 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.swordless = args.swordless.copy()
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.local_items = args.local_items.copy()
|
||||
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
world.algorithm = args.algorithm
|
||||
world.shuffleganon = args.shuffleganon
|
||||
world.custom = args.custom
|
||||
world.customitemarray = args.customitemarray
|
||||
|
||||
world.accessibility = args.accessibility.copy()
|
||||
world.retro = args.retro.copy()
|
||||
|
||||
world.hints = args.hints.copy()
|
||||
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.enemy_shuffle.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.killable_thieves = args.killable_thieves.copy()
|
||||
world.bush_shuffle = args.bush_shuffle.copy()
|
||||
world.tile_shuffle = args.tile_shuffle.copy()
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.countdown_start_time = args.countdown_start_time.copy()
|
||||
world.red_clock_time = args.red_clock_time.copy()
|
||||
world.blue_clock_time = args.blue_clock_time.copy()
|
||||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.shufflepots = args.shufflepots.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.glitch_boots = args.glitch_boots.copy()
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
world.shop_shuffle = args.shop_shuffle.copy()
|
||||
world.progression_balancing = args.progression_balancing.copy()
|
||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
world.dark_room_logic = args.dark_room_logic.copy()
|
||||
world.plando_items = args.plando_items.copy()
|
||||
world.plando_texts = args.plando_texts.copy()
|
||||
world.plando_connections = args.plando_connections.copy()
|
||||
world.er_seeds = getattr(args, "er_seeds", {})
|
||||
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.set_options(args)
|
||||
world.player_name = args.name.copy()
|
||||
world.alttp_rom = args.rom
|
||||
world.enemizer = args.enemizercli
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
|
||||
range(1, world.players + 1)}
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
# system for sharing ER layouts
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in world.shuffle[player]:
|
||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("group-") or args.race:
|
||||
world.er_seeds[player] = get_same_seed(world, (
|
||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info("Found World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
numlength = 8
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
|
||||
if not cls.hidden:
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
|
||||
f"{len(cls.location_names):3} Locations")
|
||||
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
|
||||
f"{max(cls.item_id_to_name):{numlength}} | "
|
||||
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
|
||||
f"{max(cls.location_id_to_name):{numlength}}")
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
logger.info('')
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
for item_name, count in world.start_inventory[player].value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
|
||||
for player in world.player_ids:
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
# enforce pre-defined local items.
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
world.local_items[player].add('Triforce Piece')
|
||||
|
||||
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
|
||||
if not world.mapshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Maps']
|
||||
|
||||
if not world.compassshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Compasses']
|
||||
|
||||
if not world.keyshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Small Keys']
|
||||
# This could probably use a more elegant solution.
|
||||
elif world.keyshuffle[player] == True and world.mode[player] == "Standard":
|
||||
world.local_items[player].add("Small Key (Hyrule Castle)")
|
||||
if not world.bigkeyshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Big Keys']
|
||||
world.local_items[player].value.add('Triforce Piece')
|
||||
|
||||
# Not possible to place pendants/crystals out side of boss prizes yet.
|
||||
world.non_local_items[player] -= item_name_groups['Pendants']
|
||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||
world.non_local_items[player].value -= item_name_groups['Pendants']
|
||||
world.non_local_items[player].value -= item_name_groups['Crystals']
|
||||
|
||||
# items can't be both local and non-local, prefer local
|
||||
world.non_local_items[player] -= world.local_items[player]
|
||||
world.non_local_items[player].value -= world.local_items[player].value
|
||||
|
||||
logger.info('Creating World.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
@@ -195,11 +129,14 @@ def main(args, seed=None):
|
||||
if world.players > 1:
|
||||
for player in world.player_ids:
|
||||
locality_rules(world, player)
|
||||
else:
|
||||
world.non_local_items[1].value = set()
|
||||
world.local_items[1].value = set()
|
||||
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
|
||||
for player in world.player_ids:
|
||||
exclusion_rules(world, player, args.excluded_locations[player])
|
||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
||||
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
|
||||
@@ -214,188 +151,194 @@ def main(args, seed=None):
|
||||
|
||||
AutoWorld.call_all(world, "pre_fill")
|
||||
|
||||
logger.info('Fill the world.')
|
||||
logger.info(f'Filling the world with {len(world.itempool)} items.')
|
||||
|
||||
if world.algorithm == 'flood':
|
||||
flood_items(world) # different algo, biased towards early game progress items
|
||||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world)
|
||||
|
||||
logger.info("Filling Shop Slots")
|
||||
|
||||
ShopSlotFill(world)
|
||||
AutoWorld.call_all(world, 'post_fill')
|
||||
|
||||
if world.players > 1:
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
logger.info('Generating output files.')
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor()
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
|
||||
output_file_futures = []
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||
for player in world.player_ids:
|
||||
# skip starting a thread for methods that say "pass".
|
||||
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
|
||||
for player in world.player_ids:
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
|
||||
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, RegionType.Generic):
|
||||
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 world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
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 != "A Link to the Past":
|
||||
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 = world.create_item(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
|
||||
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||
games = {}
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player].append(item.code)
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
# for now special case Factorio tech_tree_information
|
||||
sending_visible_players = set()
|
||||
for player in world.get_game_players("Factorio"):
|
||||
if world.tech_tree_information[player].value == 2:
|
||||
sending_visible_players.add(player)
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
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)
|
||||
if type(location.address) is int:
|
||||
main_entrance = get_entrance_to_region(location.parent_region)
|
||||
if location.game != "A Link to the Past":
|
||||
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
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": [[name for player, name in sorted(world.player_name.items())]],
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"remote_items": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_items},
|
||||
"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
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
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
|
||||
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
||||
item = world.create_item(
|
||||
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
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
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
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
if multidata_task:
|
||||
multidata_task.result() # retrieve exception if one exists
|
||||
pool.shutdown() # wait for all queued tasks to complete
|
||||
if not args.skip_playthrough:
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 1, 8), "clients": client_versions}
|
||||
games = {}
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
precollected_items = {player: [item.code for item in world_precollected]
|
||||
for player, world_precollected in world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
# for now special case Factorio tech_tree_information
|
||||
sending_visible_players = set()
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
if world.worlds[slot].sending_visible:
|
||||
sending_visible_players.add(slot)
|
||||
|
||||
def precollect_hint(location):
|
||||
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)
|
||||
|
||||
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:
|
||||
# item code None should be event, location.address should then also be None
|
||||
assert location.item.code is not None
|
||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
||||
if location.player in sending_visible_players:
|
||||
precollect_hint(location)
|
||||
elif location.name in world.start_location_hints[location.player]:
|
||||
precollect_hint(location)
|
||||
elif location.item.name in world.start_hints[location.item.player]:
|
||||
precollect_hint(location)
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": [[name for player, name in sorted(world.player_name.items())]],
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"remote_items": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_items},
|
||||
"remote_start_inventory": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_start_inventory},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": baked_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
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
|
||||
# retrieve exceptions via .result() if they occured.
|
||||
if multidata_task:
|
||||
multidata_task.result()
|
||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
||||
if i % 10 == 0 or i == len(output_file_futures):
|
||||
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
||||
future.result()
|
||||
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
if args.create_spoiler:
|
||||
|
||||
if args.spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
for future in output_file_futures:
|
||||
future.result()
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
logger.info(f'Creating final archive at {zipfilename}.')
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for file in os.scandir(temp_dir):
|
||||
zf.write(os.path.join(temp_dir, file), arcname=file.name)
|
||||
zf.write(file.path, arcname=file.name)
|
||||
|
||||
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
@@ -411,7 +354,6 @@ def create_playthrough(world):
|
||||
sphere_candidates = set(prog_locations)
|
||||
logging.debug('Building up collection spheres.')
|
||||
while sphere_candidates:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
@@ -431,7 +373,7 @@ def create_playthrough(world):
|
||||
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
||||
location.item.name, location.item.player, location.name, location.player) for location in
|
||||
sphere_candidates])
|
||||
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
|
||||
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||
f'Something went terribly wrong here.')
|
||||
else:
|
||||
@@ -461,9 +403,9 @@ def create_playthrough(world):
|
||||
|
||||
# second phase, sphere 0
|
||||
removed_precollected = []
|
||||
for item in (i for i in world.precollected_items if i.advancement):
|
||||
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
world.precollected_items.remove(item)
|
||||
world.precollected_items[item.player].remove(item)
|
||||
world.state.remove(item)
|
||||
if not world.can_beat_game():
|
||||
world.push_precollected(item)
|
||||
@@ -516,17 +458,20 @@ def create_playthrough(world):
|
||||
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
||||
sphere if location.player == player})
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
for path in dict(world.spoiler.paths).values():
|
||||
if any(exit_path == 'Pyramid Fairy' for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Inverted Big Bomb Shop', player))
|
||||
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
|
||||
# Maybe move the big bomb over to the Event system instead?
|
||||
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state, world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in
|
||||
chain.from_iterable(world.precollected_items.values())
|
||||
if item.advancement])}
|
||||
|
||||
for i, sphere in enumerate(collection_spheres):
|
||||
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
||||
|
||||
@@ -4,8 +4,8 @@ import re
|
||||
import atexit
|
||||
from subprocess import Popen
|
||||
from shutil import copyfile
|
||||
from base64 import b64decode
|
||||
from time import strftime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
@@ -34,7 +34,7 @@ def prompt_yes_no(prompt):
|
||||
def find_forge_jar(forge_dir):
|
||||
for entry in os.scandir(forge_dir):
|
||||
if ".jar" in entry.name and "forge" in entry.name:
|
||||
print(f"Found forge .jar: {entry.name}")
|
||||
logging.info(f"Found forge .jar: {entry.name}")
|
||||
return entry.name
|
||||
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
|
||||
|
||||
@@ -47,12 +47,12 @@ def find_ap_randomizer_jar(forge_dir):
|
||||
for entry in os.scandir(mods_dir):
|
||||
match = ap_mod_re.match(entry.name)
|
||||
if match:
|
||||
print(f"Found AP randomizer mod: {match.group()}")
|
||||
logging.info(f"Found AP randomizer mod: {match.group()}")
|
||||
return match.group()
|
||||
return None
|
||||
else:
|
||||
os.mkdir(mods_dir)
|
||||
print(f"Created mods folder in {forge_dir}")
|
||||
logging.info(f"Created mods folder in {forge_dir}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -61,15 +61,20 @@ def replace_apmc_files(forge_dir, apmc_file):
|
||||
if apmc_file is None:
|
||||
return
|
||||
apdata_dir = os.path.join(forge_dir, 'APData')
|
||||
copy_apmc = True
|
||||
if not os.path.isdir(apdata_dir):
|
||||
os.mkdir(apdata_dir)
|
||||
print(f"Created APData folder in {forge_dir}")
|
||||
logging.info(f"Created APData folder in {forge_dir}")
|
||||
for entry in os.scandir(apdata_dir):
|
||||
if ".apmc" in entry.name and entry.is_file():
|
||||
os.remove(entry.path)
|
||||
print(f"Removed {entry.name} in {apdata_dir}")
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
if entry.name.endswith(".apmc") and entry.is_file():
|
||||
if not os.path.samefile(apmc_file, entry.path):
|
||||
os.remove(entry.path)
|
||||
logging.info(f"Removed {entry.name} in {apdata_dir}")
|
||||
else: # apmc already in apdata
|
||||
copy_apmc = False
|
||||
if copy_apmc:
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
|
||||
|
||||
# Check mod version, download new mod from GitHub releases page if needed.
|
||||
@@ -81,30 +86,31 @@ def update_mod(forge_dir):
|
||||
if resp.status_code == 200: # OK
|
||||
latest_release = resp.json()[0]
|
||||
if ap_randomizer != latest_release['assets'][0]['name']:
|
||||
print(f"A new release of the Minecraft AP randomizer mod was found: {latest_release['assets'][0]['name']}")
|
||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
||||
f"{latest_release['assets'][0]['name']}")
|
||||
if ap_randomizer is not None:
|
||||
print(f"Your current mod is {ap_randomizer}.")
|
||||
logging.info(f"Your current mod is {ap_randomizer}.")
|
||||
else:
|
||||
print(f"You do not have the AP randomizer mod installed.")
|
||||
logging.info(f"You do not have the AP randomizer mod installed.")
|
||||
if prompt_yes_no("Would you like to update?"):
|
||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
|
||||
print("Downloading AP randomizer mod. This may take a moment...")
|
||||
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
||||
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
|
||||
if apmod_resp.status_code == 200:
|
||||
with open(new_ap_mod, 'wb') as f:
|
||||
f.write(apmod_resp.content)
|
||||
print(f"Wrote new mod file to {new_ap_mod}")
|
||||
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
||||
if old_ap_mod is not None:
|
||||
os.remove(old_ap_mod)
|
||||
print(f"Removed old mod file from {old_ap_mod}")
|
||||
logging.info(f"Removed old mod file from {old_ap_mod}")
|
||||
else:
|
||||
print(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
print(f"Please report this issue on the Archipelago Discord server.")
|
||||
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
|
||||
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
||||
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
|
||||
logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
||||
if not prompt_yes_no("Continue anyways?"):
|
||||
sys.exit(0)
|
||||
|
||||
@@ -122,13 +128,13 @@ def check_eula(forge_dir):
|
||||
text = f.read()
|
||||
if 'false' in text:
|
||||
# Prompt user to agree to the EULA
|
||||
print("You need to agree to the Minecraft EULA in order to run the server.")
|
||||
print("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
|
||||
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
|
||||
logging.info("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
|
||||
if prompt_yes_no("Do you agree to the EULA?"):
|
||||
f.seek(0)
|
||||
f.write(text.replace('false', 'true'))
|
||||
f.truncate()
|
||||
print(f"Set {eula_path} to true")
|
||||
logging.info(f"Set {eula_path} to true")
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
@@ -147,24 +153,25 @@ def run_forge_server(forge_dir, heap_arg):
|
||||
heap_arg = "-Xmx" + heap_arg
|
||||
|
||||
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
|
||||
print(f"Running Forge server: {argstring}")
|
||||
logging.info(f"Running Forge server: {argstring}")
|
||||
os.chdir(forge_dir)
|
||||
return Popen(argstring)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Utils.init_logging("MinecraftClient")
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
|
||||
|
||||
args = parser.parse_args()
|
||||
options = Utils.get_options()
|
||||
|
||||
apmc_file = os.path.abspath(args.apmc_file)
|
||||
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||
|
||||
# Change to executable's working directory
|
||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||
|
||||
options = Utils.get_options()
|
||||
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
|
||||
if apmc_file is not None and not os.path.isfile(apmc_file):
|
||||
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
|
||||
|
||||
@@ -35,18 +35,25 @@ def update(yes = False, force = False):
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(os.path.dirname(__file__), req_file)
|
||||
with open(path) as requirementsfile:
|
||||
requirements = pkg_resources.parse_requirements(requirementsfile)
|
||||
for requirement in requirements:
|
||||
requirement = str(requirement)
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
except pkg_resources.ResolutionError:
|
||||
if not yes:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
update_command()
|
||||
return
|
||||
for line in requirementsfile:
|
||||
if line.startswith('https://'):
|
||||
# extract name and version from url
|
||||
url = line.split(';')[0]
|
||||
wheel = line.split('/')[-1]
|
||||
name, version, _ = wheel.split('-',2)
|
||||
line = f'{name}=={version}'
|
||||
requirements = pkg_resources.parse_requirements(line)
|
||||
for requirement in requirements:
|
||||
requirement = str(requirement)
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
except pkg_resources.ResolutionError:
|
||||
if not yes:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
update_command()
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
607
MultiServer.py
607
MultiServer.py
@@ -13,9 +13,10 @@ import datetime
|
||||
import threading
|
||||
import random
|
||||
import pickle
|
||||
import itertools
|
||||
import time
|
||||
|
||||
import ModuleUpdate
|
||||
import NetUtils
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
@@ -25,16 +26,19 @@ import prompt_toolkit
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from fuzzywuzzy import process as fuzzy_process
|
||||
|
||||
import NetUtils
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
import Utils
|
||||
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, NetworkPlayer
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission
|
||||
|
||||
colorama.init()
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str] = []
|
||||
@@ -42,7 +46,6 @@ class Client(Endpoint):
|
||||
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
|
||||
super().__init__(socket)
|
||||
self.auth = False
|
||||
self.name = None
|
||||
self.team = None
|
||||
self.slot = None
|
||||
self.send_index = 0
|
||||
@@ -50,22 +53,42 @@ class Client(Endpoint):
|
||||
self.messageprocessor = client_message_processor(ctx, self)
|
||||
self.ctx = weakref.ref(ctx)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
ctx = self.ctx()
|
||||
if ctx:
|
||||
return ctx.player_names[self.team, self.slot]
|
||||
return "Deallocated"
|
||||
|
||||
|
||||
team_slot = typing.Tuple[int, int]
|
||||
|
||||
class Context(Node):
|
||||
|
||||
class Context:
|
||||
dumper = staticmethod(encode)
|
||||
loader = staticmethod(decode)
|
||||
|
||||
simple_options = {"hint_cost": int,
|
||||
"location_check_points": int,
|
||||
"server_password": str,
|
||||
"password": str,
|
||||
"forfeit_mode": str,
|
||||
"remaining_mode": str,
|
||||
"collect_mode": str,
|
||||
"item_cheat": bool,
|
||||
"compatibility": int}
|
||||
# team -> slot id -> list of clients authenticated to slot.
|
||||
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
|
||||
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]]
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||
log_network: bool = False):
|
||||
super(Context, self).__init__()
|
||||
self.log_network = log_network
|
||||
self.endpoints = []
|
||||
self.clients = {}
|
||||
self.compatibility: int = compatibility
|
||||
self.shutdown_task = None
|
||||
self.data_filename = None
|
||||
@@ -76,7 +99,9 @@ class Context(Node):
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.allow_forfeits = {}
|
||||
self.remote_items = set()
|
||||
self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
|
||||
self.remote_start_inventory = set()
|
||||
# player location_id item_id target_player_id
|
||||
self.locations = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_password = server_password
|
||||
@@ -92,6 +117,7 @@ class Context(Node):
|
||||
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
|
||||
self.forfeit_mode: str = forfeit_mode
|
||||
self.remaining_mode: str = remaining_mode
|
||||
self.collect_mode: str = collect_mode
|
||||
self.item_cheat = item_cheat
|
||||
self.running = True
|
||||
self.client_activity_timers: typing.Dict[
|
||||
@@ -113,10 +139,88 @@ class Context(Node):
|
||||
self.seed_name = ""
|
||||
self.random = random.Random()
|
||||
|
||||
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
|
||||
# General networking
|
||||
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
msg = self.dumper(msgs)
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
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}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
|
||||
sockets = []
|
||||
for endpoint in endpoints:
|
||||
if endpoint.socket and endpoint.socket.open:
|
||||
sockets.append(endpoint.socket)
|
||||
try:
|
||||
websockets.broadcast(sockets, msg)
|
||||
except RuntimeError:
|
||||
logging.exception("Exception during broadcast_send_encoded_msgs")
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing broadcast: {msg}")
|
||||
return True
|
||||
|
||||
def broadcast_all(self, msgs: typing.List[dict]):
|
||||
msgs = self.dumper(msgs)
|
||||
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
|
||||
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
||||
msgs = self.dumper(msgs)
|
||||
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
|
||||
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
|
||||
msgs = self.dumper(msgs)
|
||||
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
async def disconnect(self, endpoint: Client):
|
||||
if endpoint in self.endpoints:
|
||||
self.endpoints.remove(endpoint)
|
||||
if endpoint.slot and endpoint in self.clients[endpoint.team][endpoint.slot]:
|
||||
self.clients[endpoint.team][endpoint.slot].remove(endpoint)
|
||||
await on_client_disconnected(self, endpoint)
|
||||
|
||||
# text
|
||||
|
||||
def notify_all(self, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
# loading
|
||||
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
if multidatapath.lower().endswith(".zip"):
|
||||
@@ -153,14 +257,18 @@ class Context(Node):
|
||||
for player, version in clients_ver.items():
|
||||
self.minimum_client_versions[player] = Utils.Version(*version)
|
||||
|
||||
self.clients = {}
|
||||
for team, names in enumerate(decoded_obj['names']):
|
||||
self.clients[team] = {}
|
||||
for player, name in enumerate(names, 1):
|
||||
self.clients[team][player] = []
|
||||
self.player_names[team, player] = name
|
||||
self.player_name_lookup[name] = team, player
|
||||
self.seed_name = decoded_obj["seed_name"]
|
||||
self.random.seed(self.seed_name)
|
||||
self.connect_names = decoded_obj['connect_names']
|
||||
self.remote_items = decoded_obj['remote_items']
|
||||
self.remote_start_inventory = decoded_obj.get('remote_start_inventory', 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()}
|
||||
@@ -169,35 +277,20 @@ class Context(Node):
|
||||
# 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:
|
||||
if slot in self.remote_start_inventory:
|
||||
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)
|
||||
# declare slots without checks as done, as they're assumed to be spectators
|
||||
for slot, locations in self.locations.items():
|
||||
if not locations:
|
||||
for team in self.clients:
|
||||
self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL
|
||||
if use_embedded_server_options:
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
# saving
|
||||
|
||||
def save(self, now=False) -> bool:
|
||||
if self.saving:
|
||||
@@ -227,8 +320,8 @@ class Context(Node):
|
||||
if not self.save_filename:
|
||||
import os
|
||||
name, ext = os.path.splitext(self.data_filename)
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
try:
|
||||
with open(self.save_filename, 'rb') as f:
|
||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||
@@ -256,13 +349,6 @@ class Context(Node):
|
||||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
|
||||
def recheck_hints(self):
|
||||
for team, slot in self.hints:
|
||||
self.hints[team, slot] = {
|
||||
hint.re_check(self, team) for hint in
|
||||
self.hints[team, slot]
|
||||
}
|
||||
|
||||
def get_save(self) -> dict:
|
||||
self.recheck_hints()
|
||||
d = {
|
||||
@@ -303,42 +389,58 @@ class Context(Node):
|
||||
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')
|
||||
|
||||
# rest
|
||||
|
||||
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 recheck_hints(self):
|
||||
for team, slot in self.hints:
|
||||
self.hints[team, slot] = {
|
||||
hint.re_check(self, team) for hint in
|
||||
self.hints[team, slot]
|
||||
}
|
||||
|
||||
def get_players_package(self):
|
||||
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():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
|
||||
def get_aliased_name(self, team: int, slot: int):
|
||||
if (team, slot) in self.name_aliases:
|
||||
return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})"
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_all(self, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
def broadcast_team(self, team, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for client in self.endpoints:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(self.send_encoded_msgs(client, msgs))
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
if endpoint.auth:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
await super(Context, self).disconnect(endpoint)
|
||||
await on_client_disconnected(self, endpoint)
|
||||
def on_goal_achieved(self, client: Client):
|
||||
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
||||
f' has completed their goal.'
|
||||
self.notify_all(finished_msg)
|
||||
if "auto" in self.forfeit_mode:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
if "auto" in self.collect_mode:
|
||||
collect_player(self, client.team, client.slot)
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||
@@ -351,25 +453,23 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||
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:
|
||||
client_hints = concerns[client.slot]
|
||||
if client_hints:
|
||||
for slot, clients in ctx.clients[team].items():
|
||||
client_hints = concerns[slot]
|
||||
if client_hints:
|
||||
for client in clients:
|
||||
asyncio.create_task(ctx.send_msgs(client, client_hints))
|
||||
|
||||
|
||||
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
|
||||
def update_aliases(ctx: Context, team: int):
|
||||
cmd = ctx.dumper([{"cmd": "RoomUpdate",
|
||||
"players": ctx.get_players_package()}])
|
||||
if client is None:
|
||||
for client in ctx.endpoints:
|
||||
if client.team == team and client.auth:
|
||||
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
||||
else:
|
||||
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
||||
|
||||
for clients in ctx.clients[team].values():
|
||||
for client in clients:
|
||||
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
||||
|
||||
|
||||
async def server(websocket, path, ctx: Context):
|
||||
async def server(websocket, path: str = "/", ctx: Context = None):
|
||||
client = Client(websocket, ctx)
|
||||
ctx.endpoints.append(client)
|
||||
|
||||
@@ -394,28 +494,43 @@ async def server(websocket, path, ctx: Context):
|
||||
|
||||
|
||||
async def on_client_connected(ctx: Context, client: Client):
|
||||
players = []
|
||||
for team, clients in ctx.clients.items():
|
||||
for slot, connected_clients in clients.items():
|
||||
if connected_clients:
|
||||
name = ctx.player_names[team, slot]
|
||||
players.append(
|
||||
NetworkPlayer(team, slot,
|
||||
ctx.name_aliases.get((team, slot), name), name)
|
||||
)
|
||||
await ctx.send_msgs(client, [{
|
||||
'cmd': 'RoomInfo',
|
||||
'password': ctx.password is not None,
|
||||
'players': [
|
||||
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
|
||||
client.name) for client
|
||||
in ctx.endpoints if client.auth],
|
||||
'password': bool(ctx.password),
|
||||
'players': players,
|
||||
'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)],
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ctx.tags,
|
||||
'version': Utils.version_tuple,
|
||||
'forfeit_mode': ctx.forfeit_mode,
|
||||
'remaining_mode': ctx.remaining_mode,
|
||||
'permissions': get_permissions(ctx),
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_version': network_data_package["version"],
|
||||
'datapackage_versions': {game: game_data["version"] for game, game_data
|
||||
in network_data_package["games"].items()},
|
||||
'seed_name': ctx.seed_name
|
||||
'seed_name': ctx.seed_name,
|
||||
'time': time.time(),
|
||||
}])
|
||||
|
||||
|
||||
def get_permissions(ctx) -> typing.Dict[str, Permission]:
|
||||
return {
|
||||
"forfeit": Permission.from_text(ctx.forfeit_mode),
|
||||
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||
"collect": Permission.from_text(ctx.collect_mode)
|
||||
}
|
||||
|
||||
|
||||
async def on_client_disconnected(ctx: Context, client: Client):
|
||||
if client.auth:
|
||||
await on_client_left(ctx, client)
|
||||
@@ -424,9 +539,10 @@ async def on_client_disconnected(ctx: Context, client: Client):
|
||||
async def on_client_joined(ctx: Context, client: Client):
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||
version_str = '.'.join(str(x) for x in client.version)
|
||||
verb = "tracking" if "Tracker" in client.tags else "playing"
|
||||
ctx.notify_all(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||
f"playing {ctx.games[client.slot]} has joined. "
|
||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||
f"Client({version_str}), {client.tags}).")
|
||||
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
@@ -471,27 +587,59 @@ def get_players_string(ctx: Context):
|
||||
return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1]
|
||||
|
||||
|
||||
def get_status_string(ctx: Context, team: int):
|
||||
text = "Player Status on your team:"
|
||||
for slot in ctx.locations:
|
||||
connected = len(ctx.clients[team][slot])
|
||||
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
|
||||
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
|
||||
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
|
||||
f"{goal_text} {completion_text}"
|
||||
return text
|
||||
|
||||
|
||||
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[NetworkItem]:
|
||||
return ctx.received_items.setdefault((team, player), [])
|
||||
|
||||
|
||||
def send_new_items(ctx: Context):
|
||||
for client in ctx.endpoints:
|
||||
if client.auth: # can't send to disconnected client
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if len(items) > client.send_index:
|
||||
asyncio.create_task(ctx.send_msgs(client, [{
|
||||
"cmd": "ReceivedItems",
|
||||
"index": client.send_index,
|
||||
"items": items[client.send_index:]}]))
|
||||
client.send_index = len(items)
|
||||
for team, clients in ctx.clients.items():
|
||||
for slot, clients in clients.items():
|
||||
items = get_received_items(ctx, team, slot)
|
||||
for client in clients:
|
||||
if len(items) > client.send_index:
|
||||
asyncio.create_task(ctx.send_msgs(client, [{
|
||||
"cmd": "ReceivedItems",
|
||||
"index": client.send_index,
|
||||
"items": items[client.send_index:]}]))
|
||||
client.send_index = len(items)
|
||||
|
||||
|
||||
def update_checked_locations(ctx: Context, team: int, slot: int):
|
||||
ctx.broadcast(ctx.clients[team][slot],
|
||||
[{"cmd": "RoomUpdate", "checked_locations": get_checked_checks(ctx, team, slot)}])
|
||||
|
||||
|
||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||
# register any locations that are in the multidata
|
||||
"""register any locations that are in the multidata"""
|
||||
all_locations = 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)
|
||||
update_checked_locations(ctx, team, slot)
|
||||
|
||||
|
||||
def collect_player(ctx: Context, team: int, slot: int):
|
||||
"""register any locations that are in the multidata, pointing towards this player"""
|
||||
all_locations = collections.defaultdict(set)
|
||||
for source_slot, location_data in ctx.locations.items():
|
||||
for location_id, (item_id, target_player_id) in location_data.items():
|
||||
if target_player_id == slot:
|
||||
all_locations[source_slot].add(location_id)
|
||||
|
||||
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
|
||||
for source_player, location_ids in all_locations.items():
|
||||
register_location_checks(ctx, team, source_player, location_ids)
|
||||
update_checked_locations(ctx, team, source_player)
|
||||
|
||||
|
||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
@@ -521,10 +669,11 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
|
||||
ctx.location_checks[team, slot] |= new_locations
|
||||
send_new_items(ctx)
|
||||
for client in ctx.endpoints:
|
||||
if client.team == team and client.slot == slot:
|
||||
asyncio.create_task(ctx.send_msgs(client, [{"cmd": "RoomUpdate",
|
||||
"hint_points": get_client_points(ctx, client)}]))
|
||||
ctx.broadcast(ctx.clients[team][slot], [{
|
||||
"cmd": "RoomUpdate",
|
||||
"hint_points": get_slot_points(ctx, team, slot),
|
||||
"checked_locations": locations, # duplicated data, but used for coop
|
||||
}])
|
||||
|
||||
ctx.save()
|
||||
|
||||
@@ -574,15 +723,15 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
NetUtils.add_json_text(parts, net_item.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)
|
||||
NetUtils.add_json_item(parts, net_item.item, net_item.player)
|
||||
else:
|
||||
NetUtils.add_json_text(parts, " sent ")
|
||||
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
|
||||
NetUtils.add_json_item(parts, net_item.item, receiving_player)
|
||||
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_location(parts, net_item.location, net_item.player)
|
||||
NetUtils.add_json_text(parts, ")")
|
||||
|
||||
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
|
||||
@@ -796,6 +945,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(get_players_string(self.ctx))
|
||||
return True
|
||||
|
||||
def _cmd_status(self) -> bool:
|
||||
"""Get status information about your team."""
|
||||
self.output(get_status_string(self.ctx, self.client.team))
|
||||
return True
|
||||
|
||||
def _cmd_forfeit(self) -> bool:
|
||||
"""Surrender and send your remaining items out to their recipients"""
|
||||
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
|
||||
@@ -818,6 +972,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
" You can ask the server admin for a /forfeit")
|
||||
return False
|
||||
|
||||
def _cmd_collect(self) -> bool:
|
||||
"""Send your remaining items to yourself"""
|
||||
if "enabled" in self.ctx.collect_mode:
|
||||
collect_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
elif "disabled" in self.ctx.collect_mode:
|
||||
self.output(
|
||||
"Sorry, client collecting has been disabled on this server. You can ask the server admin for a /collect")
|
||||
return False
|
||||
else: # is auto or goal
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
collect_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
else:
|
||||
self.output(
|
||||
"Sorry, client collecting requires you to have beaten the game on this server."
|
||||
" You can ask the server admin for a /collect")
|
||||
return False
|
||||
|
||||
def _cmd_remaining(self) -> bool:
|
||||
"""List remaining items in your game, but not their location or recipient"""
|
||||
if self.ctx.remaining_mode == "enabled":
|
||||
@@ -849,7 +1022,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
def _cmd_missing(self) -> bool:
|
||||
"""List all missing location checks from the server's perspective"""
|
||||
|
||||
locations = get_missing_checks(self.ctx, self.client)
|
||||
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
if locations:
|
||||
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
|
||||
@@ -859,6 +1032,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output("No missing location checks found.")
|
||||
return True
|
||||
|
||||
def _cmd_checked(self) -> bool:
|
||||
"""List all done location checks from the server's perspective"""
|
||||
|
||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
if locations:
|
||||
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations]
|
||||
texts.append(f"Found {len(locations)} done location checks")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
self.output("No done location checks found.")
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
def _cmd_alias(self, alias_name: str = ""):
|
||||
"""Set your alias to the passed name."""
|
||||
@@ -899,14 +1085,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output("Cheating is disabled.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||
"""Use !hint {item_name/location_name},
|
||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||
If hint costs are on, this will only give you one new result,
|
||||
you can rerun the command to get more in that case."""
|
||||
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
if not item_or_location:
|
||||
if not input_text:
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||
@@ -916,16 +1097,17 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return True
|
||||
else:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
item_name, usable, response = get_intended_text(item_or_location, world.all_names)
|
||||
item_name, usable, response = get_intended_text(input_text,
|
||||
world.all_names if not explicit_location else world.location_names)
|
||||
if usable:
|
||||
if item_name in world.hint_blacklist:
|
||||
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif item_name in world.item_name_groups:
|
||||
elif item_name in world.item_name_groups and not explicit_location:
|
||||
hints = []
|
||||
for item in world.item_name_groups[item_name]:
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||
elif item_name in world.item_names: # item name
|
||||
elif item_name in world.item_names and not explicit_location: # item name
|
||||
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)
|
||||
@@ -958,19 +1140,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not hint.found:
|
||||
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
||||
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
||||
|
||||
if not_found_hints:
|
||||
if hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
f" You have {points_available} and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
elif hints:
|
||||
self.output(
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}")
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
notify_hints(self.ctx, self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
@@ -982,17 +1170,33 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||
"""Use !hint {item_name/location_name},
|
||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||
If hint costs are on, this will only give you one new result,
|
||||
you can rerun the command to get more in that case."""
|
||||
return self.get_hints(item_or_location)
|
||||
|
||||
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||
@mark_raw
|
||||
def _cmd_hint_location(self, location: str = "") -> bool:
|
||||
"""Use !hint_location {location_name},
|
||||
for example !hint_location atomic-bomb to get a spoiler peek for that location.
|
||||
(In the case of factorio, or any other game where item names and location names are identical,
|
||||
this command must be used explicitly.)"""
|
||||
return self.get_hints(location, True)
|
||||
|
||||
|
||||
def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
return [location_id for
|
||||
location_id in ctx.locations[client.slot] if
|
||||
location_id in ctx.location_checks[client.team, client.slot]]
|
||||
location_id in ctx.locations[slot] if
|
||||
location_id in ctx.location_checks[team, slot]]
|
||||
|
||||
|
||||
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||
def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
return [location_id for
|
||||
location_id in ctx.locations[client.slot] if
|
||||
location_id not in ctx.location_checks[client.team, client.slot]]
|
||||
location_id in ctx.locations[slot] if
|
||||
location_id not in ctx.location_checks[team, slot]]
|
||||
|
||||
|
||||
def get_client_points(ctx: Context, client: Client) -> int:
|
||||
@@ -1000,24 +1204,30 @@ def get_client_points(ctx: Context, client: Client) -> int:
|
||||
ctx.get_hint_cost(client.slot) * ctx.hints_used[client.team, client.slot])
|
||||
|
||||
|
||||
def get_slot_points(ctx: Context, team: int, slot: int) -> int:
|
||||
return (ctx.location_check_points * len(ctx.location_checks[team, slot]) -
|
||||
ctx.get_hint_cost(slot) * ctx.hints_used[team, slot])
|
||||
|
||||
|
||||
async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
try:
|
||||
cmd: str = args["cmd"]
|
||||
except:
|
||||
logging.exception(f"Could not get command from {args}")
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
|
||||
"text": f"Could not get command from {args} at `cmd`"}])
|
||||
raise
|
||||
|
||||
if type(cmd) is not str:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
|
||||
"text": f"Command should be str, got {type(cmd)}"}])
|
||||
return
|
||||
|
||||
if cmd == 'Connect':
|
||||
if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||
'game' not in args:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect'}])
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
|
||||
errors = set()
|
||||
@@ -1030,28 +1240,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
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:
|
||||
# likely same player with a "ghosted" slot. We bust the ghost.
|
||||
if "uuid" in args and ctx.client_ids[team, slot] == args["uuid"]:
|
||||
await ctx.send_msgs(clients[0], [{"cmd": "Print", "text": "You are getting kicked "
|
||||
"by yourself reconnecting."}])
|
||||
await clients[0].socket.close() # we have to await the DC of the ghost, so not to create data pasta
|
||||
client.name = ctx.player_names[(team, slot)]
|
||||
client.team = team
|
||||
client.slot = slot
|
||||
else:
|
||||
errors.add('SlotAlreadyTaken')
|
||||
else:
|
||||
client.name = ctx.player_names[(team, slot)]
|
||||
client.team = team
|
||||
client.slot = slot
|
||||
minver = ctx.minimum_client_versions[slot]
|
||||
if minver > args['version']:
|
||||
errors.add('IncompatibleVersion')
|
||||
if "IgnoreGame" not in args["tags"] and args['game'] != game:
|
||||
errors.add('InvalidGame')
|
||||
|
||||
# only exact version match allowed
|
||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||
@@ -1060,26 +1250,37 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
||||
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
||||
else:
|
||||
team, slot = ctx.connect_names[args['name']]
|
||||
if client.auth and client.team is not None and client.slot in ctx.clients[client.team]:
|
||||
ctx.clients[team][slot].remove(client) # re-auth, remove old entry
|
||||
if client.team != team or client.slot != slot:
|
||||
client.auth = False # swapping Team/Slot
|
||||
client.team = team
|
||||
client.slot = slot
|
||||
minver = ctx.minimum_client_versions[slot]
|
||||
if minver > args['version']:
|
||||
errors.add('IncompatibleVersion')
|
||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||
client.auth = True
|
||||
ctx.clients[team][slot].append(client)
|
||||
client.version = args['version']
|
||||
client.tags = args['tags']
|
||||
reply = [{
|
||||
"cmd": "Connected",
|
||||
"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),
|
||||
# get is needed for old multidata that was sparsely populated
|
||||
"slot_data": ctx.slot_data.get(client.slot, {})
|
||||
"missing_locations": get_missing_checks(ctx, team, slot),
|
||||
"checked_locations": get_checked_checks(ctx, team, slot),
|
||||
"slot_data": ctx.slot_data[client.slot]
|
||||
}]
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
|
||||
client.send_index = len(items)
|
||||
if not client.auth: # if this was a Re-Connect, don't print to console
|
||||
client.auth = True
|
||||
await on_client_joined(ctx, client)
|
||||
|
||||
await ctx.send_msgs(client, reply)
|
||||
await on_client_joined(ctx, client)
|
||||
|
||||
elif cmd == "GetDataPackage":
|
||||
exclusions = set(args.get("exclusions", []))
|
||||
@@ -1093,8 +1294,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
else:
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": network_data_package}])
|
||||
|
||||
elif client.auth:
|
||||
if cmd == 'Sync':
|
||||
if cmd == "ConnectUpdate":
|
||||
if not args:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': cmd,
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
|
||||
if "tags" in args:
|
||||
old_tags = client.tags
|
||||
client.tags = args["tags"]
|
||||
if set(old_tags) != set(client.tags):
|
||||
ctx.notify_all(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
|
||||
f"from {old_tags} to {client.tags}.")
|
||||
|
||||
elif cmd == 'Sync':
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
client.send_index = len(items)
|
||||
@@ -1102,13 +1318,20 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
"items": items}])
|
||||
|
||||
elif cmd == 'LocationChecks':
|
||||
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
||||
if "Tracker" in client.tags:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
|
||||
"text": "Trackers can't register new Location Checks",
|
||||
"original_cmd": cmd}])
|
||||
else:
|
||||
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
||||
|
||||
elif cmd == 'LocationScouts':
|
||||
locs = []
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
target_item, target_player = ctx.locations[client.slot][location]
|
||||
locs.append(NetworkItem(target_item, location, target_player))
|
||||
@@ -1120,7 +1343,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
|
||||
elif cmd == 'Say':
|
||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say'}])
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
|
||||
client.messageprocessor(args["text"])
|
||||
@@ -1143,10 +1367,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
||||
current = ctx.client_game_state[client.team, client.slot]
|
||||
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
|
||||
if new_status == ClientStatus.CLIENT_GOAL:
|
||||
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
|
||||
ctx.notify_all(finished_msg)
|
||||
if "auto" in ctx.forfeit_mode:
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
ctx.on_goal_achieved(client)
|
||||
|
||||
ctx.client_game_state[client.team, client.slot] = new_status
|
||||
|
||||
@@ -1164,20 +1385,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
def default(self, raw: str):
|
||||
self.ctx.notify_all('[Server]: ' + raw)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_kick(self, player_name: str) -> bool:
|
||||
"""Kick specified player from the server"""
|
||||
for client in self.ctx.endpoints:
|
||||
if client.auth and client.name.lower() == player_name.lower() and client.socket and not client.socket.closed:
|
||||
asyncio.create_task(client.socket.close())
|
||||
self.output(f"Kicked {self.ctx.get_aliased_name(client.team, client.slot)}")
|
||||
if self.ctx.commandprocessor.client == client:
|
||||
self.ctx.commandprocessor.client = None
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to kick")
|
||||
return False
|
||||
|
||||
def _cmd_save(self) -> bool:
|
||||
"""Save current state to multidata"""
|
||||
if self.ctx.saving:
|
||||
@@ -1226,9 +1433,21 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_collect(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items to player."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
collect_player(self.ctx, team, slot)
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to collect")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forfeit(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player's game to their intended recipients"""
|
||||
"""Send out the remaining items from a player to their intended recipients"""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
@@ -1332,6 +1551,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
return input_text
|
||||
setattr(self.ctx, option_name, attrtype(option))
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||
return True
|
||||
else:
|
||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||
@@ -1376,6 +1597,15 @@ def parse_args() -> argparse.Namespace:
|
||||
goal: !forfeit can be used after goal completion
|
||||
auto-enabled: !forfeit is available and automatically triggered on goal completion
|
||||
''')
|
||||
parser.add_argument('--collect_mode', default=defaults["collect_mode"], nargs='?',
|
||||
choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
|
||||
Select !collect Accessibility. (default: %(default)s)
|
||||
auto: Automatic "collect" on goal completion
|
||||
enabled: !collect is always available
|
||||
disabled: !collect is never available
|
||||
goal: !collect can be used after goal completion
|
||||
auto-enabled: !collect is available and automatically triggered on goal completion
|
||||
''')
|
||||
parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?',
|
||||
choices=['enabled', 'disabled', "goal"], help='''\
|
||||
Select !remaining Accessibility. (default: %(default)s)
|
||||
@@ -1426,13 +1656,12 @@ async def auto_shutdown(ctx, to_cancel=None):
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
logging.basicConfig(force=True,
|
||||
format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
Utils.init_logging("Server", loglevel=args.loglevel.lower())
|
||||
|
||||
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
|
||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.collect_mode,
|
||||
args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
@@ -1451,7 +1680,7 @@ async def main(args: argparse.Namespace):
|
||||
|
||||
ctx.init_save(not args.disable_save)
|
||||
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||
|
||||
93
NetUtils.py
93
NetUtils.py
@@ -1,6 +1,4 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
import enum
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
@@ -15,8 +13,8 @@ class JSONMessagePart(typing.TypedDict, total=False):
|
||||
# optional
|
||||
type: str
|
||||
color: str
|
||||
# mainly for items, optional
|
||||
found: bool
|
||||
# owning player for location/item
|
||||
player: int
|
||||
|
||||
|
||||
class ClientStatus(enum.IntEnum):
|
||||
@@ -27,6 +25,25 @@ class ClientStatus(enum.IntEnum):
|
||||
CLIENT_GOAL = 30
|
||||
|
||||
|
||||
class Permission(enum.IntEnum):
|
||||
disabled = 0b000 # 0, completely disables access
|
||||
enabled = 0b001 # 1, allows manual use
|
||||
goal = 0b010 # 2, allows manual use after goal completion
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
|
||||
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str):
|
||||
data = 0
|
||||
if "auto" in text:
|
||||
data |= 0b110
|
||||
elif "goal" in text:
|
||||
data |= 0b010
|
||||
if "enabled" in text:
|
||||
data |= 0b001
|
||||
return Permission(data)
|
||||
|
||||
|
||||
class NetworkPlayer(typing.NamedTuple):
|
||||
team: int
|
||||
slot: int
|
||||
@@ -45,7 +62,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
|
||||
data = obj._asdict()
|
||||
data["class"] = obj.__class__.__name__
|
||||
return data
|
||||
if isinstance(obj, (tuple, list)):
|
||||
if isinstance(obj, (tuple, list, set)):
|
||||
return tuple(_scan_for_TypedTuples(o) for o in obj)
|
||||
if isinstance(obj, dict):
|
||||
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
|
||||
@@ -94,53 +111,6 @@ def _object_hook(o: typing.Any) -> typing.Any:
|
||||
decode = JSONDecoder(object_hook=_object_hook).decode
|
||||
|
||||
|
||||
class Node:
|
||||
endpoints: typing.List
|
||||
dumper = staticmethod(encode)
|
||||
loader = staticmethod(decode)
|
||||
|
||||
def __init__(self):
|
||||
self.endpoints = []
|
||||
super(Node, self).__init__()
|
||||
self.log_network = 0
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
msg = self.dumper(msgs)
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
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}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
self.endpoints.remove(endpoint)
|
||||
|
||||
|
||||
class Endpoint:
|
||||
socket: websockets.WebSocketServerProtocol
|
||||
|
||||
@@ -198,8 +168,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
return "".join(self.handle_node(section) for section in input_object)
|
||||
|
||||
def handle_node(self, node: JSONMessagePart):
|
||||
type = node.get("type", None)
|
||||
handler = self.handlers.get(type, self.handlers["text"])
|
||||
node_type = node.get("type", None)
|
||||
handler = self.handlers.get(node_type, self.handlers["text"])
|
||||
return handler(node)
|
||||
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
@@ -270,6 +240,14 @@ def add_json_text(parts: list, text: typing.Any, **kwargs) -> None:
|
||||
parts.append({"text": str(text), **kwargs})
|
||||
|
||||
|
||||
def add_json_item(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.item_id, **kwargs})
|
||||
|
||||
|
||||
def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||
|
||||
|
||||
class Hint(typing.NamedTuple):
|
||||
receiving_player: int
|
||||
finding_player: int
|
||||
@@ -294,9 +272,9 @@ class Hint(typing.NamedTuple):
|
||||
add_json_text(parts, "[Hint]: ")
|
||||
add_json_text(parts, self.receiving_player, type="player_id")
|
||||
add_json_text(parts, "'s ")
|
||||
add_json_text(parts, self.item, type="item_id", found=self.found)
|
||||
add_json_item(parts, self.item, self.receiving_player)
|
||||
add_json_text(parts, " is at ")
|
||||
add_json_text(parts, self.location, type="location_id")
|
||||
add_json_location(parts, self.location, self.finding_player)
|
||||
add_json_text(parts, " in ")
|
||||
add_json_text(parts, self.finding_player, type="player_id")
|
||||
if self.entrance:
|
||||
@@ -311,7 +289,8 @@ class Hint(typing.NamedTuple):
|
||||
|
||||
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
||||
"receiving": self.receiving_player,
|
||||
"item": NetworkItem(self.item, self.location, self.finding_player)}
|
||||
"item": NetworkItem(self.item, self.location, self.finding_player),
|
||||
"found": self.found}
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
|
||||
214
Options.py
214
Options.py
@@ -9,7 +9,7 @@ class AssembleOptions(type):
|
||||
name_lookup = attrs["name_lookup"] = {}
|
||||
# merge parent class options
|
||||
for base in bases:
|
||||
if hasattr(base, "options"):
|
||||
if getattr(base, "options", None):
|
||||
options.update(base.options)
|
||||
name_lookup.update(base.name_lookup)
|
||||
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
||||
@@ -29,7 +29,9 @@ class AssembleOptions(type):
|
||||
def validate(self, *args, **kwargs):
|
||||
func(self, *args, **kwargs)
|
||||
self.value = self.schema.validate(self.value)
|
||||
|
||||
return validate
|
||||
|
||||
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
@@ -43,6 +45,9 @@ class Option(metaclass=AssembleOptions):
|
||||
# Handled in get_option_name()
|
||||
autodisplayname = False
|
||||
|
||||
# can be weighted between selections
|
||||
supports_weighting = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
||||
|
||||
@@ -57,11 +62,12 @@ class Option(metaclass=AssembleOptions):
|
||||
"""For display purposes."""
|
||||
return self.get_option_name(self.value)
|
||||
|
||||
def get_option_name(self, value: typing.Any) -> str:
|
||||
if self.autodisplayname:
|
||||
return self.name_lookup[self.value].replace("_", " ").title()
|
||||
@classmethod
|
||||
def get_option_name(cls, value: typing.Any) -> str:
|
||||
if cls.autodisplayname:
|
||||
return cls.name_lookup[value].replace("_", " ").title()
|
||||
else:
|
||||
return self.name_lookup[self.value]
|
||||
return cls.name_lookup[value]
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
@@ -80,6 +86,7 @@ class Toggle(Option):
|
||||
default = 0
|
||||
|
||||
def __init__(self, value: int):
|
||||
assert value == 0 or value == 1
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
@@ -114,9 +121,11 @@ class Toggle(Option):
|
||||
def __int__(self):
|
||||
return int(self.value)
|
||||
|
||||
def get_option_name(self, value):
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ["No", "Yes"][int(value)]
|
||||
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
default = 1
|
||||
|
||||
@@ -130,10 +139,8 @@ class Choice(Option):
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
# TODO: turn on after most people have adjusted their yamls to no longer have suboptions with "random" in them
|
||||
# maybe in 0.2?
|
||||
# if text == "random":
|
||||
# return cls(random.choice(list(cls.options.values())))
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
for optionname, value in cls.options.items():
|
||||
if optionname == text:
|
||||
return cls(value)
|
||||
@@ -147,6 +154,36 @@ class Choice(Option):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.value == self.value
|
||||
elif isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other == self.current_key
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other == self.value
|
||||
elif isinstance(other, bool):
|
||||
return other == bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.value != self.value
|
||||
elif isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other != self.current_key
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other != self.value
|
||||
elif isinstance(other, bool):
|
||||
return other != bool(self.value)
|
||||
elif other is None:
|
||||
return False
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
@@ -180,7 +217,7 @@ class Range(Option, int):
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
return str(self.value)
|
||||
return str(value)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
@@ -205,9 +242,11 @@ class OptionNameSet(Option):
|
||||
|
||||
class OptionDict(Option):
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
value: typing.Dict[str, typing.Any]
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
self.value: typing.Dict[str, typing.Any] = value
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||
@@ -217,30 +256,165 @@ class OptionDict(Option):
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
def get_option_name(self, value):
|
||||
return str(value)
|
||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
|
||||
|
||||
class ItemDict(OptionDict):
|
||||
# implemented by Generate
|
||||
verify_item_name = True
|
||||
|
||||
def __init__(self, value: typing.Dict[str, int]):
|
||||
if any(item_count < 1 for item_count in value.values()):
|
||||
raise Exception("Cannot have non-positive item counts.")
|
||||
super(ItemDict, self).__init__(value)
|
||||
|
||||
|
||||
class OptionList(Option):
|
||||
default = []
|
||||
supports_weighting = False
|
||||
value: list
|
||||
|
||||
def __init__(self, value: typing.List[str, typing.Any]):
|
||||
self.value = value
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
return cls([option.strip() for option in text.split(",")])
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if type(data) == list:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
return ", ".join(value)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
|
||||
|
||||
class OptionSet(Option):
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
value: set
|
||||
|
||||
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
|
||||
self.value = set(value)
|
||||
super(OptionSet, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
return cls([option.strip() for option in text.split(",")])
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if type(data) == list:
|
||||
return cls(data)
|
||||
elif type(data) == set:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
return ", ".join(value)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
|
||||
|
||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||
|
||||
|
||||
class Accessibility(Choice):
|
||||
"""Set rules for reachability of your items/locations.
|
||||
Locations: ensure everything can be reached and acquired.
|
||||
Items: ensure all logically relevant items can be acquired.
|
||||
Minimal: ensure what is needed to reach your goal can be acquired."""
|
||||
|
||||
option_locations = 0
|
||||
option_items = 1
|
||||
option_beatable = 2
|
||||
option_minimal = 2
|
||||
alias_none = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class ProgressionBalancing(DefaultOnToggle):
|
||||
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
|
||||
|
||||
|
||||
common_options = {
|
||||
"progression_balancing": ProgressionBalancing,
|
||||
"accessibility": Accessibility
|
||||
}
|
||||
|
||||
|
||||
class ItemSet(OptionSet):
|
||||
# implemented by Generate
|
||||
verify_item_name = True
|
||||
|
||||
|
||||
class LocalItems(ItemSet):
|
||||
"""Forces these items to be in their native world."""
|
||||
displayname = "Local Items"
|
||||
|
||||
|
||||
class NonLocalItems(ItemSet):
|
||||
"""Forces these items to be outside their native world."""
|
||||
displayname = "Not Local Items"
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
"""Start with these items."""
|
||||
verify_item_name = True
|
||||
displayname = "Start Inventory"
|
||||
|
||||
|
||||
class StartHints(ItemSet):
|
||||
"""Start with these item's locations prefilled into the !hint command."""
|
||||
displayname = "Start Hints"
|
||||
|
||||
|
||||
class StartLocationHints(OptionSet):
|
||||
displayname = "Start Location Hints"
|
||||
|
||||
|
||||
class ExcludeLocations(OptionSet):
|
||||
"""Prevent these locations from having an important item"""
|
||||
displayname = "Excluded Locations"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class DeathLink(Toggle):
|
||||
"""When you die, everyone dies. Of course the reverse is true too."""
|
||||
displayname = "Death Link"
|
||||
|
||||
|
||||
per_game_common_options = {
|
||||
"local_items": LocalItems,
|
||||
"non_local_items": NonLocalItems,
|
||||
"start_inventory": StartInventory,
|
||||
"start_hints": StartHints,
|
||||
"start_location_hints": StartLocationHints,
|
||||
"exclude_locations": OptionSet
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
from worlds.alttp.Options import Logic
|
||||
import argparse
|
||||
mapshuffle = Toggle
|
||||
compassshuffle = Toggle
|
||||
|
||||
map_shuffle = Toggle
|
||||
compass_shuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkeyshuffle = Toggle
|
||||
bigkey_shuffle = Toggle
|
||||
hints = Toggle
|
||||
test = argparse.Namespace()
|
||||
test.logic = Logic.from_text("no_logic")
|
||||
test.mapshuffle = mapshuffle.from_text("ON")
|
||||
test.map_shuffle = map_shuffle.from_text("ON")
|
||||
test.hints = hints.from_text('OFF')
|
||||
try:
|
||||
test.logic = Logic.from_text("overworld_glitches_typo")
|
||||
@@ -250,7 +424,7 @@ if __name__ == "__main__":
|
||||
test.logic_owg = Logic.from_text("owg")
|
||||
except KeyError as e:
|
||||
print(e)
|
||||
if test.mapshuffle:
|
||||
print("Mapshuffle is on")
|
||||
if test.map_shuffle:
|
||||
print("map_shuffle is on")
|
||||
print(f"Hints are {bool(test.hints)}")
|
||||
print(test)
|
||||
|
||||
98
Patch.py
98
Patch.py
@@ -1,3 +1,5 @@
|
||||
# TODO: convert this into a system like AutoWorld
|
||||
|
||||
import bsdiff4
|
||||
import yaml
|
||||
import os
|
||||
@@ -10,54 +12,88 @@ from typing import Tuple, Optional
|
||||
|
||||
import Utils
|
||||
|
||||
current_patch_version = 3
|
||||
|
||||
current_patch_version = 2
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
GAME_SM = "Super Metroid"
|
||||
GAME_SOE = "Secret of Evermore"
|
||||
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore"}
|
||||
|
||||
preferred_endings = {
|
||||
GAME_ALTTP: "apbp",
|
||||
GAME_SM: "apm3",
|
||||
GAME_SOE: "apsoe"
|
||||
}
|
||||
|
||||
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
from worlds.alttp.Rom import JAP10HASH
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
||||
if game == GAME_ALTTP:
|
||||
from worlds.alttp.Rom import JAP10HASH as HASH
|
||||
elif game == GAME_SM:
|
||||
from worlds.sm.Rom import JAP10HASH as HASH
|
||||
elif game == GAME_SOE:
|
||||
from worlds.soe.Patch import USHASH as HASH
|
||||
else:
|
||||
raise RuntimeError(f"Selected game {game} for base rom not found.")
|
||||
|
||||
patch = yaml.dump({"meta": metadata,
|
||||
"patch": patch,
|
||||
"game": "A Link to the Past",
|
||||
"game": game,
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 1,
|
||||
"compatible_version": 3,
|
||||
"version": current_patch_version,
|
||||
"base_checksum": JAP10HASH})
|
||||
"base_checksum": HASH})
|
||||
return patch.encode(encoding="utf-8-sig")
|
||||
|
||||
|
||||
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
from worlds.alttp.Rom import get_base_rom_bytes
|
||||
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
|
||||
return generate_yaml(patch, metadata)
|
||||
patch = bsdiff4.diff(get_base_rom_data(game), rom)
|
||||
return generate_yaml(patch, metadata, game)
|
||||
|
||||
|
||||
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: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> 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),
|
||||
meta)
|
||||
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
|
||||
meta,
|
||||
game)
|
||||
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + (
|
||||
".apbp" if game == GAME_ALTTP else ".apm3")
|
||||
write_lzma(bytes, target)
|
||||
return target
|
||||
|
||||
|
||||
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
|
||||
from worlds.alttp.Rom import get_base_rom_bytes
|
||||
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
||||
game_name = data["game"]
|
||||
if not ignore_version and data["compatible_version"] > current_patch_version:
|
||||
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
|
||||
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
|
||||
patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"])
|
||||
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
|
||||
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
|
||||
target = os.path.splitext(patch_file)[0] + ".sfc"
|
||||
return data["meta"], target, patched_data
|
||||
|
||||
|
||||
def get_base_rom_data(game: str):
|
||||
if game == GAME_ALTTP:
|
||||
from worlds.alttp.Rom import get_base_rom_bytes
|
||||
elif game == "alttp": # old version for A Link to the Past
|
||||
from worlds.alttp.Rom import get_base_rom_bytes
|
||||
elif game == GAME_SM:
|
||||
from worlds.sm.Rom import get_base_rom_bytes
|
||||
elif game == GAME_SOE:
|
||||
file_name = Utils.get_options()["soe_options"]["rom"]
|
||||
get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb")))
|
||||
else:
|
||||
raise RuntimeError("Selected game for base rom not found.")
|
||||
return get_base_rom_bytes()
|
||||
|
||||
|
||||
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
||||
data, target, patched_data = create_rom_bytes(patch_file)
|
||||
with open(target, "wb") as f:
|
||||
@@ -68,7 +104,7 @@ def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
||||
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
|
||||
data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig"))
|
||||
data["meta"]["server"] = server
|
||||
bytes = generate_yaml(data["patch"], data["meta"])
|
||||
bytes = generate_yaml(data["patch"], data["meta"], data["game"])
|
||||
return lzma.compress(bytes)
|
||||
|
||||
|
||||
@@ -82,6 +118,14 @@ def write_lzma(data: bytes, path: str):
|
||||
f.write(data)
|
||||
|
||||
|
||||
def read_rom(stream, strip_header=True) -> bytearray:
|
||||
"""Reads rom into bytearray and optionally strips off any smc header"""
|
||||
buffer = bytearray(stream.read())
|
||||
if strip_header and len(buffer) % 0x400 == 0x200:
|
||||
return buffer[0x200:]
|
||||
return buffer
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = Utils.get_public_ipv4()
|
||||
options = Utils.get_options()['server_options']
|
||||
@@ -113,7 +157,13 @@ if __name__ == "__main__":
|
||||
if 'server' in data:
|
||||
Utils.persistent_store("servers", data['hash'], data['server'])
|
||||
print(f"Host is {data['server']}")
|
||||
|
||||
elif rom.endswith(".apm3"):
|
||||
print(f"Applying patch {rom}")
|
||||
data, target = create_rom_file(rom)
|
||||
print(f"Created rom {target}.")
|
||||
if 'server' in data:
|
||||
Utils.persistent_store("servers", data['hash'], data['server'])
|
||||
print(f"Host is {data['server']}")
|
||||
elif rom.endswith(".archipelago"):
|
||||
import json
|
||||
import zlib
|
||||
@@ -139,7 +189,7 @@ if __name__ == "__main__":
|
||||
|
||||
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
|
||||
data = zfr.read(zfinfo)
|
||||
if zfinfo.filename.endswith(".apbp"):
|
||||
if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"):
|
||||
data = update_patch_data(data, server)
|
||||
with ziplock:
|
||||
zfw.writestr(zfinfo, data)
|
||||
@@ -160,12 +210,4 @@ if __name__ == "__main__":
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
input("Press enter to close.")
|
||||
|
||||
|
||||
def read_rom(stream, strip_header=True) -> bytearray:
|
||||
"""Reads rom into bytearray and optionally strips off any smc header"""
|
||||
buffer = bytearray(stream.read())
|
||||
if strip_header and len(buffer) % 0x400 == 0x200:
|
||||
return buffer[0x200:]
|
||||
return buffer
|
||||
input("Press enter to close.")
|
||||
13
README.md
13
README.md
@@ -7,8 +7,14 @@ Currently, the following games are supported:
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Slay the Spire
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
* Timespinner
|
||||
* Super Metroid
|
||||
* Secret of Evermore
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
windows binaries.
|
||||
|
||||
@@ -30,13 +36,14 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt
|
||||
## Running Archipelago
|
||||
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).
|
||||
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/ArchipelagoMW/Archipelago/wiki/Running-from-source).
|
||||
|
||||
## Related Repositories
|
||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
||||
|
||||
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
|
||||
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
|
||||
* [Enemizer](https://github.com/Ijwu/Enemizer)
|
||||
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome. We have a few asks of any new contributors.
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import argparse
|
||||
import atexit
|
||||
exit_func = atexit.register(input, "Press enter to close.")
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import base64
|
||||
import shutil
|
||||
import logging
|
||||
import asyncio
|
||||
from json import loads, dumps
|
||||
|
||||
from Utils import get_item_name_from_id
|
||||
from Utils import get_item_name_from_id, init_logging
|
||||
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
if __name__ == "__main__":
|
||||
init_logging("SNIClient")
|
||||
|
||||
import colorama
|
||||
|
||||
from NetUtils import *
|
||||
|
||||
from worlds.alttp import Regions, Shops
|
||||
from worlds.alttp import Items
|
||||
from worlds.alttp.Rom import ROM_PLAYER_LIMIT
|
||||
from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser
|
||||
from Patch import GAME_ALTTP, GAME_SM
|
||||
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
from MultiServer import mark_raw
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "LttPClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "LttPClient.txt"), "w"))
|
||||
class DeathState(enum.IntEnum):
|
||||
killing_player = 1
|
||||
alive = 2
|
||||
dead = 3
|
||||
|
||||
|
||||
class LttPCommandProcessor(ClientCommandProcessor):
|
||||
ctx: Context
|
||||
|
||||
def _cmd_slow_mode(self, toggle: str = ""):
|
||||
"""Toggle slow mode, which limits how fast you send / receive items."""
|
||||
if toggle:
|
||||
@@ -53,11 +53,27 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
||||
|
||||
@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"""
|
||||
def _cmd_snes(self, snes_options: str = "") -> bool:
|
||||
"""Connect to a snes. Optionally include network address of a snes to connect to,
|
||||
otherwise show available devices; and a SNES device number if more than one SNES is detected"""
|
||||
|
||||
snes_address = self.ctx.snes_address
|
||||
snes_device_number = -1
|
||||
|
||||
options = snes_options.split()
|
||||
num_options = len(options)
|
||||
|
||||
if num_options > 0:
|
||||
snes_address = options[0]
|
||||
|
||||
if num_options > 1:
|
||||
try:
|
||||
snes_device_number = int(options[1])
|
||||
except:
|
||||
pass
|
||||
|
||||
self.ctx.snes_reconnect_address = None
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address if snes_address else self.ctx.snes_address))
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
|
||||
return True
|
||||
|
||||
def _cmd_snes_close(self) -> bool:
|
||||
@@ -69,6 +85,19 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||
else:
|
||||
return False
|
||||
|
||||
# Left here for quick re-addition for debugging.
|
||||
# def _cmd_snes_write(self, address, data):
|
||||
# """Write the specified byte (base10) to the SNES' memory address (base16)."""
|
||||
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
|
||||
# self.output("No attached SNES Device.")
|
||||
# return False
|
||||
#
|
||||
# snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)]))
|
||||
# asyncio.create_task(snes_flush_writes(self.ctx))
|
||||
# self.output("Data Sent")
|
||||
# return True
|
||||
|
||||
|
||||
class Context(CommonContext):
|
||||
command_processor = LttPCommandProcessor
|
||||
game = "A Link to the Past"
|
||||
@@ -86,6 +115,8 @@ class Context(CommonContext):
|
||||
self.snes_request_lock = asyncio.Lock()
|
||||
self.snes_write_buffer = []
|
||||
self.snes_connector_lock = threading.Lock()
|
||||
self.death_state = DeathState.alive # for death link flop behaviour
|
||||
self.killing_player_task = None
|
||||
|
||||
self.awaiting_rom = False
|
||||
self.rom = None
|
||||
@@ -101,7 +132,7 @@ class Context(CommonContext):
|
||||
raise Exception('Invalid ROM detected, '
|
||||
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(Context, self).server_auth(password_requested)
|
||||
if self.rom is None:
|
||||
@@ -113,10 +144,52 @@ class Context(CommonContext):
|
||||
self.auth = self.rom
|
||||
auth = base64.b64encode(self.rom).decode()
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
|
||||
'tags': get_tags(self),
|
||||
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
|
||||
}])
|
||||
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
|
||||
'tags': self.tags,
|
||||
'uuid': Utils.get_unique_identifier(),
|
||||
'game': self.game
|
||||
}])
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
if not self.killing_player_task or self.killing_player_task.done():
|
||||
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
|
||||
super(Context, self).on_deathlink(data)
|
||||
|
||||
async def handle_deathlink_state(self, currently_dead: bool):
|
||||
# in this state we only care about triggering a death send
|
||||
if self.death_state == DeathState.alive:
|
||||
if currently_dead:
|
||||
self.death_state = DeathState.dead
|
||||
await self.send_death()
|
||||
# in this state we care about confirming a kill, to move state to dead
|
||||
elif self.death_state == DeathState.killing_player:
|
||||
# this is being handled in deathlink_kill_player(ctx) already
|
||||
pass
|
||||
# in this state we wait until the player is alive again
|
||||
elif self.death_state == DeathState.dead:
|
||||
if not currently_dead:
|
||||
self.death_state = DeathState.alive
|
||||
|
||||
|
||||
async def deathlink_kill_player(ctx: Context):
|
||||
ctx.death_state = DeathState.killing_player
|
||||
while ctx.death_state == DeathState.killing_player and \
|
||||
ctx.snes_state == SNESState.SNES_ATTACHED:
|
||||
if ctx.game == GAME_ALTTP:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
|
||||
elif ctx.game == GAME_SM:
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
gamemode = None
|
||||
if ctx.game == GAME_ALTTP:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
elif ctx.game == GAME_SM:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if not gamemode or gamemode[0] in (DEATH_MODES if ctx.game == GAME_ALTTP else SM_DEATH_MODES):
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
|
||||
def color_item(item_id: int, green: bool = False) -> str:
|
||||
@@ -129,6 +202,7 @@ def color_item(item_id: int, green: bool = False) -> str:
|
||||
|
||||
SNES_RECONNECT_DELAY = 5
|
||||
|
||||
# LttP
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
@@ -139,6 +213,7 @@ ROMNAME_SIZE = 0x15
|
||||
|
||||
INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
ENDGAME_MODES = {0x19, 0x1a}
|
||||
DEATH_MODES = {0x12}
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
@@ -154,6 +229,21 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
||||
|
||||
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
|
||||
|
||||
# SM
|
||||
SM_ROMNAME_START = 0x1C4F00
|
||||
|
||||
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SM_ENDGAME_MODES = {0x26, 0x27}
|
||||
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
|
||||
|
||||
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
|
||||
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
|
||||
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
|
||||
|
||||
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
||||
|
||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
@@ -377,7 +467,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
||||
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
||||
|
||||
location_table_uw_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_uw.items()}
|
||||
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
|
||||
|
||||
location_table_npc = {'Mushroom': 0x1000,
|
||||
'King Zora': 0x2,
|
||||
@@ -393,7 +483,7 @@ location_table_npc = {'Mushroom': 0x1000,
|
||||
'Stumpy': 0x8,
|
||||
'Bombos Tablet': 0x200}
|
||||
|
||||
location_table_npc_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_npc.items()}
|
||||
location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()}
|
||||
|
||||
location_table_ow = {'Flute Spot': 0x2a,
|
||||
'Sunken Treasure': 0x3b,
|
||||
@@ -408,14 +498,15 @@ location_table_ow = {'Flute Spot': 0x2a,
|
||||
'Bumper Cave Ledge': 0x4a,
|
||||
'Floating Island': 0x5}
|
||||
|
||||
location_table_ow_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_ow.items()}
|
||||
location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()}
|
||||
|
||||
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
||||
'Purple Chest': (0x3c9, 0x10),
|
||||
"Link's Uncle": (0x3c6, 0x1),
|
||||
'Hobo': (0x3c9, 0x1)}
|
||||
|
||||
location_table_misc_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_misc.items()}
|
||||
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
||||
|
||||
|
||||
class SNESState(enum.IntEnum):
|
||||
SNES_DISCONNECTED = 0
|
||||
@@ -431,16 +522,18 @@ def launch_sni(ctx: Context):
|
||||
sni_path = Utils.local_path(sni_path)
|
||||
if os.path.isdir(sni_path):
|
||||
for file in os.listdir(sni_path):
|
||||
if file.startswith("sni.") and not file.endswith(".proto"):
|
||||
lower_file = file.lower()
|
||||
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or lower_file == "sni":
|
||||
sni_path = os.path.join(sni_path, file)
|
||||
|
||||
if os.path.isfile(sni_path):
|
||||
snes_logger.info(f"Attempting to start {sni_path}")
|
||||
import subprocess
|
||||
if Utils.is_frozen(): # if it spawns a visible console, may as well populate it
|
||||
import sys
|
||||
if not sys.stdout: # if it spawns a visible console, may as well populate it
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path))
|
||||
else:
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
snes_logger.info(
|
||||
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
|
||||
@@ -490,13 +583,23 @@ async def get_snes_devices(ctx: Context):
|
||||
await socket.send(dumps(DeviceList_Request))
|
||||
reply = loads(await socket.recv())
|
||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||
|
||||
|
||||
await verify_snes_app(socket)
|
||||
await socket.close()
|
||||
return devices
|
||||
|
||||
|
||||
async def snes_connect(ctx: Context, address):
|
||||
async def verify_snes_app(socket):
|
||||
AppVersion_Request = {
|
||||
"Opcode": "AppVersion",
|
||||
}
|
||||
await socket.send(dumps(AppVersion_Request))
|
||||
|
||||
app: str = loads(await socket.recv())["Results"][0]
|
||||
if "SNI" not in app:
|
||||
snes_logger.warning(f"Warning: Did not find SNI as the endpoint, instead {app} was found.")
|
||||
|
||||
|
||||
async def snes_connect(ctx: Context, address, deviceIndex=-1):
|
||||
global SNES_RECONNECT_DELAY
|
||||
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
|
||||
if ctx.rom:
|
||||
@@ -505,6 +608,7 @@ async def snes_connect(ctx: Context, address):
|
||||
snes_logger.error('Already connected to SNI, likely awaiting a device.')
|
||||
return
|
||||
|
||||
device = None
|
||||
recv_task = None
|
||||
ctx.snes_state = SNESState.SNES_CONNECTING
|
||||
socket = await _snes_connect(ctx, address)
|
||||
@@ -513,15 +617,30 @@ async def snes_connect(ctx: Context, address):
|
||||
|
||||
try:
|
||||
devices = await get_snes_devices(ctx)
|
||||
numDevices = len(devices)
|
||||
|
||||
if len(devices) == 1:
|
||||
if numDevices == 1:
|
||||
device = devices[0]
|
||||
elif ctx.snes_reconnect_address:
|
||||
if ctx.snes_attached_device[1] in devices:
|
||||
device = ctx.snes_attached_device[1]
|
||||
else:
|
||||
device = devices[ctx.snes_attached_device[0]]
|
||||
else:
|
||||
elif numDevices > 1:
|
||||
if deviceIndex == -1:
|
||||
snes_logger.info(
|
||||
"Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
|
||||
|
||||
for idx, availableDevice in enumerate(devices):
|
||||
snes_logger.info(str(idx + 1) + ": " + availableDevice)
|
||||
|
||||
elif (deviceIndex < 0) or (deviceIndex - 1) > numDevices:
|
||||
snes_logger.warning("SNES device number out of range")
|
||||
|
||||
else:
|
||||
device = devices[deviceIndex - 1]
|
||||
|
||||
if device is None:
|
||||
await snes_disconnect(ctx)
|
||||
return
|
||||
|
||||
@@ -681,12 +800,6 @@ async def snes_flush_writes(ctx: Context):
|
||||
await snes_write(ctx, writes)
|
||||
|
||||
|
||||
# kept as function for easier wrapping by plugins
|
||||
def get_tags(ctx: Context):
|
||||
tags = ['AP']
|
||||
return tags
|
||||
|
||||
|
||||
async def track_locations(ctx: Context, roomid, roomdata):
|
||||
new_locations = []
|
||||
|
||||
@@ -694,7 +807,8 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
new_locations.append(location_id)
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_name_getter(location_id)
|
||||
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
|
||||
try:
|
||||
if roomid in location_shop_ids:
|
||||
@@ -764,7 +878,6 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
|
||||
new_check(location_id)
|
||||
|
||||
|
||||
if new_locations:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||
|
||||
@@ -781,11 +894,30 @@ async def game_watcher(ctx: Context):
|
||||
|
||||
if not ctx.rom:
|
||||
ctx.finished_game = False
|
||||
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
gameName = await snes_read(ctx, SM_ROMNAME_START, 2)
|
||||
if gameName is None:
|
||||
continue
|
||||
elif gameName == b"SM":
|
||||
ctx.game = GAME_SM
|
||||
else:
|
||||
ctx.game = GAME_ALTTP
|
||||
|
||||
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
|
||||
continue
|
||||
|
||||
ctx.rom = rom
|
||||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
|
||||
SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
if death_link:
|
||||
death_link = bool(death_link[0] & 0b1)
|
||||
old_tags = ctx.tags.copy()
|
||||
if death_link:
|
||||
ctx.tags.add("DeathLink")
|
||||
else:
|
||||
ctx.tags -= {"DeathLink"}
|
||||
if old_tags != ctx.tags and ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}])
|
||||
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
||||
ctx.locations_checked = set()
|
||||
ctx.locations_scouted = set()
|
||||
@@ -798,65 +930,131 @@ async def game_watcher(ctx: Context):
|
||||
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||
await ctx.disconnect()
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
if gamemode is None or gameend is None or game_timer is None or \
|
||||
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
||||
continue
|
||||
if ctx.game == GAME_ALTTP:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
delay = 7 if ctx.slow_mode else 2
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if time.perf_counter() - perf_counter < delay:
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
if gamemode is None or gameend is None or game_timer is None or \
|
||||
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
||||
continue
|
||||
|
||||
delay = 7 if ctx.slow_mode else 2
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if time.perf_counter() - perf_counter < delay:
|
||||
continue
|
||||
else:
|
||||
perf_counter = time.perf_counter()
|
||||
else:
|
||||
perf_counter = time.perf_counter()
|
||||
else:
|
||||
game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24)
|
||||
if abs(game_timer - prev_game_timer) < (delay * 60):
|
||||
game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24)
|
||||
if abs(game_timer - prev_game_timer) < (delay * 60):
|
||||
continue
|
||||
else:
|
||||
prev_game_timer = game_timer
|
||||
|
||||
if gamemode in ENDGAME_MODES: # triforce room and credits
|
||||
continue
|
||||
else:
|
||||
prev_game_timer = game_timer
|
||||
|
||||
if gamemode in ENDGAME_MODES: # triforce room and credits
|
||||
continue
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
continue
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2]
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
roomdata = data[6]
|
||||
scout_location = data[7]
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2]
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
roomdata = data[6]
|
||||
scout_location = data[7]
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR,
|
||||
bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
|
||||
bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
|
||||
bytes([ctx.locations_info[scout_location][0]]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location][1])]))
|
||||
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, bytes([ctx.locations_info[scout_location][0]]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, bytes([ctx.locations_info[scout_location][1]]))
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
elif ctx.game == GAME_SM:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in SM_DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
continue
|
||||
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2] | (data[3] << 8)
|
||||
|
||||
while (recv_index < recv_item):
|
||||
itemAdress = recv_index * 8
|
||||
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
|
||||
# worldId = message[0] | (message[1] << 8) # unused
|
||||
# itemId = message[2] | (message[3] << 8) # unused
|
||||
itemIndex = (message[4] | (message[5] << 8)) >> 3
|
||||
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.sm.Locations import locations_start_id
|
||||
location_id = locations_start_id + itemIndex
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_name_getter(location_id)
|
||||
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
|
||||
itemOutPtr = data[2] | (data[3] << 8)
|
||||
|
||||
from worlds.sm.Items import items_start_id
|
||||
if itemOutPtr < len(ctx.items_received):
|
||||
item = ctx.items_received[itemOutPtr]
|
||||
itemId = item.item - items_start_id
|
||||
|
||||
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes([playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF]))
|
||||
itemOutPtr += 1
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
|
||||
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), itemOutPtr, len(ctx.items_received)))
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_options()["lttp_options"].get("rom_start", True)
|
||||
@@ -867,44 +1065,49 @@ async def run_game(romfile):
|
||||
subprocess.Popen([auto_start, romfile],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def main():
|
||||
multiprocessing.freeze_support()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a Archipelago Binary Patch file')
|
||||
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
|
||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--founditems', default=False, action='store_true',
|
||||
help='Show items found by other players for themselves.')
|
||||
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
|
||||
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logging.info("Patch file was supplied. Creating sfc rom..")
|
||||
meta, romfile = Patch.create_rom_file(args.diff_file)
|
||||
args.connect = meta["server"]
|
||||
logging.info(f"Wrote rom file to {romfile}")
|
||||
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile)
|
||||
if adjusted:
|
||||
try:
|
||||
shutil.move(adjustedromfile, romfile)
|
||||
adjustedromfile = romfile
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||
if args.diff_file.endswith(".apsoe"):
|
||||
import webbrowser
|
||||
webbrowser.open("http://www.evermizer.com/apclient/")
|
||||
logging.info("Starting Evermizer Client in your Browser...")
|
||||
import time
|
||||
time.sleep(3)
|
||||
sys.exit()
|
||||
elif args.diff_file.endswith((".apbp", "apz3")):
|
||||
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
|
||||
if adjusted:
|
||||
try:
|
||||
shutil.move(adjustedromfile, romfile)
|
||||
adjustedromfile = romfile
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||
else:
|
||||
asyncio.create_task(run_game(romfile))
|
||||
|
||||
ctx = Context(args.snes, args.connect, args.password)
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if Utils.is_frozen() or "--nogui" not in sys.argv:
|
||||
if gui_enabled:
|
||||
input_task = None
|
||||
from kvui import LttPManager
|
||||
ctx.ui = LttPManager(ctx)
|
||||
from kvui import SNIManager
|
||||
ctx.ui = SNIManager(ctx)
|
||||
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
|
||||
else:
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
@@ -946,4 +1149,3 @@ if __name__ == '__main__':
|
||||
loop.run_until_complete(main())
|
||||
loop.close()
|
||||
colorama.deinit()
|
||||
atexit.unregister(exit_func)
|
||||
101
Utils.py
101
Utils.py
@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.6"
|
||||
__version__ = "0.2.0"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
@@ -24,6 +24,8 @@ import pickle
|
||||
import functools
|
||||
import io
|
||||
import collections
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from yaml import load, dump, safe_load
|
||||
|
||||
@@ -120,17 +122,25 @@ parse_yaml = safe_load
|
||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||
|
||||
|
||||
def get_cert_none_ssl_context():
|
||||
import ssl
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
return ctx
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_public_ipv4() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
import logging
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
|
||||
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip()
|
||||
except Exception as e:
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
|
||||
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
|
||||
except:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
@@ -141,10 +151,10 @@ def get_public_ipv4() -> str:
|
||||
def get_public_ipv6() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
import logging
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
|
||||
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||
@@ -161,6 +171,14 @@ def get_default_options() -> dict:
|
||||
"factorio_options": {
|
||||
"executable": "factorio\\bin\\x64\\factorio",
|
||||
},
|
||||
"sm_options": {
|
||||
"rom_file": "Super Metroid (JU).sfc",
|
||||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
},
|
||||
"soe_options": {
|
||||
"rom_file": "Secret of Evermore (USA).sfc",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
"sni": "SNI",
|
||||
@@ -180,6 +198,7 @@ def get_default_options() -> dict:
|
||||
"location_check_points": 1,
|
||||
"hint_cost": 10,
|
||||
"forfeit_mode": "goal",
|
||||
"collect_mode": "disabled",
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
@@ -196,6 +215,13 @@ def get_default_options() -> dict:
|
||||
"glitch_triforce_room": 1,
|
||||
"race": 0,
|
||||
"plando_options": "bosses",
|
||||
},
|
||||
"minecraft_options": {
|
||||
"forge_directory": "Minecraft Forge server",
|
||||
"max_heap_size": "2G"
|
||||
},
|
||||
"oot_options": {
|
||||
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +229,6 @@ def get_default_options() -> dict:
|
||||
|
||||
|
||||
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
||||
import logging
|
||||
for key, value in src.items():
|
||||
new_keys = keys.copy()
|
||||
new_keys.append(key)
|
||||
@@ -270,7 +295,6 @@ def persistent_load() -> typing.Dict[dict]:
|
||||
with open(path, "r") as f:
|
||||
storage = unsafe_parse_yaml(f.read())
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.debug(f"Could not read store: {e}")
|
||||
if storage is None:
|
||||
storage = {}
|
||||
@@ -278,7 +302,7 @@ def persistent_load() -> typing.Dict[dict]:
|
||||
return storage
|
||||
|
||||
|
||||
def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> typing.Tuple[str, bool]:
|
||||
if hasattr(get_adjuster_settings, "adjuster_settings"):
|
||||
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
|
||||
else:
|
||||
@@ -303,11 +327,12 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
if sprite_pool:
|
||||
printed_options["sprite_pool"] = sprite_pool
|
||||
|
||||
|
||||
if hasattr(get_adjuster_settings, "adjust_wanted"):
|
||||
adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted")
|
||||
elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request
|
||||
return romfile, False
|
||||
elif skip_questions:
|
||||
return romfile, False
|
||||
else:
|
||||
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
||||
f"{pprint.pformat(printed_options)}\n"
|
||||
@@ -328,7 +353,6 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
return romfile, False
|
||||
else:
|
||||
adjusted = False
|
||||
import logging
|
||||
if not hasattr(get_adjuster_settings, "adjust_wanted"):
|
||||
logging.info(f"Skipping post-patch adjustment")
|
||||
get_adjuster_settings.adjuster_settings = adjuster_settings
|
||||
@@ -356,16 +380,28 @@ safe_builtins = {
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||
self.options_module = importlib.import_module("Options")
|
||||
self.net_utils_module = importlib.import_module("NetUtils")
|
||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||
|
||||
def find_class(self, module, name):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by MultiServer -> savegame/multidata
|
||||
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 getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
||||
return getattr(self.generic_properties_module, name)
|
||||
if module.endswith("Options"):
|
||||
if module == "Options":
|
||||
mod = self.options_module
|
||||
else:
|
||||
mod = importlib.import_module(module)
|
||||
obj = getattr(mod, name)
|
||||
if issubclass(obj, self.options_module.Option):
|
||||
return obj
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
@@ -380,4 +416,33 @@ def restricted_loads(s):
|
||||
class KeyedDefaultDict(collections.defaultdict):
|
||||
def __missing__(self, key):
|
||||
self[key] = value = self.default_factory(key)
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def get_text_between(text: str, start: str, end: str) -> str:
|
||||
return text[text.index(start) + len(start): text.rindex(end)]
|
||||
|
||||
|
||||
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
|
||||
|
||||
|
||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
||||
log_format: str = "[%(name)s]: %(message)s"):
|
||||
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
||||
log_folder = local_path("logs")
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
handler.close()
|
||||
root_logger.setLevel(loglevel)
|
||||
file_handler = logging.FileHandler(
|
||||
os.path.join(log_folder, f"{name}.txt"),
|
||||
write_mode,
|
||||
encoding="utf-8-sig")
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
root_logger.addHandler(file_handler)
|
||||
if sys.stdout:
|
||||
root_logger.addHandler(
|
||||
logging.StreamHandler(sys.stdout)
|
||||
)
|
||||
|
||||
@@ -36,7 +36,11 @@ if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method('spawn')
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
update_sprites_lttp()
|
||||
try:
|
||||
update_sprites_lttp()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
app = get_app()
|
||||
create_options_files()
|
||||
if app.config["SELFLAUNCH"]:
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 flask_compress import Compress
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
from .models import *
|
||||
|
||||
@@ -27,7 +28,7 @@ app.config["SELFLAUNCH"] = True
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
|
||||
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
|
||||
@@ -81,32 +82,10 @@ def page_not_found(err):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
games_list = {
|
||||
"A Link to the Past": ("The Legend of Zelda: A Link to the Past",
|
||||
"""
|
||||
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
|
||||
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
|
||||
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
|
||||
Ganon!"""),
|
||||
"Factorio": ("Factorio",
|
||||
"""
|
||||
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
|
||||
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
|
||||
research new technologies, and become more efficient in your quest to build a rocket and return home.
|
||||
"""),
|
||||
"Minecraft": ("Minecraft",
|
||||
"""
|
||||
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
|
||||
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!"""),
|
||||
"Subnautica": ("Subnautica",
|
||||
"""
|
||||
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
|
||||
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
|
||||
You must find a cure for yourself, build an escape rocket, and leave the planet.
|
||||
"""),
|
||||
}
|
||||
# Start Playing Page
|
||||
@app.route('/start-playing')
|
||||
def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@@ -115,22 +94,20 @@ def player_settings(game):
|
||||
return render_template(f"player-settings.html", game=game)
|
||||
|
||||
|
||||
# Game sub-pages
|
||||
@app.route('/games/<string:game>/<string:page>')
|
||||
def game_pages(game, page):
|
||||
return render_template(f"/games/{game}/{page}.html")
|
||||
|
||||
|
||||
# Game landing pages
|
||||
@app.route('/games/<game>')
|
||||
def game_page(game):
|
||||
return render_template(f"/games/{game}/{game}.html")
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
def game_info(game, lang):
|
||||
return render_template('gameInfo.html', game=game, lang=lang)
|
||||
|
||||
|
||||
# List of supported games
|
||||
@app.route('/games')
|
||||
def games():
|
||||
return render_template("games/games.html", games_list=games_list)
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world.__doc__ if world.__doc__ else "No description provided."
|
||||
return render_template("supportedGames.html", worlds=worlds)
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
@@ -138,14 +115,14 @@ def tutorial(game, file, lang):
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang)
|
||||
|
||||
|
||||
@app.route('/tutorial')
|
||||
@app.route('/tutorial/')
|
||||
def tutorial_landing():
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def weighted_settings():
|
||||
return render_template("weightedSettings.html")
|
||||
@app.route('/faq/<string:lang>/')
|
||||
def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
@@ -178,7 +155,6 @@ def _read_log(path: str):
|
||||
|
||||
@app.route('/log/<suuid:room>')
|
||||
def display_log(room: UUID):
|
||||
# noinspection PyTypeChecker
|
||||
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||
|
||||
|
||||
@@ -198,15 +174,20 @@ def hostRoom(room: UUID):
|
||||
|
||||
return render_template("hostRoom.html", room=room)
|
||||
|
||||
|
||||
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
|
||||
def hostRoomRedirect(room: UUID):
|
||||
return redirect(url_for("hostRoom", room=room))
|
||||
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
@app.route('/discord')
|
||||
def discord():
|
||||
return redirect("https://discord.gg/archipelago")
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||
|
||||
@@ -16,7 +16,7 @@ def generate_api():
|
||||
try:
|
||||
options = {}
|
||||
race = False
|
||||
|
||||
meta_options_source = {}
|
||||
if 'file' in request.files:
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
@@ -24,14 +24,20 @@ def generate_api():
|
||||
return {"text": options}, 400
|
||||
if "race" in request.form:
|
||||
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
|
||||
meta_options_source = request.form
|
||||
|
||||
json_data = request.get_json()
|
||||
if json_data:
|
||||
meta_options_source = json_data
|
||||
if 'weights' in json_data:
|
||||
# example: options = {"player1weights" : {<weightsdata>}}
|
||||
options = json_data["weights"]
|
||||
if "race" in json_data:
|
||||
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
|
||||
|
||||
hint_cost = int(meta_options_source.get("hint_cost", 10))
|
||||
forfeit_mode = meta_options_source.get("forfeit_mode", "goal")
|
||||
|
||||
if not options:
|
||||
return {"text": "No options found. Expected file attachment or json weights."
|
||||
}, 400
|
||||
@@ -48,7 +54,7 @@ def generate_api():
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||
meta=json.dumps({"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}), state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
||||
|
||||
@@ -89,14 +89,14 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
options = restricted_loads(generation.options)
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
pool.apply_async(gen_game, (options,),
|
||||
{"race": meta["race"],
|
||||
{"meta": meta,
|
||||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
handle_generation_success, handle_generation_failure)
|
||||
except:
|
||||
except Exception as e:
|
||||
generation.state = STATE_ERROR
|
||||
commit()
|
||||
raise
|
||||
logging.exception(e)
|
||||
else:
|
||||
generation.state = STATE_STARTED
|
||||
|
||||
|
||||
@@ -49,9 +49,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
||||
for file in infolist:
|
||||
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(".yaml"):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
elif file.filename.endswith(".txt"):
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options = {file.filename: file.read()}
|
||||
@@ -73,7 +71,8 @@ def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[
|
||||
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
||||
else:
|
||||
try:
|
||||
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
|
||||
rolled_results[filename] = roll_settings(yaml_data,
|
||||
plando_options={"bosses", "items", "connections", "texts"})
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||
else:
|
||||
|
||||
@@ -11,7 +11,7 @@ import time
|
||||
import random
|
||||
import pickle
|
||||
|
||||
|
||||
import Utils
|
||||
from .models import *
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||
@@ -48,7 +48,7 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||
|
||||
class WebHostContext(Context):
|
||||
def __init__(self):
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", 0, 2)
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
self.tags = ["AP", "WebHost"]
|
||||
@@ -111,11 +111,7 @@ def run_server_process(room_id, ponyconfig: dict):
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s',
|
||||
level=logging.INFO,
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.join(LOGS_FOLDER, f"{room_id}.txt"), 'a', 'utf-8-sig')])
|
||||
Utils.init_logging(str(room_id), write_mode="a")
|
||||
ctx = WebHostContext()
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from flask import send_file, Response
|
||||
from flask import send_file, Response, render_template
|
||||
from pony.orm import select
|
||||
|
||||
from Patch import update_patch_data
|
||||
from WebHostLib import app, Slot, Room, Seed
|
||||
from Patch import update_patch_data, preferred_endings
|
||||
from WebHostLib import app, Slot, Room, Seed, cache
|
||||
import zipfile
|
||||
|
||||
|
||||
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
||||
def download_patch(room_id, patch_id):
|
||||
patch = Slot.get(id=patch_id)
|
||||
@@ -19,7 +20,8 @@ def download_patch(room_id, patch_id):
|
||||
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"
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||
f"{preferred_endings[patch.game]}"
|
||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
||||
|
||||
|
||||
@@ -28,23 +30,6 @@ def download_spoiler(seed_id):
|
||||
return Response(Seed.get(id=seed_id).spoiler, mimetype="text/plain")
|
||||
|
||||
|
||||
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
|
||||
def download_raw_patch(seed_id, player_id: int):
|
||||
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"
|
||||
else:
|
||||
import io
|
||||
|
||||
patch_data = update_patch_data(patch.data, server="")
|
||||
patch_data = io.BytesIO(patch_data)
|
||||
|
||||
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:room_id>/<int:player_id>")
|
||||
def download_slot_file(room_id, player_id: int):
|
||||
room = Room.get(id=room_id)
|
||||
@@ -66,6 +51,18 @@ def download_slot_file(room_id, player_id: int):
|
||||
for name in zf.namelist():
|
||||
if name.endswith("info.json"):
|
||||
fname = name.rsplit("/", 1)[0]+".zip"
|
||||
elif slot_data.game == "Ocarina of Time":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
||||
else:
|
||||
return "Game download not supported."
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
||||
|
||||
@app.route("/templates")
|
||||
@cache.cached()
|
||||
def list_yaml_templates():
|
||||
files = []
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
for world_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
files.append(world_name)
|
||||
return render_template("templates.html", files=files)
|
||||
@@ -2,19 +2,22 @@ import os
|
||||
import tempfile
|
||||
import random
|
||||
import json
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from typing import Dict, Optional as TypeOptional
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from Main import get_seed, seeddigits
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
from Generate import handle_name
|
||||
import pickle
|
||||
|
||||
from .models import *
|
||||
from WebHostLib import app
|
||||
from .check import get_yaml_data, roll_options
|
||||
from .upload import upload_zip_to_db
|
||||
|
||||
|
||||
@app.route('/generate', methods=['GET', 'POST'])
|
||||
@@ -31,6 +34,14 @@ def generate(race=False):
|
||||
flash(options)
|
||||
else:
|
||||
results, gen_options = roll_options(options)
|
||||
# get form data -> server settings
|
||||
hint_cost = int(request.form.get("hint_cost", 10))
|
||||
forfeit_mode = request.form.get("forfeit_mode", "goal")
|
||||
meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}
|
||||
if race:
|
||||
meta["item_cheat"] = False
|
||||
meta["remaining"] = False
|
||||
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
@@ -40,7 +51,8 @@ def generate(race=False):
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||
meta=json.dumps(meta),
|
||||
state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
@@ -48,18 +60,24 @@ def generate(race=False):
|
||||
else:
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
race=race, owner=session["_id"].int)
|
||||
meta=meta, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
|
||||
return redirect(url_for("viewSeed", seed=seed_id))
|
||||
|
||||
return render_template("generate.html", race=race)
|
||||
|
||||
|
||||
def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
|
||||
if not meta:
|
||||
meta: Dict[str, object] = {}
|
||||
|
||||
meta.setdefault("hint_cost", 10)
|
||||
race = meta.get("race", False)
|
||||
del (meta["race"])
|
||||
try:
|
||||
target = tempfile.TemporaryDirectory()
|
||||
playercount = len(gen_options)
|
||||
@@ -69,14 +87,13 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
if race:
|
||||
random.seed() # reset to time-based random source
|
||||
|
||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
|
||||
erargs = parse_arguments(['--multi', str(playercount)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.create_spoiler = not race
|
||||
erargs.spoiler = 0 if race else 2
|
||||
erargs.race = race
|
||||
erargs.skip_playthrough = race
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = target.name
|
||||
erargs.teams = 1
|
||||
@@ -85,15 +102,18 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
for k, v in settings.items():
|
||||
if v is not None:
|
||||
getattr(erargs, k)[player] = v
|
||||
if hasattr(erargs, k):
|
||||
getattr(erargs, k)[player] = v
|
||||
else:
|
||||
setattr(erargs, k, {player: v})
|
||||
|
||||
if not erargs.name[player]:
|
||||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
ERmain(erargs, seed)
|
||||
ERmain(erargs, seed, baked_server_options=meta)
|
||||
|
||||
return upload_to_db(target.name, owner, sid, race)
|
||||
return upload_to_db(target.name, sid, owner, race)
|
||||
except BaseException as e:
|
||||
if sid:
|
||||
with db_session:
|
||||
@@ -101,7 +121,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
if gen is not None:
|
||||
gen.state = STATE_ERROR
|
||||
meta = json.loads(gen.meta)
|
||||
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
|
||||
meta["error"] = (e.__class__.__name__ + ": " + str(e))
|
||||
gen.meta = json.dumps(meta)
|
||||
|
||||
commit()
|
||||
@@ -123,37 +143,19 @@ def wait_seed(seed: UUID):
|
||||
return render_template("waitSeed.html", seed_id=seed_id)
|
||||
|
||||
|
||||
def upload_to_db(folder, owner, sid, race:bool):
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
|
||||
multidata = None
|
||||
def upload_to_db(folder, sid, owner, race):
|
||||
for file in os.listdir(folder):
|
||||
file = os.path.join(folder, file)
|
||||
if file.endswith(".apbp"):
|
||||
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])
|
||||
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"):
|
||||
multidata = open(file, "rb").read()
|
||||
if multidata:
|
||||
with db_session:
|
||||
if sid:
|
||||
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, 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)
|
||||
if gen is not None:
|
||||
gen.delete()
|
||||
return seed.id
|
||||
else:
|
||||
raise Exception("Multidata required (.archipelago), but not found.")
|
||||
if file.endswith(".zip"):
|
||||
with db_session:
|
||||
with zipfile.ZipFile(file) as zfile:
|
||||
res = upload_zip_to_db(zfile, owner, {"race": race}, sid)
|
||||
if type(res) == "str":
|
||||
raise Exception(res)
|
||||
elif res:
|
||||
seed = res
|
||||
gen = Generation.get(id=seed.id)
|
||||
if gen is not None:
|
||||
gen.delete()
|
||||
return seed.id
|
||||
raise Exception("Generation zipfile not found.")
|
||||
|
||||
@@ -5,23 +5,43 @@ import yaml
|
||||
import json
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import Options
|
||||
|
||||
target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||
|
||||
|
||||
def create():
|
||||
def dictify_range(option):
|
||||
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
|
||||
option.default: 50}
|
||||
notes = {
|
||||
option.range_start: "minimum value",
|
||||
option.range_end: "maximum value"
|
||||
}
|
||||
return data, notes
|
||||
|
||||
def default_converter(default_value):
|
||||
if isinstance(default_value, (set, frozenset)):
|
||||
return list(default_value)
|
||||
return default_value
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
||||
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump
|
||||
options={**world.options, **Options.per_game_common_options},
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range, default_converter=default_converter,
|
||||
)
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
|
||||
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
||||
f.write(res)
|
||||
|
||||
# Generate JSON files for player-settings pages
|
||||
player_settings = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"game": game_name,
|
||||
"name": "Player",
|
||||
},
|
||||
}
|
||||
@@ -31,15 +51,15 @@ def create():
|
||||
if option.options:
|
||||
this_option = {
|
||||
"type": "select",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
}
|
||||
|
||||
for sub_option_name, sub_option_id in option.options.items():
|
||||
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||
this_option["options"].append({
|
||||
"name": sub_option_name,
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
|
||||
@@ -51,7 +71,7 @@ def create():
|
||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
|
||||
"min": option.range_start,
|
||||
@@ -60,5 +80,7 @@ def create():
|
||||
|
||||
player_settings["gameOptions"] = game_options
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
|
||||
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
flask>=2.0.1
|
||||
flask>=2.0.2
|
||||
pony>=0.7.14
|
||||
waitress>=2.0.0
|
||||
flask-caching>=1.10.1
|
||||
|
||||
53
WebHostLib/static/assets/faq.js
Normal file
53
WebHostLib/static/assets/faq.js
Normal file
@@ -0,0 +1,53 @@
|
||||
window.addEventListener('load', () => {
|
||||
const tutorialWrapper = document.getElementById('faq-wrapper');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, the tutorial is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the tutorial.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
|
||||
`faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
|
||||
for (let i=0; i < headers.length; i++){
|
||||
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
|
||||
headers[i].setAttribute('id', headerId);
|
||||
headers[i].addEventListener('click', () =>
|
||||
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
|
||||
}
|
||||
|
||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
||||
if (scrollTargetIndex > -1) {
|
||||
try{
|
||||
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
|
||||
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
52
WebHostLib/static/assets/faq/faq_en.md
Normal file
52
WebHostLib/static/assets/faq/faq_en.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## What is a randomizer?
|
||||
A randomizer is a modification of a video game which reorganizes the items required to progress through the game.
|
||||
A normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a
|
||||
randomized game, you might first find item C, then A, then B.
|
||||
|
||||
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
|
||||
play a randomized game. Putting items in non-standard locations can require the player to think about the game world
|
||||
and the items they encounter in new and interesting ways.
|
||||
|
||||
## What happens if an item is placed somewhere it is impossible to get?
|
||||
The randomizer has many strict sets of rules it must follow when generating a game. One of the functions of these
|
||||
rules is to ensure items necessary to complete the game will be accessible to the player. Many games also have a
|
||||
subset of rules allowing certain items to be placed in normally unreachable locations, provided the player has
|
||||
indicated they are comfortable exploiting certain glitches in the game.
|
||||
|
||||
## What is a multi-world?
|
||||
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example,
|
||||
in a two player multi-world, players A and B each get their own randomized version of a game, called seeds. In each
|
||||
player's game, they may find items which belong to the other player. If player A finds an item which belongs to
|
||||
player B, the item will be sent to player B's world over the internet.
|
||||
|
||||
This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete
|
||||
their game.
|
||||
|
||||
## What happens if a person has to leave early?
|
||||
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all
|
||||
the items in that game which belong to other players are sent out automatically, so other players can continue to
|
||||
play.
|
||||
|
||||
## What does multi-game mean?
|
||||
While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world
|
||||
allows players to randomize any of a number of supported games, and send items between them. This allows players of
|
||||
different games to interact with one another in a single multiplayer environment.
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
Yes. All our supported games can be generated as single-player experiences, and so long as you download the software,
|
||||
the website is not required to generate them.
|
||||
|
||||
## How do I get started?
|
||||
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others,
|
||||
please join our [Discord server](https://discord.gg/8Z65BR2). There are always people ready to answer any questions
|
||||
you might have.
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
The best way to get started is to take a look at our [code on GitHub](https://github.com/ArchipelagoMW/Archipelago).
|
||||
There, you will find examples of games in the [worlds](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds)
|
||||
folder, as well as some [documentation](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs) on our
|
||||
network interfaces.
|
||||
|
||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||
53
WebHostLib/static/assets/gameInfo.js
Normal file
53
WebHostLib/static/assets/gameInfo.js
Normal file
@@ -0,0 +1,53 @@
|
||||
window.addEventListener('load', () => {
|
||||
const gameInfo = document.getElementById('game-info');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, this game's info page is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the info page.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/gameInfo/` +
|
||||
`${gameInfo.getAttribute('data-lang')}_${gameInfo.getAttribute('data-game')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
|
||||
for (let i=0; i < headers.length; i++){
|
||||
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
|
||||
headers[i].setAttribute('id', headerId);
|
||||
headers[i].addEventListener('click', () =>
|
||||
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
|
||||
}
|
||||
|
||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
||||
if (scrollTargetIndex > -1) {
|
||||
try{
|
||||
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
|
||||
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
gameInfo.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
26
WebHostLib/static/assets/gameInfo/en_A Link to the Past.md
Normal file
26
WebHostLib/static/assets/gameInfo/en_A Link to the Past.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# A Link to the Past
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
|
||||
contain any of those items may have their contents changed.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in LttP?
|
||||
Items belonging to other worlds are represented by a Power Star from Super Mario World.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
|
||||
business!
|
||||
|
||||
29
WebHostLib/static/assets/gameInfo/en_Factorio.md
Normal file
29
WebHostLib/static/assets/gameInfo/en_Factorio.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Factorio
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
In Factorio, the research tree is shuffled, causing certain technologies to be obtained in a non-standard order.
|
||||
Recipe costs, technology requirements, and science pack requirements may also be shuffled at the player's discretion.
|
||||
|
||||
## What Factorio items can appear in other players' worlds?
|
||||
Factorio's technologies are removed from its tech tree and placed into other players' worlds. When those technologies
|
||||
are found, they are sent back to Factorio along with, optionally, free samples of those technologies.
|
||||
|
||||
## What is a free sample?
|
||||
A free sample is a single or stack of items in Factorio, granted by a technology received from another world. For
|
||||
example, receiving the technology
|
||||
`Portable Solar Panel` may also grant the player a stack of portable solar panels,
|
||||
and place them directly into the player's inventory.
|
||||
|
||||
## What does another world's item look like in Factorio?
|
||||
In Factorio, items which need to be sent to other worlds appear in the tech tree as new research items. They are
|
||||
represented by the Archipelago icon, and must be researched as if it were a normal technology. Upon successful
|
||||
completion of research, the item will be sent to its home world.
|
||||
|
||||
## When the engineer receives an item, what happens?
|
||||
When the player receives a technology, it is instantly learned and able to be crafted. A message will appear in the
|
||||
chat log to notify the player, and if free samples are enabled the player may also receive some items directly to
|
||||
their inventory.
|
||||
22
WebHostLib/static/assets/gameInfo/en_Minecraft.md
Normal file
22
WebHostLib/static/assets/gameInfo/en_Minecraft.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Minecraft
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which
|
||||
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as
|
||||
item checks, and occasionally when completing your own achievements.
|
||||
|
||||
## What is considered a location check in minecraft?
|
||||
Location checks in are completed when the player completes various Minecraft achievements. Opening the advancements
|
||||
menu in-game by pressing "L" will display outstanding achievements.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item in Minecraft, it either unlocks crafting recipes or puts items into the player's
|
||||
inventory directly.
|
||||
|
||||
## What is the victory condition?
|
||||
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
|
||||
sequence either by skipping it or watching hit play out.
|
||||
26
WebHostLib/static/assets/gameInfo/en_Ocarina of Time.md
Normal file
26
WebHostLib/static/assets/gameInfo/en_Ocarina of Time.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Ocarina of Time
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
|
||||
contain any of those items may have their contents changed. Gold Skultulla locations may also be included as necessary
|
||||
checks at the user's discretion.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in OoT?
|
||||
Items belonging to other worlds are represented by an Ocarina of Time.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
|
||||
business!
|
||||
36
WebHostLib/static/assets/gameInfo/en_Risk of Rain 2.md
Normal file
36
WebHostLib/static/assets/gameInfo/en_Risk of Rain 2.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Risk of Rain 2
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Risk of Rain is already a random game, by virtue of being a roguelite. The Archipelago mod implements pure multiworld
|
||||
functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
|
||||
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants
|
||||
by other players in other worlds.
|
||||
|
||||
## What Risk of Rain items can appear in other players' worlds?
|
||||
The Risk of Rain items are:
|
||||
* `Common Item` (White items)
|
||||
* `Uncommon Item` (Green items)
|
||||
* `Boss Item` (Yellow items)
|
||||
* `Legendary Item` (Red items)
|
||||
* `Lunar Item` (Blue items)
|
||||
* `Equipment` (Orange items)
|
||||
* `Dio's Best Friend` (Used if you set the YAML setting `total_revives_available` above `0`)
|
||||
|
||||
Each item grants you a random in-game item from the category it belongs to.
|
||||
|
||||
When an item is granted by another world to the Risk of Rain player (one of the items listed above) then a random
|
||||
in-game item of that tier will appear in the Risk of Rain player's inventory. If the item grant is an `Equipment`
|
||||
and the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground
|
||||
and _the new equipment_ will take it's place. (If you want the old one back, pick it up.)
|
||||
|
||||
## What does another world's item look like in Risk of Rain?
|
||||
When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for another
|
||||
player's world. The item in Risk of Rain will disappear in a poof of smoke and the grant will automatically go out to the multiworld.
|
||||
|
||||
## What is the item pickup step?
|
||||
The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_
|
||||
item that is spawned disappears (in a poof of smoke) and goes out to the multiworld.
|
||||
29
WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
Normal file
29
WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Secret of Evermore
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all options
|
||||
necessary to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which would normally be acquired throughout the game have been moved around! Progression logic remains,
|
||||
so the game is always able to be completed. However, because of the item shuffle, the player may need to access certain
|
||||
areas before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any
|
||||
weapon is obtained.
|
||||
|
||||
Additional help can be found in the [guide](https://github.com/black-sliver/evermizer/blob/feat-mw/guide.md).
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells
|
||||
and the dog can be randomized using yaml options.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed in another player's world.
|
||||
Specific items can be limited to your own world using plando.
|
||||
|
||||
## What does another world's item look like in Secret of Evermore?
|
||||
Secret of Evermore will display "Sent an Item". Check the client output if you want to know which.
|
||||
|
||||
## What happens when the player receives an item?
|
||||
When the player receives an item, a popup will appear to show which item was received. Items won't be recieved while a
|
||||
script is active such as when visiting Nobilia Market or during most Boss Fights. Once all scripts have ended, items
|
||||
will be recieved.
|
||||
27
WebHostLib/static/assets/gameInfo/en_Subnautica.md
Normal file
27
WebHostLib/static/assets/gameInfo/en_Subnautica.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Subnautica
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
The most noticeable change is the complete removal of freestanding technologies. The technology blueprints normally
|
||||
awarded from scanning those items have been shuffled into location checks throughout the AP item pool.
|
||||
|
||||
## What is the goal of Subnautica when randomized?
|
||||
The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, and escape into space.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
Most of the technologies the player will need throughout the game will be shuffled. Location checks in Subnautica are
|
||||
data pads and technology lockers.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Most technologies may be shuffled into another player's world.
|
||||
|
||||
## What does another world's item look like in Subnautica?
|
||||
Location checks in Subnautica are data pads and technology lockers. Opening one of these will send an item to
|
||||
another player's world.
|
||||
|
||||
## When the player receives a technology, what happens?
|
||||
When the player receives a technology, the chat log displays a notification the technology has been received.
|
||||
|
||||
25
WebHostLib/static/assets/gameInfo/en_Super Metroid.md
Normal file
25
WebHostLib/static/assets/gameInfo/en_Super Metroid.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Super Metroid
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All power-ups and ammunition can be shuffled, and all locations in the game which could contain any of those items
|
||||
may have their contents changed.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in Super Metroid?
|
||||
A unique item sprite has been added to the game to represent items belonging to another world.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, a text box will appear to show which item was received, and from whom.
|
||||
|
||||
28
WebHostLib/static/assets/gameInfo/en_Timespinner.md
Normal file
28
WebHostLib/static/assets/gameInfo/en_Timespinner.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Timespinner
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game. All rings and spells are also randomized into those item locations, therefor you can no longer craft them at the alchemist
|
||||
|
||||
## What is the goal of Timespinner when randomized?
|
||||
The goal remains unchanged. Kill the Sandman\Nightmare!
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All main inventory items, orbs, collectables, and familiers can be shuffled, and all locations in the game which could
|
||||
contain any of those items may have their contents changed.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in Timespinner?
|
||||
Items belonging to other worlds are represented by the vanilla item [Elemental Beads](https://timespinnerwiki.com/Use_Items), Elemental Beads have no use in the randomizer
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, the same items popup will be displayed as when you would normally obtain the item
|
||||
|
||||
2
WebHostLib/static/assets/md5.min.js
vendored
Normal file
2
WebHostLib/static/assets/md5.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Copyright © 2011 Sebastian Tschan, https://blueimp.net
|
||||
!function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<<t%32,n[14+(t+64>>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h<n.length;h+=16)c=l(r=c,e=f,o=i,u=a,n[h],7,-680876936),a=l(a,c,f,i,n[h+1],12,-389564586),i=l(i,a,c,f,n[h+2],17,606105819),f=l(f,i,a,c,n[h+3],22,-1044525330),c=l(c,f,i,a,n[h+4],7,-176418897),a=l(a,c,f,i,n[h+5],12,1200080426),i=l(i,a,c,f,n[h+6],17,-1473231341),f=l(f,i,a,c,n[h+7],22,-45705983),c=l(c,f,i,a,n[h+8],7,1770035416),a=l(a,c,f,i,n[h+9],12,-1958414417),i=l(i,a,c,f,n[h+10],17,-42063),f=l(f,i,a,c,n[h+11],22,-1990404162),c=l(c,f,i,a,n[h+12],7,1804603682),a=l(a,c,f,i,n[h+13],12,-40341101),i=l(i,a,c,f,n[h+14],17,-1502002290),c=v(c,f=l(f,i,a,c,n[h+15],22,1236535329),i,a,n[h+1],5,-165796510),a=v(a,c,f,i,n[h+6],9,-1069501632),i=v(i,a,c,f,n[h+11],14,643717713),f=v(f,i,a,c,n[h],20,-373897302),c=v(c,f,i,a,n[h+5],5,-701558691),a=v(a,c,f,i,n[h+10],9,38016083),i=v(i,a,c,f,n[h+15],14,-660478335),f=v(f,i,a,c,n[h+4],20,-405537848),c=v(c,f,i,a,n[h+9],5,568446438),a=v(a,c,f,i,n[h+14],9,-1019803690),i=v(i,a,c,f,n[h+3],14,-187363961),f=v(f,i,a,c,n[h+8],20,1163531501),c=v(c,f,i,a,n[h+13],5,-1444681467),a=v(a,c,f,i,n[h+2],9,-51403784),i=v(i,a,c,f,n[h+7],14,1735328473),c=g(c,f=v(f,i,a,c,n[h+12],20,-1926607734),i,a,n[h+5],4,-378558),a=g(a,c,f,i,n[h+8],11,-2022574463),i=g(i,a,c,f,n[h+11],16,1839030562),f=g(f,i,a,c,n[h+14],23,-35309556),c=g(c,f,i,a,n[h+1],4,-1530992060),a=g(a,c,f,i,n[h+4],11,1272893353),i=g(i,a,c,f,n[h+7],16,-155497632),f=g(f,i,a,c,n[h+10],23,-1094730640),c=g(c,f,i,a,n[h+13],4,681279174),a=g(a,c,f,i,n[h],11,-358537222),i=g(i,a,c,f,n[h+3],16,-722521979),f=g(f,i,a,c,n[h+6],23,76029189),c=g(c,f,i,a,n[h+9],4,-640364487),a=g(a,c,f,i,n[h+12],11,-421815835),i=g(i,a,c,f,n[h+15],16,530742520),c=m(c,f=g(f,i,a,c,n[h+2],23,-995338651),i,a,n[h],6,-198630844),a=m(a,c,f,i,n[h+7],10,1126891415),i=m(i,a,c,f,n[h+14],15,-1416354905),f=m(f,i,a,c,n[h+5],21,-57434055),c=m(c,f,i,a,n[h+12],6,1700485571),a=m(a,c,f,i,n[h+3],10,-1894986606),i=m(i,a,c,f,n[h+10],15,-1051523),f=m(f,i,a,c,n[h+1],21,-2054922799),c=m(c,f,i,a,n[h+8],6,1873313359),a=m(a,c,f,i,n[h+15],10,-30611744),i=m(i,a,c,f,n[h+6],15,-1560198380),f=m(f,i,a,c,n[h+13],21,1309151649),c=m(c,f,i,a,n[h+4],6,-145523070),a=m(a,c,f,i,n[h+11],10,-1120210379),i=m(i,a,c,f,n[h+2],15,718787259),f=m(f,i,a,c,n[h+9],21,-343485551),c=d(c,r),f=d(f,e),i=d(i,o),a=d(a,u);return[c,f,i,a]}function a(n){for(var t="",r=32*n.length,e=0;e<r;e+=8)t+=String.fromCharCode(n[e>>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e<t.length;e+=1)t[e]=0;for(var r=8*n.length,e=0;e<r;e+=8)t[e>>5]|=(255&n.charCodeAt(e/8))<<e%32;return t}function e(n){for(var t,r="0123456789abcdef",e="",o=0;o<n.length;o+=1)t=n.charCodeAt(o),e+=r.charAt(t>>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16<o.length&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}(r(n),r(t))}function t(n,t,r){return t?r?u(t,n):e(u(t,n)):r?o(n):e(o(n))}"function"==typeof define&&define.amd?define(function(){return t}):"object"==typeof module&&module.exports?module.exports=t:n.md5=t}(this);
|
||||
52
WebHostLib/static/assets/ootTracker.js
Normal file
52
WebHostLib/static/assets/ootTracker.js
Normal file
@@ -0,0 +1,52 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters, small keys, and boss keys in the location-table
|
||||
const types = ['counter', 'smallkeys', 'bosskeys'];
|
||||
for (let j = 0; j < types.length; j++) {
|
||||
let counters = document.getElementsByClassName(types[j]);
|
||||
const fakeCounters = fakeDOM.getElementsByClassName(types[j]);
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -4,9 +4,25 @@ window.addEventListener('load', () => {
|
||||
gameName = document.getElementById('player-settings').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerHTML = gameName;
|
||||
document.getElementById('game-name').innerText = gameName;
|
||||
|
||||
Promise.all([fetchSettingData()]).then((results) => {
|
||||
let settingHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!settingHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
localStorage.setItem(`${gameName}-hash`, md5(results[0]));
|
||||
localStorage.removeItem(gameName);
|
||||
settingHash = md5(results[0]);
|
||||
}
|
||||
|
||||
if (settingHash !== md5(results[0])) {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.";
|
||||
userMessage.style.display = "block";
|
||||
userMessage.addEventListener('click', resetSettings);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultSettings(results[0]);
|
||||
buildUI(results[0]);
|
||||
@@ -14,7 +30,7 @@ window.addEventListener('load', () => {
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true))
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
@@ -28,6 +44,12 @@ window.addEventListener('load', () => {
|
||||
})
|
||||
});
|
||||
|
||||
const resetSettings = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`)
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
@@ -39,7 +61,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true);
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
@@ -83,7 +105,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', setting);
|
||||
label.setAttribute('data-tooltip', settings[setting].description);
|
||||
label.innerText = `${settings[setting].friendlyName}:`;
|
||||
label.innerText = `${settings[setting].displayName}:`;
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
|
||||
49
WebHostLib/static/assets/timespinnerTracker.js
Normal file
49
WebHostLib/static/assets/timespinnerTracker.js
Normal file
@@ -0,0 +1,49 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters in the location-table
|
||||
let counters = document.getElementsByClassName('counter');
|
||||
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -20,6 +20,9 @@ window.addEventListener('load', () => {
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
|
||||
32
WebHostLib/static/assets/tutorial/Subnautica/setup_en.md
Normal file
32
WebHostLib/static/assets/tutorial/Subnautica/setup_en.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Subnautica Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Subnautica](https://store.steampowered.com/app/264710/Subnautica/)
|
||||
- [QModManager4](https://www.nexusmods.com/subnautica/mods/201)
|
||||
- [Archipelago Mod for Subnautica](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc/releases)
|
||||
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Install QModManager4 as per its instructions.
|
||||
|
||||
2. The folder you installed QModManager4 into will now have a /QMods directory. It might appear after a start of Subnautica. You can also create this folder yourself.
|
||||
|
||||
3. Unpack the Archipelago Mod into this folder, so that Subnautica/QMods/Archipelago/ is a valid path.
|
||||
|
||||
4. Start Subnautica. You should see a Connect Menu in the topleft of your main Menu.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you don't see the connect window check that you see a qmodmanager_log-Subnautica.txt in Subnautica, if not QModManager4 is not correctly installed, otherwise open it and look for `[Info : BepInEx] Loading [Archipelago 1.0.0.0]`, version number doesn't matter. If it doesn't show this, then QModManager4 didn't find the Archipelago mod, so check your paths.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. In Host, enter the address of the server, such as archipelago.gg:38281, your server host should be able to tell you this.
|
||||
|
||||
2. In Password enter the server password if one exists, otherwise leave blank.
|
||||
|
||||
3. In PlayerName enter your "name" field from the yaml, or website config.
|
||||
|
||||
4. Hit Connect. If it says succesfully authenticated you can now create a new savegame or resume the correct savegame.
|
||||
180
WebHostLib/static/assets/tutorial/archipelago/plando_en.md
Normal file
180
WebHostLib/static/assets/tutorial/archipelago/plando_en.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Archipelago Plando Guide
|
||||
|
||||
## What is Plando?
|
||||
The purposes of randomizers is to randomize the items in a game to give a new experience.
|
||||
Plando takes this concept and changes it up by allowing you to plan out certain aspects of the game by placing certain
|
||||
items in certain locations, certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region
|
||||
connections. Each of these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
|
||||
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
|
||||
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss plando.
|
||||
|
||||
### Enabling Plando
|
||||
On the website plando will already be enabled. If you will be generating the game locally plando features must be enabled (opt-in).
|
||||
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
|
||||
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such as
|
||||
`plando_options: bosses, items, texts, connections`.
|
||||
|
||||
## Item Plando
|
||||
Item plando allows a player to place an item in a specific location or specific locations, place multiple items into
|
||||
a list of specific locations both in their own game or in another player's game. **Note that there's a very good chance that
|
||||
cross-game plando could very well be broken i.e. placing on of your items in someone else's world playing a different game.**
|
||||
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, and either item and location, or items and locations.
|
||||
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or false
|
||||
and defaults to true if omitted.
|
||||
* `world` is the target world to place the item in.
|
||||
* It gets ignored if only one world is generated.
|
||||
* Can be a number, name, true, false, or null. False is the default.
|
||||
* If a number is used it targets that slot or player number in the multiworld.
|
||||
* If a name is used it will target the world with that player name.
|
||||
* If set to true it will be any player's world besides your own.
|
||||
* If set to false it will target your own world.
|
||||
* If set to null it will target a random world in the multiworld.
|
||||
* `force` determines whether the generator will fail if the item can't be placed in the location can be true, false,
|
||||
or silent. Silent is the default.
|
||||
* If set to true the item must be placed and the generator will throw an error if it is unable to do so.
|
||||
* If set to false the generator will log a warning if the placement can't be done but will still generate.
|
||||
* If set to silent and the placement fails it will be ignored entirely.
|
||||
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and if
|
||||
omitted will default to 100.
|
||||
* Single Placement is when you use a plando block to place a single item at a single location.
|
||||
* `item` is the item you would like to place and `location` is the location to place it.
|
||||
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
|
||||
* `items` defines the items to use and a number letting you place multiple of it.
|
||||
* `locations` is a list of possible locations those items can be placed in.
|
||||
* Using the multi placement method, placements are picked randomly.
|
||||
|
||||
### Available Items
|
||||
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
|
||||
* [Factorio Non-Progressive](https://wiki.factorio.com/Technologies) Note that these use the *internal names*. For example, `advanced-electronics`
|
||||
* [Factorio Progressive](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/factorio/Technologies.py#L374)
|
||||
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Items.py#L14)
|
||||
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py#L61)
|
||||
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Items.py#L8)
|
||||
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Items.py#L13)
|
||||
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/items.json)
|
||||
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Items.py#L11)
|
||||
|
||||
### Available Locations
|
||||
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L429)
|
||||
* [Factorio](https://wiki.factorio.com/Technologies) Same as items
|
||||
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Locations.py#L18)
|
||||
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LocationList.py#L38)
|
||||
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Locations.py#L17) This is a special
|
||||
case. The locations are "ItemPickup[number]" up to the maximum set in the yaml.
|
||||
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Locations.py)
|
||||
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/locations.json)
|
||||
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Locations.py#L13)
|
||||
|
||||
|
||||
A list of all available items and locations can also be found in the [server's datapackage](/api/datapackage).
|
||||
### Examples
|
||||
```yaml
|
||||
plando_items:
|
||||
# example block 1 - Timespinner
|
||||
- item:
|
||||
Empire Orb: 1
|
||||
Radiant Orb: 1
|
||||
location: Starter Chest 1
|
||||
from_pool: true
|
||||
world: true
|
||||
percentage: 50
|
||||
|
||||
# example block 2 - Ocarina of Time
|
||||
- items:
|
||||
Kokiri Sword: 1
|
||||
Biggoron Sword: 1
|
||||
Bow: 1
|
||||
Magic Meter: 1
|
||||
Progressive Strength Upgrade: 3
|
||||
Progressive Hookshot: 2
|
||||
locations:
|
||||
- Deku Tree Slingshot Chest
|
||||
- Dodongos Cavern Bomb Bag Chest
|
||||
- Jabu Jabus Belly Boomerang Chest
|
||||
- Bottom of the Well Lens of Truth Chest
|
||||
- Forest Temple Bow Chest
|
||||
- Fire Temple Megaton Hammer Chest
|
||||
- Water Temple Longshot Chest
|
||||
- Shadow Temple Hover Boots Chest
|
||||
- Spirit Temple Silver Gauntlets Chest
|
||||
world: false
|
||||
|
||||
# example block 3 - Slay the Spire
|
||||
- items:
|
||||
Boss Relic: 3
|
||||
locations:
|
||||
Boss Relic 1
|
||||
Boss Relic 2
|
||||
Boss Relic 3
|
||||
|
||||
# example block 4 - Factorio
|
||||
- items:
|
||||
progressive-electric-energy-distribution: 2
|
||||
electric-energy-accumulators: 1
|
||||
progressive-turret: 2
|
||||
locations:
|
||||
military
|
||||
gun-turret
|
||||
logistic-science-pack
|
||||
steel-processing
|
||||
percentage: 80
|
||||
force: true
|
||||
```
|
||||
1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's
|
||||
Starter Chest 1 and removes the chosen item from the item pool.
|
||||
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
|
||||
in their own dungeon major item chests.
|
||||
3. This block will always trigger and will lock boss relics on the bosses.
|
||||
4. This block has an 80% chance of occuring and when it does will place all but 1 of the items randomly among the four
|
||||
locations chosen here.
|
||||
|
||||
## Boss Plando
|
||||
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
|
||||
[relevant guide](/tutorial/zelda3/plando/en)
|
||||
|
||||
## Text Plando
|
||||
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
|
||||
[relevant guide](/tutorial/zelda3/plando/en)
|
||||
|
||||
## Connections Plando
|
||||
This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with
|
||||
their connections is different I will only explain the basics here while more specifics for Link to the Past connection
|
||||
plando can be found in its plando guide.
|
||||
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support subweights.
|
||||
* `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100.
|
||||
* Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance shuffle.
|
||||
* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate.
|
||||
|
||||
[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
|
||||
|
||||
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62)
|
||||
|
||||
### Examples
|
||||
```yaml
|
||||
plando_connections:
|
||||
# example block 1 - Link to the Past
|
||||
- entrance: Cave Shop (Lake Hylia)
|
||||
exit: Cave 45
|
||||
direction: entrance
|
||||
- entrance: Cave 45
|
||||
exit: Cave Shop (Lake Hylia)
|
||||
direction: entrance
|
||||
- entrance: Agahnims Tower
|
||||
exit: Old Man Cave Exit (West)
|
||||
direction: exit
|
||||
|
||||
# example block 2 - Minecraft
|
||||
- entrance: Overworld Structure 1
|
||||
exit: Nether Fortress
|
||||
direction: both
|
||||
- entrance: Overworld Structure 2
|
||||
exit: Village
|
||||
direction: both
|
||||
```
|
||||
|
||||
1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and
|
||||
when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to the
|
||||
lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take you to
|
||||
their locations as normal but leaving old man cave will exit at Agahnim's Tower.
|
||||
2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the Minecraft
|
||||
connection plando to work structure shuffle must be enabled.
|
||||
67
WebHostLib/static/assets/tutorial/archipelago/setup_en.md
Normal file
67
WebHostLib/static/assets/tutorial/archipelago/setup_en.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Archipelago Setup Guide
|
||||
|
||||
## Installing the Archipelago software
|
||||
The most recent public release of Archipelago can be found [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
Run the exe file, and after accepting the license agreement you will be prompted on which components you would like to install.
|
||||
The generator allows you to generate multiworld games on your computer. The ROM setups are optional but are required if
|
||||
anyone in the game that you generate wants to play any of those games as they are needed to generate the relevant patch
|
||||
files. The server will allow you to host the multiworld on your machine but this also requires you to port forward. The
|
||||
default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of other guides on the internet
|
||||
that will be more suited to your hardware. The `Clients` are what you use to connect your game to the multiworld. If the
|
||||
game/games you plan to play are available here go ahead and install these as well. If the game you choose to play is
|
||||
supported by Archipelago but not listed in the installation check the relevant tutorial.
|
||||
|
||||
## Generating a game
|
||||
### Gather all player YAMLS
|
||||
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they wish to play with.
|
||||
A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file.
|
||||
Each player can go to the game's player settings page in order to determine the settings how they want them and then download a YAML file containing these settings.
|
||||
After getting the YAML files of each participant for your multiworld game, these can all either be placed together in the
|
||||
`Archipelago\Players` folder or compressed into a ZIP folder to then be uploaded to the [website generator](/generate).
|
||||
If rolling locally ensure that the folder is clear of any files you do not wish to include in the game such as the
|
||||
included default player settings files.
|
||||
|
||||
#### Changing local host settings for generation
|
||||
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
|
||||
auto-forfeit, plando support, or setting a password. All of these settings plus other options are able to be changed by
|
||||
modifying the `host.yaml` file in the base `Archipelago` folder. The settings chosen here are baked into
|
||||
the serverdata file that gets output with the other files after generation so if rolling locally ensure this file is edited
|
||||
to your liking *before* rolling the seed.
|
||||
|
||||
### Rolling the seed
|
||||
|
||||
#### On the Website
|
||||
After gathering the YAML files together in one location, select all of the files and compress them into a .zip folder.
|
||||
Next go to the [Start Playing](/start-playing) page and click on `generate a randomized game` to reach the website generator.
|
||||
Here, you can adjust some server settings such as forfeit rules and the cost for a player to use a hint before generation.
|
||||
After adjusting the host settings to your liking click on the Upload File button and using the explorer window that opens,
|
||||
navigate to the location where you zipped the player files and upload this zip. The page will generate your game and refresh
|
||||
multiple times to check on completion status. After the generation completes you will be on a Seed Info page that provides
|
||||
the seed, the date/time of creation, a link to the spoiler log, if available, and links to any rooms created from this seed.
|
||||
To begin playing, click on `Create New Room`, which will take you to the room page. From here you can navigate back to thse
|
||||
Seed Info page or to the Tracker page. Sharing the link to this page with your friends will provide them with the
|
||||
necessary info and files for them to connect to the multiworld.
|
||||
|
||||
#### Rolling using the generation program
|
||||
After gathering the YAML files together in the `Archipelago\Players` folder, run the program `ArchipelagoGenerate.exe`
|
||||
in the base `Archipelago` folder. This will then open a console window and either silently close itself or spit out an
|
||||
error. If you receive an error, it is likely due to an error in the YAML file. If the error is unhelpful in figuring
|
||||
out the issue asking in the ***#tech-support*** channel of our Discord for help with finding it is highly recommended.
|
||||
The generator will put a zip folder into your `Archipelago\output` folder with the format `AP_XXXXXXXXX`.zip.
|
||||
This contains the patch files and relevant mods for the players as well as the serverdata for the host.
|
||||
|
||||
## Hosting a multiworld
|
||||
### Uploading the seed to the website
|
||||
The easiest and most recommended method is to generate the game on the website which will allow you to create a private
|
||||
room with all the necessary files you can share, as well as hosting the game and supporting item trackers for various games.
|
||||
If for some reason the seed was rolled on a machine, then either the resulting zip file or the resulting `AP_XXXXX.archipelago`
|
||||
inside the zip file can be uploaded to the [upload page](/uploads). This will give a page with the seed info and have a
|
||||
link to the spoiler if it exists. Click on Create New room and then share the link for the room with the other players
|
||||
so that they can download their patches or mods. The room will also have a link to a Multiworld Tracker and tell you
|
||||
what the players need to connect to from their clients.
|
||||
|
||||
### Hosting a seed locally
|
||||
For this we'll assume you have already port forwarding `38281` and have generated a seed that is still in the `outputs`
|
||||
folder. Next, you'll want to run `ArchipelagoServer.exe`. A window will open in order to open the multiworld data for the
|
||||
game. You can either use the generated zip folder or extract the .archipelago file and use it. If everything worked correctly the console window should tell you it's now hosting a game with the IP, port, and password that clients will need in order to connect.
|
||||
Extract the patch and mod files then send those to your friends, and you're done!
|
||||
69
WebHostLib/static/assets/tutorial/archipelago/triggers_en.md
Normal file
69
WebHostLib/static/assets/tutorial/archipelago/triggers_en.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Archipelago Triggers Guide
|
||||
|
||||
## What are triggers?
|
||||
Triggers allow you to customize your game settings by allowing you to define certain options or even a variety of
|
||||
settings to occur or "trigger" under certain conditions. These are essentially "if, then statements" for options in your game.
|
||||
A good example of what you can do with triggers is the custom
|
||||
[mercenary mode](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml)
|
||||
that was created using entirely triggers and plando. For more information on plando you can reference
|
||||
[this guide](/tutorial/archipelago/plando/en) or [this guide](/tutorial/zelda3/plando/en).
|
||||
|
||||
## Trigger use
|
||||
Triggers have to be defined in the root of the yaml file meaning it must be outside of a game section.
|
||||
The best place to do this is the bottom of the yaml.
|
||||
- Triggers comprise of the trigger section and then each trigger must have an `option_category`, `option_name`, and
|
||||
`option_result` from which it will react to and then an `options` section where the definition of what will happen.
|
||||
- `option_category` is the defining section from which the option is defined in.
|
||||
- Example: `A Link to the Past`
|
||||
- This is the root category the option is located in. If the option you're triggering off of is in root then you
|
||||
would use `null`, otherwise this is the game for which you want this option trigger to activate.
|
||||
- `option_name` is the option setting from which the triggered choice is going to react to.
|
||||
- Example: `shop_item_slots`
|
||||
- This can be any option from any category defined in the yaml file in either root or a game section except for `game`.
|
||||
- `option_result` is the result of this option setting from which you would like to react.
|
||||
- Example: `15`
|
||||
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple
|
||||
results you would need multiple triggers for this.
|
||||
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring
|
||||
another option also gets selected or placing an item in a certain location.
|
||||
- Example:
|
||||
```yaml
|
||||
A Link to the Past:
|
||||
start_inventory:
|
||||
Rupees (300): 2
|
||||
```
|
||||
This format must be:
|
||||
|
||||
```yaml
|
||||
root option:
|
||||
option to change:
|
||||
desired result
|
||||
```
|
||||
|
||||
### Examples
|
||||
The above examples all together will end up looking like this:
|
||||
```yaml
|
||||
triggers:
|
||||
- option_category: A Link to the Past
|
||||
option_name: shop_item_slots
|
||||
option_result: 15
|
||||
options:
|
||||
A Link to the Past:
|
||||
start_inventory:
|
||||
Rupees(300): 2
|
||||
```
|
||||
|
||||
For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600 rupees at the beginning.
|
||||
These can also be used to change other options.
|
||||
|
||||
For example:
|
||||
```yaml
|
||||
triggers:
|
||||
- option_category: Timespinner
|
||||
option_name: SpecificKeycards
|
||||
option_result: true
|
||||
options:
|
||||
Timespinner:
|
||||
Inverted: true
|
||||
```
|
||||
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
WebHostLib/static/assets/tutorial/factorio/factorio-download.png
Normal file
BIN
WebHostLib/static/assets/tutorial/factorio/factorio-download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 627 KiB |
@@ -1,52 +1,125 @@
|
||||
# Factorio Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
##### Players
|
||||
- [Factorio](https://factorio.com) - Needed by Players and Hosts
|
||||
|
||||
### Server Host
|
||||
- [Factorio](https://factorio.com)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
##### Server Hosts
|
||||
- [Factorio](https://factorio.com) - Needed by Players and Hosts
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) - Needed by Hosts
|
||||
|
||||
### Players
|
||||
- [Factorio](https://factorio.com)
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
## General Concept
|
||||
### What is a config file and why do I need one?
|
||||
Your config 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 config 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.
|
||||
|
||||
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
|
||||
### Where do I get a config file?
|
||||
The [Player Settings](/games/Factorio/player-settings) page on the website allows you to configure
|
||||
your personal settings and export a config file from them.
|
||||
|
||||
### Verifying your config file
|
||||
If you would like to validate your config file to make sure it works, you may do so on
|
||||
the [YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Connecting to Someone Else's Factorio Game
|
||||
Connecting to someone else's game is the simplest way to play Factorio with Archipelago. It allows multiple
|
||||
people to play in a single world, all contributing to the completion of the seed.
|
||||
|
||||
1. Acquire the Archipelago mod for this seed. It should be named `AP_*.zip`, where `*` is the seed number.
|
||||
2. Copy the mod file into your Factorio `mods` folder, which by default is located at:
|
||||
`C:\Users\YourName\AppData\Roaming\Factorio\mods`
|
||||
3. Get the server address from the person hosting the game you are joining
|
||||
4. Launch Factorio
|
||||
5. Click on "Multiplayer" in the main menu
|
||||
6. Click on "Connect to address"
|
||||
7. Enter the address into this box
|
||||
8. Click "Connect"
|
||||
|
||||
## Prepare to Host Your Own Factorio Game
|
||||
|
||||
### Defining Some Terms
|
||||
In Archipelago, multiple Factorio worlds may be played simultaneously. Each of these worlds must be hosted by a
|
||||
Factorio server, which is connected to the Archipelago Server via middleware.
|
||||
|
||||
This guide uses the following terms to refer to the software:
|
||||
- **Factorio Client** - The Factorio instance which will be used to play the game.
|
||||
- **Factorio Server** - The Factorio instance which will be used to host the Factorio world. Any number of
|
||||
Factorio Clients may connect to this server.
|
||||
- **Archipelago Client** - The middleware software used to connect the Factorio Server to the Archipelago Server.
|
||||
- **Archipelago Server** - The central Archipelago server, which connects all games to each other.
|
||||
|
||||
### What a Playable State Looks Like
|
||||
- An Archipelago Server
|
||||
- The generated Factorio Mod, created as a result of running `ArchipelagoGenerate.exe`
|
||||
- One running instance of `ArchipelagoFactorioClient.exe` (the Archipelago Client) per Factorio world
|
||||
- A running modded Factorio Server, which should have been started by the Archipelago Client automatically
|
||||
- A running modded Factorio Client
|
||||
|
||||
### Dedicated Server Setup
|
||||
You need a dedicated isolated Factorio installation that the FactorioClient can take control over. If you intend to both host a world and play on the same device, you will need two separate Factorio installations; one for the FactorioClient to hook into and control, and one for you to play on.
|
||||
The easiest and cheapest way to do so is to either buy or register a Factorio key on factorio.com, which allows you to download as many Factorio games as you want. If you own a steam copy already you can link your account on the website.
|
||||
1. Download the latest Factorio from https://factorio.com/download for your system, for Windows the recommendation is "win64-manual".
|
||||
To play Factorio with Archipelago, a dedicated server setup is required. This dedicated Factorio Server must be
|
||||
installed separately from your main Factorio Client installation. The recommended way to install two instances
|
||||
of Factorio on your computer is to download the Factorio installer file directly from
|
||||
[factorio.com](https://factorio.com/download).
|
||||
|
||||
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. In this same folder if there are shortcuts named "mods" and "saves" delete these and replace with folders with the same names.
|
||||
4. Navigate to where you installed ArchipelagoFactorioClient and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your hosting Factorio.exe. If you put Factorio into your Archipelago folder, this would already match.<br>
|
||||
ex.
|
||||
#### If you purchased Factorio on Steam, GOG, etc.
|
||||
You can register your copy of Factorio on [factorio.com](https://factorio.com/). You will be required to
|
||||
create an account, if you have not done so already. As part of that process, you will be able to enter your
|
||||
Factorio product code. This will allow you to download the game directly from their website.
|
||||
|
||||
#### Download the Standalone Version
|
||||
It is recommended to download the standalone version of Factorio for use as a dedicated server. Doing so prevents
|
||||
any potential conflicts with your currently-installed version of Factorio. Download the file by clicking on the
|
||||
button appropriate to your operating system, and extract the folder to a convenient location (we recommend
|
||||
C:\Factorio or similar).<br />
|
||||
<img src="/static/assets/tutorial/factorio/factorio-download.png" />
|
||||
|
||||
Next, you should launch your Factorio Server by running `factorio.exe`, which is located at: `bin/x64/factorio.exe`.
|
||||
You will be asked to log-in to your Factorio account using the same credentials you used on Factorio's website.
|
||||
After you have logged in, you may close the game.
|
||||
|
||||
#### Configure your Archipelago Installation
|
||||
You must modify your `host.yaml` file inside your Archipelago installation directory so that it points to your
|
||||
standalone Factorio executable. Here is an example of the appropriate setup, note the double `\\` are required:
|
||||
```yaml
|
||||
factorio_options:
|
||||
executable: C:\\Program Files\\factorio\\bin\\x64\\factorio"
|
||||
executable: C:\\factorio\\bin\\x64\\factorio"
|
||||
```
|
||||
### Player Setup
|
||||
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer. If you're connecting to a FactorioClient on the same system you will connect to localhost
|
||||
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
With all that complete, you are now able to...
|
||||
|
||||
1. Install the generated Factorio AP Mod (would be in /Mods after step 2 of Setup)
|
||||
## Host Your Own Factorio Game
|
||||
|
||||
2. Run FactorioClient, it should launch a Factorio server, which you can control with /factorio <original factorio commands>,
|
||||
1. Obtain the Factorio mod for this Archipelago seed. It should be named `AP_*.zip`, where `*` is the seed number.
|
||||
2. Install the mod into your Factorio Server by copying the zip file into the `mods` folder.
|
||||
3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
|
||||
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`.
|
||||
4. Obtain the Archipelago Server address from the website's host room, or from the server host.
|
||||
5. Run your Archipelago Client, which is named `ArchilepagoFactorioClient.exe`. This was installed along with
|
||||
Archipelago if you chose to include it during the installation process.
|
||||
6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter"
|
||||
<br /><img src="/static/assets/tutorial/factorio/connect-to-ap-server.png" />
|
||||
7. Launch your Factorio Client
|
||||
8. Click on "Multiplayer" in the main menu
|
||||
9. Click on "Connect to address"
|
||||
10. Enter `localhost` into the server address box
|
||||
11. Click "Connect"
|
||||
|
||||
* It should start up, create a world and become ready for Factorio connections.
|
||||
3. In FactorioClient, do /connect <Archipelago Server Address> to join that multiworld. You can find further commands with /help as well as !help once connected.
|
||||
## Allowing Other People to Join Your Game
|
||||
1. Ensure your Archipelago Client is running.
|
||||
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
|
||||
3. Obtain your IP address by visiting [this website](https://whatismyip.com/).
|
||||
4. Provide your IP address to anyone you want to join your game, and have them follow the steps for
|
||||
"Connecting to Someone Else's Factorio Game" above.
|
||||
|
||||
* / commands are run on your local client, ! commands are requests for the AP server
|
||||
|
||||
* Players should be able to connect to your Factorio Server and begin playing.
|
||||
|
||||
4. You can join yourself by connecting to address localhost, other people will need to connect to your IP and you may need to port forward for the Factorio Server for those connections.
|
||||
## Troubleshooting
|
||||
In case any problems should occur, the Archipelago Client will create a file `FactorioClient.txt` in the `/logs`.
|
||||
The contents of this file may help you troubleshoot an issue on your own and is vital for requesting help from other
|
||||
people in Archipelago.
|
||||
|
||||
## Additional Resources
|
||||
- [Alternate Tutorial by Umenen](https://docs.google.com/document/d/1yZPAaXB-QcetD8FJsmsFrenAHO5V6Y2ctMAyIoT9jS4)
|
||||
- [Factorio Speedrun Guide](https://www.youtube.com/watch?v=ExLrmK1c7tA)
|
||||
- [Factorio Wiki](https://wiki.factorio.com/)
|
||||
|
||||
@@ -1,52 +1,12 @@
|
||||
# Guia instalación de Minecraft Randomizer
|
||||
|
||||
#Instalacion automatica para el huesped de partida
|
||||
- descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and activa el modulo `Minecraft Client`
|
||||
|
||||
## 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?
|
||||
@@ -58,42 +18,71 @@ 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
|
||||
description: Basic Minecraft Yaml
|
||||
# Tu nombre en el juego. Espacios seran sustituidos por guinoes bajos y
|
||||
# hay un limite de 16 caracteres
|
||||
name: TuNombre
|
||||
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
|
||||
```
|
||||
|
||||
# Opciones compartidas por todos los juegos:
|
||||
accessibility: locations
|
||||
progression_balancing: on
|
||||
# Opciones Especficicas para Minecraft
|
||||
|
||||
Minecraft:
|
||||
# Numero de logros requeridos (87 max) para que aparezca el Ender Dragon y completar el juego.
|
||||
advancement_goal: 50
|
||||
|
||||
# Numero de trozos de huevo de dragon a obtener (30 max) antes de que el Ender Dragon aparezca.
|
||||
egg_shards_required: 10
|
||||
|
||||
# Numero de huevos disponibles en la partida (30 max).
|
||||
egg_shards_available: 15
|
||||
|
||||
# Modifica el nivel de objetos logicamente requeridos para
|
||||
# explorar areas peligrosas y luchar contra jefes.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
|
||||
# Si off, los logros que dependan de suerte o sean tediosos tendran objetos de apoyo, no necesarios para completar el juego.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Si off, los logros muy dificiles tendran objetos de apoyo, no necesarios para completar el juego.
|
||||
# Solo afecta a How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Algunos logros requieren derrotar al Ender Dragon;
|
||||
# Si esto se queda en off, dichos logros no tendran objetos necesarios.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Permite el mezclado de villas, puesto, fortalezas, bastiones y ciudades de END.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Añade brujulas de estructura al juego,
|
||||
# apuntaran a la estructura correspondiente mas cercana.
|
||||
structure_compasses:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Reemplaza un porcentaje de objetos innecesarios por trampas abeja
|
||||
# las cuales crearan multiples abejas agresivas alrededor de los jugadores cuando se reciba.
|
||||
bee_traps:
|
||||
0: 1
|
||||
25: 0
|
||||
50: 0
|
||||
75: 0
|
||||
100: 0
|
||||
```
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
@@ -104,18 +93,39 @@ Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAM
|
||||
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
|
||||
|
||||
Haz doble click en tu fichero `.apmc` para que se arranque el cliente de minecraft y el servidor forge se ejecute.
|
||||
|
||||
### 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>)`
|
||||
Una vez en juego introduce `/connect <AP-Address> (Port) (<Password>)` donde `<AP-Address>` es la dirección del servidor. `(Port)` solo es requerido si el servidor Archipelago no esta usando el puerto por defecto 38281.
|
||||
`(<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.
|
||||
|
||||
## Procedimiento de instalación manual
|
||||
Solo es requerido si quieres usar una instalacion de forge por ti mismo, recomendamos usar el instalador de Archipelago
|
||||
###Software Requerido
|
||||
- [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)
|
||||
**NO INSTALES ESTO EN TU CLIENTE MINECRAFT**
|
||||
|
||||
|
||||
### 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.5 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
|
||||
|
||||
98
WebHostLib/static/assets/tutorial/ror2/setup_en.md
Normal file
98
WebHostLib/static/assets/tutorial/ror2/setup_en.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Risk of Rain 2 Setup Guide
|
||||
|
||||
## Install using r2modman
|
||||
### Install r2modman
|
||||
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
|
||||
|
||||
[https://thunderstore.io/package/ebkr/r2modman/](https://thunderstore.io/package/ebkr/r2modman/)
|
||||
|
||||
### Install Archipelago Mod using r2modman
|
||||
You can install the Archipelago mod using r2modman in one of two ways.
|
||||
|
||||
[https://thunderstore.io/package/ArchipelagoMW/Archipelago/](https://thunderstore.io/package/ArchipelagoMW/Archipelago/)
|
||||
|
||||
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
|
||||
|
||||
You can also search for the "Archipelago" mod in the r2modman interface.
|
||||
The mod manager should automatically install all necessary dependencies as well.
|
||||
|
||||
### Running the Modded Game
|
||||
Click on the "Start modded" button in the top left in r2modman to start the game with the
|
||||
Archipelago mod installed.
|
||||
|
||||
## Joining an Archipelago Session
|
||||
There will be a menu button on the right side of the screen in the character select menu.
|
||||
Click it in order to bring up the in lobby mod config.
|
||||
From here you can expand the Archipelago sections and fill in the relevant info.
|
||||
Keep password blank if there is no password on the server.
|
||||
|
||||
Simply check `Enable Archipelago?` and when you start the run it will automatically connect.
|
||||
|
||||
## Gameplay
|
||||
The Risk of Rain 2 players send checks by causing items to spawn in-game. That means opening chests or killing bosses, generally.
|
||||
An item check is only sent out after a certain number of items are picked up. This count is configurable in the player's YAML.
|
||||
|
||||
## YAML Settings
|
||||
An example YAML would look like this:
|
||||
```yaml
|
||||
description: Ijwu-ror2
|
||||
name: Ijwu
|
||||
|
||||
game:
|
||||
Risk of Rain 2: 1
|
||||
|
||||
Risk of Rain 2:
|
||||
total_locations: 15
|
||||
total_revivals: 4
|
||||
start_with_revive: true
|
||||
item_pickup_step: 1
|
||||
enable_lunar: true
|
||||
item_weights:
|
||||
default: 50
|
||||
new: 0
|
||||
uncommon: 0
|
||||
legendary: 0
|
||||
lunartic: 0
|
||||
chaos: 0
|
||||
no_scraps: 0
|
||||
even: 0
|
||||
scraps_only: 0
|
||||
item_pool_presets: true
|
||||
# custom item weights
|
||||
green_scrap: 16
|
||||
red_scrap: 4
|
||||
yellow_scrap: 1
|
||||
white_scrap: 32
|
||||
common_item: 64
|
||||
uncommon_item: 32
|
||||
legendary_item: 8
|
||||
boss_item: 4
|
||||
lunar_item: 16
|
||||
equipment: 32
|
||||
```
|
||||
|
||||
| Name | Description | Allowed values |
|
||||
| ---- | ----------- | -------------- |
|
||||
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 100 |
|
||||
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
|
||||
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
|
||||
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
|
||||
| enable_lunar | Allows for lunar items to be shuffled into the item pool on behalf of the Risk of Rain player. | true/false |
|
||||
| item_weights | Each option here is a preset item weight that can be used to customize your generate item pool with certain settings. | default, new, uncommon, legendary, lunartic, chaos, no_scraps, even, scraps_only |
|
||||
| item_pool_presets | A simple toggle to determine whether the item_weight presets are used or the custom item pool as defined below | true/false |
|
||||
| custom item weights | Each defined item here is a single item in the pool that will have a weight against the other items when the item pool gets generated. These values can be modified to adjust how frequently certain items appear | 0-100|
|
||||
|
||||
|
||||
Using the example YAML above: the Risk of Rain 2 player will have 15 total items which they can pick up for other players. (total_locations = 15)
|
||||
|
||||
They will have 15 items waiting for them in the item pool which will be distributed out to the multiworld. (total_locations = 15)
|
||||
|
||||
They will complete a location check every second item. (item_pickup_step = 1)
|
||||
|
||||
They will have 4 of the items which other players can grant them replaced with `Dio's Best Friend`. (total_revivals = 4)
|
||||
|
||||
The player will also start with a `Dio's Best Friend`. (start_with_revive = true)
|
||||
|
||||
The player will have lunar items shuffled into the item pool on their behalf. (enable_lunar = true)
|
||||
|
||||
The player will have the default preset generated item pool with the custom item weights being ignored. (item_weights: default and item_pool_presets: true)
|
||||
@@ -0,0 +1,116 @@
|
||||
# Secret of Evermore Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (included in Archipelago if already installed)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI with ROM access
|
||||
- [snes9x-rr win32.zip](https://github.com/gocha/snes9x-rr/releases) +
|
||||
[socket.dll](http://www.nyo.fr/~skarsnik/socket.dll) +
|
||||
[connector.lua](https://raw.githubusercontent.com/alttpo/sni/main/lua/Connector.lua)
|
||||
- or [BizHawk](http://tasvideos.org/BizHawk.html)
|
||||
- or [bsnes-plus-nwa](https://github.com/black-sliver/bsnes-plus)
|
||||
- Or SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
|
||||
- Your Secret of Evermore US ROM file, probably named `Secret of Evermore (USA).sfc`
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
Your config 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 config 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 config file?
|
||||
The [Player Settings](/games/Secret%20of%20Evermore/player-settings) page on the website allows you to configure your
|
||||
personal settings and export a config file from them.
|
||||
|
||||
### Verifying your config file
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
Stand-alone "Evermizer" has a way of balancing single-player games, but may not always be on par feature-wise.
|
||||
Head over to [evermizer.com](https://evermizer.com) if you want to try the official stand-alone, otherwise read below.
|
||||
|
||||
1. Navigate to the [Player Settings](/games/Secret%20of%20Evermore/player-settings) page, configure your options, and
|
||||
click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Run your patch file through [apbpatch](https://evermizer.com/apbpatch) and load it in your emulator or console.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
|
||||
everyone's patch files. Your patch file should have a `.apsoe` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, open [apbpatch](https://evermizer.com/apbpatch) and
|
||||
generate your ROM from it. Load the ROM file in your emulator or console.
|
||||
|
||||
### Connect to SNI
|
||||
|
||||
#### With an emulator
|
||||
Start SNI either from the Archipelago install folder or the stand-alone version.
|
||||
If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x-rr
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the `Connector.lua` file you downloaded above
|
||||
6. If the script window complains about missing `socket.dll` make sure the DLL is in snes9x or the lua file's directory.
|
||||
|
||||
##### BizHawk
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
||||
these menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
5. Select the `Connector.lua` file you downloaded above
|
||||
|
||||
##### bsnes-plus-nwa
|
||||
This should automatically connect to SNI.
|
||||
If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
#### With hardware
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
||||
done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware
|
||||
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
|
||||
[on this page](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
1. Copy the ROM file to your SD card.
|
||||
2. Load the ROM file from the menu.
|
||||
|
||||
### Open the client
|
||||
Open [ap-soeclient](http://evermizer.com/apclient) in a modern browser. Do not switch tabs, open it in a new window
|
||||
if you want to use the browser while playing. Do not minimize the window with the client.
|
||||
|
||||
The client should automatically connect to SNI, the "SNES" status should change to green.
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
Enter `/connect server:port` in the client's command prompt and press enter. You'll find `server:port` on the same page
|
||||
that had the patch file.
|
||||
|
||||
### Play the game
|
||||
When the game is loaded but not yet past the intro cutscene, the "Game" status is yellow. When the client shows "AP" as
|
||||
green and "Game" as yellow, you're ready to play. The intro can be skipped pressing the START button and "Game" should
|
||||
change to green. Congratulations on successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
|
||||
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the website linked above.
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
|
||||
so they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
126
WebHostLib/static/assets/tutorial/super-metroid/multiworld_en.md
Normal file
126
WebHostLib/static/assets/tutorial/super-metroid/multiworld_en.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Super Metroid Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [Super Metroid Client](https://github.com/ArchipelagoMW/SuperMetroidClient/releases)
|
||||
- **sniConnector.lua** (located on the client download page)
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in the Super Metroid Client)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
|
||||
- Your Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc`
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Windows Setup
|
||||
1. Download and install the Super Metroid Client from the link above, making sure to install the most recent version.
|
||||
**The file is located in the assets section at the bottom of the version information**.
|
||||
2. During setup, you will be asked to locate your base ROM file. This is your Super Metroid ROM file.
|
||||
3. If you are using an emulator, you should assign your Lua capable emulator as your default program
|
||||
for launching ROM files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
3. Check the box next to **Always use this app to open .sfc files**
|
||||
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside
|
||||
the folder you extracted in step one.
|
||||
|
||||
### Macintosh Setup
|
||||
- We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help.
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
Your config 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 config 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 config file?
|
||||
The [Player Settings](/games/Super%20Metroid/player-settings) page on the website allows you to configure your
|
||||
personal settings and export a config file from them.
|
||||
|
||||
### Verifying your config file
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
1. Navigate to the [Player Settings](/games/Super%20Metroid/player-settings) page, configure your options, and click
|
||||
the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from
|
||||
the patch file, and open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
|
||||
everyone's patch files. Your patch file should have a `.apm3` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically
|
||||
launch the client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
When the client launched automatically, SNI should have also automatically launched in the background.
|
||||
If this is its first time launching, you may be prompted to allow it to communicate through the Windows
|
||||
Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the `sniConnector.lua` file you downloaded above
|
||||
|
||||
##### BizHawk
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
||||
these menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
5. Select the `sniConnector.lua` file you downloaded above
|
||||
|
||||
#### With hardware
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
||||
done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware
|
||||
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
|
||||
[on this page](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Power on your device and load the ROM.
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
The patch file which launched your client should have automatically connected you to the AP Server.
|
||||
There are a few reasons this may not happen however, including if the game is hosted on the website but
|
||||
was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host
|
||||
for the address of the server, and copy/paste it into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server
|
||||
Status: Connected".
|
||||
|
||||
### Play the game
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations
|
||||
on successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
|
||||
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the website linked above.
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
|
||||
so they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
60
WebHostLib/static/assets/tutorial/timespinner/setup_en.md
Normal file
60
WebHostLib/static/assets/tutorial/timespinner/setup_en.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Timespinner Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner)
|
||||
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
|
||||
## General Concept
|
||||
|
||||
The timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe instead of Timespinner.exe to start the game in randomized mode, for more info see the [readme](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run TsRandomizer.exe
|
||||
2. Select "New Game"
|
||||
3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard
|
||||
4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails
|
||||
* NOTE: the input fields support Ctrl + V pasting of values
|
||||
5. Select "Connect"
|
||||
6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty
|
||||
|
||||
## YAML Settings
|
||||
An example YAML would look like this:
|
||||
```yaml
|
||||
description: Default Timespinner Template
|
||||
name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
game:
|
||||
Timespinner: 1
|
||||
requires:
|
||||
version: 0.1.8
|
||||
Timespinner:
|
||||
StartWithJewelryBox: # Start with Jewelry Box unlocked
|
||||
false: 50
|
||||
true: 0
|
||||
DownloadableItems: # With the tablet you will be able to download items at terminals
|
||||
false: 50
|
||||
true: 50
|
||||
FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors
|
||||
false: 50
|
||||
true: 0
|
||||
StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer
|
||||
false: 50
|
||||
true: 50
|
||||
QuickSeed: # Start with Talaria Attachment, Nyoom!
|
||||
false: 50
|
||||
true: 0
|
||||
SpecificKeycards: # Keycards can only open corresponding doors
|
||||
false: 0
|
||||
true: 50
|
||||
Inverted: # Start in the past
|
||||
false: 50
|
||||
true: 50
|
||||
```
|
||||
* All Options are either enabled or not, if values are specified for both true & false the generator will select one based on weight
|
||||
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
|
||||
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds
|
||||
@@ -1,4 +1,51 @@
|
||||
[
|
||||
{
|
||||
"gameTitle": "Archipelago",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Tutorial",
|
||||
"description": "A Guide to setting up the Archipelago software to generate multiworld games on your computer.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "archipelago/setup_en.md",
|
||||
"link": "archipelago/setup/en",
|
||||
"authors": [
|
||||
"alwaysintreble"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Archipelago Triggers Guide",
|
||||
"description": "A Guide to setting up and using triggers in your game settings.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "archipelago/triggers_en.md",
|
||||
"link": "archipelago/triggers/en",
|
||||
"authors": [
|
||||
"alwaysintreble"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Archipelago Plando Guide",
|
||||
"description": "A guide to understanding and using plando for your game.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "archipelago/plando_en.md",
|
||||
"link": "archipelago/plando/en",
|
||||
"authors": [
|
||||
"alwaysintreble"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "The Legend of Zelda: A Link to the Past",
|
||||
"tutorials": [
|
||||
@@ -86,6 +133,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "The Legend of Zelda: Ocarina of Time",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Tutorial",
|
||||
"description": "A guide to setting up the Archipelago Ocarina of Time software on your computer.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "zelda5/setup_en.md",
|
||||
"link": "zelda5/setup/en",
|
||||
"authors": [
|
||||
"Edos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "Spanish",
|
||||
"filename": "zelda5/setup_es.md",
|
||||
"link": "zelda5/setup/es",
|
||||
"authors": [
|
||||
"Edos"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Factorio",
|
||||
"tutorials": [
|
||||
@@ -98,7 +172,8 @@
|
||||
"filename": "factorio/setup_en.md",
|
||||
"link": "factorio/setup/en",
|
||||
"authors": [
|
||||
"Berserker"
|
||||
"Berserker",
|
||||
"Farrak Kilhn"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -139,5 +214,100 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Risk of Rain 2",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Risk of Rain 2 integration for Archipelago multiworld games.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "ror2/setup_en.md",
|
||||
"link": "ror2/setup/en",
|
||||
"authors": [
|
||||
"Ijwu"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Timespinner",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Timespinner randomizer connected to an Archipelago Multiworld",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "timespinner/setup_en.md",
|
||||
"link": "timespinner/setup/en",
|
||||
"authors": [
|
||||
"Jarno"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Subnautica",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "Subnautica/setup_en.md",
|
||||
"link": "Subnautica/setup/en",
|
||||
"authors": [
|
||||
"Berserker"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Super Metroid",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Super Metroid Client on your computer. This guide covers single-player, multiworld, and related software.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "super-metroid/multiworld_en.md",
|
||||
"link": "super-metroid/multiworld/en",
|
||||
"authors": [
|
||||
"Farrak Kilhn"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Secret of Evermore",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "secret-of-evermore/multiworld_en.md",
|
||||
"link": "secret-of-evermore/multiworld/en",
|
||||
"authors": [
|
||||
"Black Sliver"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Benötigte Software
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
|
||||
- Ein Emulator, der lua-scripts abspielen kann
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Integriert in Archipelago)
|
||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien fähig zu einer Internetverbindung
|
||||
- Ein Emulator, der mit SNI verbinden kann
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- Ein SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), oder andere kompatible Hardware
|
||||
@@ -42,7 +42,7 @@ jeder Spieler sein Spiel nach seinem eigenen Geschmack gestalten, während ander
|
||||
Einstellungen wählen können!
|
||||
|
||||
### Wo bekomme ich so eine YAML-Datei her?
|
||||
Die [Player Settings](/player-settings) Seite auf der Website ermöglicht das einfache Erstellen und Herunterladen
|
||||
Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen und Herunterladen
|
||||
deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
|
||||
|
||||
### Deine YAML-Datei ist gewichtet!
|
||||
@@ -74,12 +74,12 @@ bei der [YAML Validator](/mysterycheck) Seite tun.
|
||||
### Erhalte deine Patch-Datei und erstelle dein ROM
|
||||
Wenn du an einem MultiWorld-Spiel teilnehmen möchtest, wirst du in der Regel vom Host nach deiner YAML-Datei gefragt.
|
||||
Sobald du diese weitergegeben hast, wird der Host einen Link bereitstellen, wo du deinen Patch oder eine .zip-Datei
|
||||
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.bmbp`.
|
||||
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.apbp`.
|
||||
|
||||
### Mit dem Client verbinden
|
||||
|
||||
#### Via Emulator
|
||||
Wenn der client den Emulator automatisch gestartet hat, wird QUsb2Snes ebenfalls im Hintergrund gestartet.
|
||||
Wenn der client den Emulator automatisch gestartet hat, wird SNI ebenfalls im Hintergrund gestartet.
|
||||
Wenn dies das erste Mal ist, wird möglicherweise ein Fenster angezeigt, wo man bestätigen muss, dass das Programm
|
||||
durch die Windows Firewall kommunizieren darf.
|
||||
|
||||
@@ -88,8 +88,9 @@ durch die Windows Firewall kommunizieren darf.
|
||||
2. Klicke auf den Reiter "File" oben im Menü und wähle **Lua Scripting**
|
||||
3. Klicke auf **New Lua Script Window...**
|
||||
4. Im sich neu öffnenden Fenster, klicke auf **Browse...**
|
||||
5. Navigiere zum Ort, wo du snes9x Multitroid installiert hast, öffne den `lua`-Ordner und öffne `multibridge.lua`
|
||||
6. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
|
||||
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
|
||||
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
|
||||
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
|
||||
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke).
|
||||
|
||||
##### BizHawk
|
||||
@@ -99,9 +100,8 @@ durch die Windows Firewall kommunizieren darf.
|
||||
2. Lade die entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde.
|
||||
3. Klicke auf das Tools-Menü und klicke auf **Lua Console**
|
||||
4. Klicke auf den Button um ein neues Lua-Script zu öffnen.
|
||||
5. Navigiere zum Verzeichnis, wo du die Multiworld Utilities installiert hast und dort in folgende Ordner:
|
||||
`QUsb2Snes/Qusb2Snes/LuaBridge`
|
||||
6. Wähle dort die `luabridge.lua` und klicke auf Öffnen.
|
||||
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
|
||||
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
|
||||
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
|
||||
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke)
|
||||
|
||||
@@ -111,15 +111,11 @@ das noch nicht getan hast, so tue dies am besten jetzt! SD2SNES und FXPak Pro Nu
|
||||
[hier](https://github.com/RedGuyyyy/sd2snes/releases). Nutzer ähnlicher Hardware finden Hilfestellung
|
||||
[auf dieser Seite](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
**UM MIT HARDWARE ZU VERBINDEN WIRD AKTUELL EINE ALTE VERSION VON QUSB2SNES BENÖTIGT
|
||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
||||
Neuere Versionen funktionieren möglicherweise nur eingeschränkt, fehlerhaft oder gar nicht!
|
||||
|
||||
1. Schließe deinen Emulator, falls er automatisch gestartet haben sollte.
|
||||
2. Schließe QUsb2Snes, welches automatisch mit dem Client gestartet wurde (in der Taskleiste zu finden).
|
||||
3. Starte die richtige version von QUsb2Snes (v0.7.16).
|
||||
4. Starte deine (Original-)Konsole und lade die ROM-Datei.
|
||||
5. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
|
||||
2. Start SNI
|
||||
3. Starte deine (Original-)Konsole und lade die ROM-Datei.
|
||||
4. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
|
||||
zeigen sollte.
|
||||
|
||||
### Mit dem MultiServer verbinden
|
||||
@@ -137,7 +133,7 @@ können du und deine Freunde loslegen! Glückwunsch zum erfolgreichen Beitritt z
|
||||
|
||||
## Ein Multiworld-Spiel hosten
|
||||
Die Empfohlene Art, ein Spiel zu hosten, ist, den Service auf
|
||||
[der website](https://berserkermulti.world/generate) zu nutzen. Das Ganze ist recht einfach:
|
||||
[der website](/generate) zu nutzen. Das Ganze ist recht einfach:
|
||||
|
||||
1. Lasse dir von deinen Mitspielern die YAML-Datei zuschicken.
|
||||
2. Erstelle einen Zip-komprimierten Ordner´, in den du alle YAML-Dateien deiner Spieler einfügst.
|
||||
|
||||
@@ -1,98 +1,71 @@
|
||||
# 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/mJKEHaiyR_Y" frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
## Required Software
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the SNIClient included with
|
||||
[Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- If installing Archipelago, make sure to check the box for SNIClient -> A Link to the Past Patch Setup during install, or SNI will not be included
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and SNIClient)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of running Lua scripts
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
|
||||
- Your Japanese v1.0 ROM file, probably named `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Windows Setup
|
||||
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
|
||||
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal
|
||||
multiworld games, you want `Setup.Archipelago.exe`
|
||||
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
|
||||
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
|
||||
installed this software before and are simply upgrading now, you will not be prompted to locate your
|
||||
ROM file a second time.
|
||||
- You may also be prompted to install Microsoft Visual C++. If you already have this software on your computer
|
||||
(possibly because a Steam game installed it already), the installer will not prompt you to install it again.
|
||||
1. Download and install your preferred client from the link above, making sure to install the most recent version.
|
||||
**The installer file is located in the assets section at the bottom of the version information**.
|
||||
- During setup, you will be asked to locate your base ROM file. This is your Japanese Link to the Past ROM file.
|
||||
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program
|
||||
for launching ROM files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right click on a ROM file and select **Open with...**
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
3. Check the box next to **Always use this app to open .sfc files**
|
||||
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside
|
||||
the folder you extracted in step one.
|
||||
|
||||
### Macintosh Setup
|
||||
- We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help.
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
## 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
|
||||
### What is a config file and why do I need one?
|
||||
Your config 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 config 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?
|
||||
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
|
||||
export a YAML file from them.
|
||||
### Where do I get a config file?
|
||||
The [Player Settings](/games/A%20Link%20to%20the%20Past/player-settings) page on the website allows you to configure
|
||||
your personal settings and export a config file from them.
|
||||
|
||||
### Advanced YAML configuration
|
||||
A more advanced version of the YAML file can be created using the [Weighted Settings](/weighted-settings) page,
|
||||
which allows you to configure up to three presets. The Weighted Settings page has many options which are
|
||||
primarily represented with sliders. This allows you to choose how likely certain options are to occur relative
|
||||
to other options within a category.
|
||||
|
||||
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
|
||||
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20, and your value for "Off" is 40.
|
||||
|
||||
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
|
||||
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
|
||||
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
|
||||
|
||||
If you never want an option to be chosen, simply set its value to zero. Remember that each setting must have at
|
||||
lease one option set to a number greater than zero.
|
||||
|
||||
### Verifying your YAML file
|
||||
If you would like to validate your YAML file to make sure it works, you may do so on the
|
||||
### Verifying your config file
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
||||
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
|
||||
Client is unnecessary for single player games, you may close it and the WebUI.
|
||||
1. Navigate to the [Player Settings](/games/A%20Link%20to%20the%20Past/player-settings) page, configure your options,
|
||||
and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and the Z3Client will launch automatically, create your ROM from
|
||||
the patch file, and open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
|
||||
everyone's patch files. Your patch file should have a `.bmbp` extension.
|
||||
everyone's patch files. Your patch file should have a `.apbp` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically
|
||||
launch the client, and will also create your ROM file in the same place as your patch file.
|
||||
launch the client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
When the client launched automatically, QUsb2Snes should have also automatically launched in the background.
|
||||
When the client launched automatically, SNI should have also automatically launched in the background.
|
||||
If this is its first time launching, you may be prompted to allow it to communicate through the Windows
|
||||
Firewall.
|
||||
|
||||
@@ -101,23 +74,21 @@ Firewall.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Browse to the location you extracted snes9x Multitroid to, enter the `lua` folder, and choose `multibridge.lua`
|
||||
6. Observe a name has been assigned to you, and that the client shows "SNES Device: Connected", with that same
|
||||
name in the upper left corner.
|
||||
5. Select the connector lua file included with your client
|
||||
- Z3Client users should download `sniConnector.lua` from the client download page
|
||||
- SNIClient users should look in their Archipelago folder for `/sni/Connector.lua`
|
||||
|
||||
##### BizHawk
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
||||
these menu options:
|
||||
these menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
5. Browse to your MultiWorld Utilities installation directory, and into the following directories:
|
||||
`QUsb2Snes/Qusb2Snes/LuaBridge`
|
||||
6. Select `luabridge.lua` and click Open.
|
||||
7. Observe a name has been assigned to you, and that the client shows "SNES Device: Connected", with that same
|
||||
name in the upper left corner.
|
||||
5. Select the `sniConnector.lua` file you downloaded above
|
||||
- Z3Client users should download `sniConnector.lua` from the client download page
|
||||
- SNIClient users should look in their Archipelago folder for `/sni/Connector.lua`
|
||||
|
||||
#### With hardware
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
||||
@@ -127,54 +98,30 @@ done so already, please do this now. SD2SNES and FXPak Pro users may download th
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Power on your device and load the ROM.
|
||||
3. Observe the client window now shows "SNES Device: Connected", and lists the name of your device.
|
||||
|
||||
### Connect to the MultiServer
|
||||
The patch file which launched your client should have automatically connected you to the MultiServer.
|
||||
### Connect to the Archipelago Server
|
||||
The patch file which launched your client should have automatically connected you to the AP Server.
|
||||
There are a few reasons this may not happen however, including if the game is hosted on the website but
|
||||
was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host
|
||||
for the address of the server, and copy/paste it into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server
|
||||
Status: Connected". If the client does not connect after a few moments, you may need to refresh the page.
|
||||
Status: Connected".
|
||||
|
||||
### Play the game
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations
|
||||
on successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
The recommended way to host a game is to use the hosting service provided on
|
||||
[the website](/generate). The process is relatively simple:
|
||||
The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
|
||||
|
||||
1. Collect YAML files from your players.
|
||||
2. Create a zip file containing your players' YAML files.
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the website linked above.
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
|
||||
so they may download their patch files from there.
|
||||
**Note:** The patch files provided on this page will allow players to automatically connect to the server,
|
||||
while the patch files on the "Seed Info" page will not.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. You should also provide this link
|
||||
to your players, so they can watch the progress of the game. Any observers may also be given the link to
|
||||
this page.
|
||||
so they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
|
||||
## Auto-Tracking
|
||||
If you would like to use auto-tracking for your game, several pieces of software provide this functionality.
|
||||
The recommended software for auto-tracking is currently
|
||||
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||
|
||||
### Installation
|
||||
1. Download the appropriate installation file for your computer (Windows users want the `.msi` file).
|
||||
2. During the installation process, you may be asked to install the Microsoft Visual Studio Build Tools. A link
|
||||
to this software is provided during the installation procedure, and it must be installed manually.
|
||||
|
||||
### Enable auto-tracking
|
||||
1. With OpenTracker launched, click the Tracking menu at the top of the window, then choose **AutoTracker...**
|
||||
2. Click the **Get Devices** button
|
||||
3. Select your SNES device from the drop-down list
|
||||
4. If you would like to track small keys and dungeon items, check the box labeled **Race Illegal Tracking**
|
||||
5. Click the **Start Autotracking** button
|
||||
6. Close the AutoTracker window, as it is no longer necessary
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta
|
||||
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
||||
|
||||
### Donde puedo obtener un fichero YAML?
|
||||
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||
descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
@@ -67,7 +67,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
|
||||
[YAML Validator](/mysterycheck).
|
||||
|
||||
## Generar una partida para un jugador
|
||||
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
|
||||
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
||||
|
||||
@@ -49,7 +49,7 @@ sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra
|
||||
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
||||
|
||||
### Où est-ce que j'obtiens un fichier YAML ?
|
||||
La page [Génération de partie](/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||
|
||||
### Configuration avancée du fichier YAML
|
||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
|
||||
@@ -71,7 +71,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
|
||||
[Validateur de YAML](/mysterycheck).
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
1. Aller sur la page [Génération de partie](/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
- Example: `simple`
|
||||
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
|
||||
a last instruction.
|
||||
- [Available Bosses](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L135)
|
||||
- [Available Arenas](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L186)
|
||||
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
|
||||
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)
|
||||
|
||||
#### Examples
|
||||
```yaml
|
||||
@@ -79,8 +79,8 @@ boss_shuffle:
|
||||
- placements are picked randomly, not sorted in any way
|
||||
- Warning: Placing non-Dungeon Prizes on Prize locations and
|
||||
Prizes on non-Prize locations will break the game in various ways.
|
||||
- [Available Items](https://github.com/Berserker66/MultiWorld-Utilities/blob/3b5ba161dea223b96e9b1fc890e03469d9c6eb59/Items.py#L26)
|
||||
- [Available Locations](https://github.com/Berserker66/MultiWorld-Utilities/blob/3b5ba161dea223b96e9b1fc890e03469d9c6eb59/Regions.py#L418)
|
||||
- [Available Items](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
|
||||
- [Available Locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L434)
|
||||
|
||||
#### Examples
|
||||
```yaml
|
||||
@@ -133,10 +133,10 @@ Link's House and removes the picked item from the item pool.
|
||||
- `\n` is a newline.
|
||||
- `@` is the entered player's name.
|
||||
- Warning: Text Mapper does not support full unicode.
|
||||
- [Alphabet](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L756)
|
||||
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
|
||||
- at is the location within the game to attach the text to.
|
||||
- can be weighted.
|
||||
- [List of targets](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L1498)
|
||||
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
|
||||
|
||||
#### Example
|
||||
```yaml
|
||||
@@ -160,7 +160,7 @@ This has a 50% chance to trigger at all. If it does, it throws a coin between `u
|
||||
- entrance is the overworld door
|
||||
- exit is the underworld exit
|
||||
- direction can be `both`, `entrance` or `exit`
|
||||
- doors can be found in [this file](https://github.com/Berserker66/MultiWorld-Utilities/blob/main/EntranceShuffle.py)
|
||||
- doors can be found in [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
387
WebHostLib/static/assets/tutorial/zelda5/setup_en.md
Normal file
387
WebHostLib/static/assets/tutorial/zelda5/setup_en.md
Normal file
@@ -0,0 +1,387 @@
|
||||
# Setup Guide for Ocarina of time Archipelago
|
||||
|
||||
## Important
|
||||
|
||||
As we are using Z5Client and Bizhawk, this guide is only applicable to Windows.
|
||||
|
||||
## Required Software
|
||||
|
||||
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) We recommend download Z5Client-setup as it makes some steps automatic.
|
||||
|
||||
## Install Emulator and client
|
||||
|
||||
Download getBizhawk.ps1 from previous link. Place it on the folder where you want your emulator to be installed, right click on it and select "Run with PowerShell". This will download all the needed dependencies used by the emulator. This can take a while.
|
||||
|
||||
It is strongly recommended to associate N64 rom extension (\*.n64) to the Bizhawk we've just installed. To do so, we simple have to search any N64 rom we happened to own, right click and select "Open with...", we unfold the list that appears and select the bottom option "Look for another application", we browse to Bizhawk folder and select EmuHawk.exe
|
||||
|
||||
Place the ootMulti.lua file from the previous link inside the "lua" folder from the just installed emulator.
|
||||
|
||||
Install the Z5Client using its setup.
|
||||
|
||||
## 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 OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in the "Players" folder.
|
||||
```yaml
|
||||
description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
|
||||
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
name: YourName
|
||||
game:
|
||||
Ocarina of Time: 1
|
||||
requires:
|
||||
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
|
||||
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
|
||||
progression_balancing:
|
||||
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
Ocarina of Time:
|
||||
logic_rules: # Set the logic used for the generator.
|
||||
glitchless: 50
|
||||
glitched: 0
|
||||
no_logic: 0
|
||||
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
|
||||
false: 50
|
||||
true: 0
|
||||
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
|
||||
open: 50
|
||||
closed_deku: 0
|
||||
closed: 0
|
||||
open_kakariko: # Set the state of the Kakariko Village gate.
|
||||
open: 50
|
||||
zelda: 0
|
||||
closed: 0
|
||||
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
|
||||
false: 0
|
||||
true: 50
|
||||
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
|
||||
open: 0
|
||||
adult: 0
|
||||
closed: 50
|
||||
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
|
||||
normal: 0
|
||||
fast: 50
|
||||
open: 0
|
||||
bridge: # Set the requirements for the Rainbow Bridge.
|
||||
open: 0
|
||||
vanilla: 0
|
||||
stones: 0
|
||||
medallions: 50
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
trials: # Set the number of required trials in Ganon's Castle.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 50 # minimum value
|
||||
6: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
starting_age: # Choose which age Link will start as.
|
||||
child: 50
|
||||
adult: 0
|
||||
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
|
||||
false: 50
|
||||
true: 0
|
||||
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
|
||||
# you can add additional values between minimum and maximum
|
||||
1: 0 # minimum value
|
||||
50: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
20: 50
|
||||
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
|
||||
false: 50
|
||||
true: 0
|
||||
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
3: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
9: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
100: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
|
||||
remove: 0
|
||||
startwith: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
|
||||
vanilla: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
|
||||
remove: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
on_lacs: 0
|
||||
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
|
||||
false: 50
|
||||
true: 0
|
||||
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
|
||||
vanilla: 50
|
||||
stones: 0
|
||||
medallions: 0
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
lacs_stones: # Set the number of Spiritual Stones required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
3: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_medallions: # Set the number of medallions required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_rewards: # Set the number of dungeon rewards required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
9: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
100: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_song_items: # Set where songs can appear.
|
||||
song: 50
|
||||
dungeon: 0
|
||||
any: 0
|
||||
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
|
||||
0: 0
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
random_value: 0
|
||||
off: 50
|
||||
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
|
||||
off: 50
|
||||
dungeons: 0
|
||||
overworld: 0
|
||||
all: 0
|
||||
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
|
||||
off: 50
|
||||
low: 0
|
||||
regular: 0
|
||||
random_prices: 0
|
||||
shuffle_cows: # Cows give items when Epona's Song is played.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
|
||||
false: 50
|
||||
true: 0
|
||||
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
|
||||
false: 50
|
||||
true: 0
|
||||
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
|
||||
false: 0
|
||||
true: 50
|
||||
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
|
||||
false: 0
|
||||
true: 50
|
||||
no_epona_race: # Epona can always be summoned with Epona's Song.
|
||||
false: 0
|
||||
true: 50
|
||||
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
|
||||
false: 0
|
||||
true: 50
|
||||
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
|
||||
false: 50
|
||||
true: 0
|
||||
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
|
||||
false: 0
|
||||
true: 50
|
||||
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
|
||||
false: 50
|
||||
true: 0
|
||||
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
|
||||
\# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
7: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
hints: # Gossip Stones can give hints about item locations.
|
||||
none: 0
|
||||
mask: 0
|
||||
agony: 0
|
||||
always: 50
|
||||
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
|
||||
balanced: 50
|
||||
ddr: 0
|
||||
league: 0
|
||||
mw2: 0
|
||||
scrubs: 0
|
||||
strong: 0
|
||||
tournament: 0
|
||||
useless: 0
|
||||
very_strong: 0
|
||||
text_shuffle: # Randomizes text in the game for comedic effect.
|
||||
none: 50
|
||||
except_hints: 0
|
||||
complete: 0
|
||||
damage_multiplier: # Controls the amount of damage Link takes.
|
||||
half: 0
|
||||
normal: 50
|
||||
double: 0
|
||||
quadruple: 0
|
||||
ohko: 0
|
||||
no_collectible_hearts: # Hearts will not drop from enemies or objects.
|
||||
false: 50
|
||||
true: 0
|
||||
starting_tod: # Change the starting time of day.
|
||||
default: 50
|
||||
sunrise: 0
|
||||
morning: 0
|
||||
noon: 0
|
||||
afternoon: 0
|
||||
sunset: 0
|
||||
evening: 0
|
||||
midnight: 0
|
||||
witching_hour: 0
|
||||
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
|
||||
false: 50
|
||||
true: 0
|
||||
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
|
||||
false: 50
|
||||
true: 0
|
||||
item_pool_value: # Changes the number of items available in the game.
|
||||
plentiful: 0
|
||||
balanced: 50
|
||||
scarce: 0
|
||||
minimal: 0
|
||||
junk_ice_traps: # Adds ice traps to the item pool.
|
||||
off: 0
|
||||
normal: 50
|
||||
on: 0
|
||||
mayhem: 0
|
||||
onslaught: 0
|
||||
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
|
||||
major_only: 50
|
||||
junk_only: 0
|
||||
anything: 0
|
||||
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 50
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 0
|
||||
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 0
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 50
|
||||
|
||||
```
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your OOT patch file
|
||||
|
||||
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 `.z5ap` extension.
|
||||
|
||||
Double click on your `.z5ap` file to start Z5Client and start the ROM patch process. Once the process is finished (this can take a while), the emulator will be started automatically (If we associated the extension to the emulator as recommended)
|
||||
|
||||
### Connect to multiserver
|
||||
Once both the Z5Client and the emulator are started we must connect them. Within the emulator we click on the "Tools" menu and select "Lua console". In the new window click on the folder icon and look for the ootMulti.lua file. Once the file is loaded it will connect automatically to Z5Client.
|
||||
|
||||
Note: We strongly advise you don't open any emulator menu while it and Z5client are connected, as the script will halt and disconnects can happen. If you get disconnected just double click on the script again.
|
||||
|
||||
To connect the client to the multiserver simply put address:port on the textfield on top and press enter (if the server uses password, type on the bottom textfield /connect <address>:<port> [password], to connect)
|
||||
|
||||
Now you are ready to start your adventure in Hyrule.
|
||||
372
WebHostLib/static/assets/tutorial/zelda5/setup_es.md
Normal file
372
WebHostLib/static/assets/tutorial/zelda5/setup_es.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Guia instalación de Ocarina of time Archipelago
|
||||
|
||||
## Nota importante
|
||||
|
||||
Al usar el cliente y bizhawk, esta guia solo es aplicable en Windows.
|
||||
|
||||
## Software Requerido
|
||||
|
||||
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) Recomendamos bajar el setup de Z5client ya que automatizara varios pasos mas adelante
|
||||
|
||||
## Instala emulador y cliente
|
||||
|
||||
Descarga el fichero getBizhawk.ps1 del enlace anterior. Colocalo en la carpeta donde desees instalar el emulador, haz click derecho en él y selecciona "Ejecutar con PowerShell". Esto descargará todas las dependencias necesarias para el emulador. Puede tardar un rato.
|
||||
|
||||
Es recomendable asociar la extensión de las roms de N64 (\*.n64) al bizhawk que hemos instalado anteriormente. Para hacerlo simplemente debemos buscar alguna rom de n64 que tengamos, hacer click derecho, seleccionar "Abrir con...", desplegar la lista y buscar la opción "Buscar otra aplicación", navegar hasta el directorio de bizhawk y seleccionar EmuHawk.exe
|
||||
|
||||
Situa el fichero ootMulti.lua del enlace anterior en la carpeta "lua" del emulador recien instalado.
|
||||
|
||||
Instala el cliente Z5Client.
|
||||
|
||||
## Configura tu fichero YAML
|
||||
|
||||
### Que es un fichero YAML y por qué necesito uno?
|
||||
Tu fichero YAML contiene un numero de opciones que proveen al generador con información 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 OOT tendra este aspecto. (Hay muchas opciones cosméticas que se han ignorado para este tutorial, si quieres ver una lista completa, descarga (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] y buscar el fichero de ejemplo en el directorio "Players"))
|
||||
```yaml
|
||||
description: Default Ocarina of Time Template # Describe tu fichero yalm
|
||||
\# Tu nombre en el juego. Los espacio seran reemplazados por _ y hay un limite de 16 caracteres
|
||||
name: YourName{number}
|
||||
game:
|
||||
Ocarina of Time: 1
|
||||
requires:
|
||||
version: 0.1.7 # Version de archipelago minima.
|
||||
\# Opciones compartidas por todos los juegos:
|
||||
accessibility:
|
||||
items: 0 # Garantiza que puedes obtener todos los objetos pero no todas las localizaciones
|
||||
locations: 50 # Garantiza que puedes obtener todas las localizaciones
|
||||
none: 0 # Solo garantiza que el juego pueda completarse.
|
||||
progression_balancing:
|
||||
on: 50 # Un sistema para reducir tiempos de espera en una partida multiworld
|
||||
off: 0
|
||||
Ocarina of Time:
|
||||
logic_rules: # Logica usada por el randomizer.
|
||||
glitchless: 50
|
||||
glitched: 0
|
||||
no_logic: 0
|
||||
logic_no_night_tokens_without_suns_song: # Las skulltulas nocturnas requeriran la cancion del sol por logica
|
||||
false: 50
|
||||
true: 0
|
||||
open_forest: # Indica el estado del bosque Kokiri y el camino al Arbol Deku.
|
||||
open: 50
|
||||
closed_deku: 0
|
||||
closed: 0
|
||||
open_kakariko: # Indica el estado de la puerta de Kakariko hacia la montaña de la muerte.
|
||||
open: 50
|
||||
zelda: 0
|
||||
closed: 0
|
||||
open_door_of_time: # Abre la puerta del tiempo sin la cancion del tiempo.
|
||||
false: 0
|
||||
true: 50
|
||||
zora_fountain: # Indica el estado del rey zora bloqueando el camino a la fuente Zora.
|
||||
open: 0
|
||||
adult: 0
|
||||
closed: 50
|
||||
gerudo_fortress: # Indica los requerimientos para acceder a la fortaleza Gerudo.
|
||||
normal: 0
|
||||
fast: 50
|
||||
open: 0
|
||||
bridge: # Indica los requerimientos para el puente arco iris.
|
||||
open: 0
|
||||
vanilla: 0
|
||||
stones: 0
|
||||
medallions: 50
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
trials: # Numero de pruebas dentro del castillo de Ganon.
|
||||
0: 50 # minimum value
|
||||
6: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
starting_age: # Indica la edad con la que empieza link.
|
||||
child: 50
|
||||
adult: 0
|
||||
triforce_hunt: # Reune piezas de trifuerza para completar el juego.
|
||||
false: 50
|
||||
true: 0
|
||||
triforce_goal: # Numero de piezas de trifuerza requeridas. El numero de piezas disponibles es determinado por la opcion "Item pool".
|
||||
1: 0 # minimum value
|
||||
50: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
20: 50
|
||||
bombchus_in_logic: # Los bombchus son considerados para la logica. El primer pack encontrado da 20 chus y las tiendas kokiri y el bazaar los venden. Bombchus abren la bolera.
|
||||
false: 50
|
||||
true: 0
|
||||
bridge_stones: # Numero de piedras para abrir el puente arco iris.
|
||||
0: 0 # minimum value
|
||||
3: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_medallions: # Numero de medallones para abrir el puente arco iris.
|
||||
0: 0 # minimum value
|
||||
6: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_rewards: # Numero de mazmorras (cualquier combinacion de medallones y piedras) para abrir el puente arco iris.
|
||||
0: 0 # minimum value
|
||||
9: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_tokens: # Numero de skultullas de oro requeridas para el puente arco iris.
|
||||
0: 0 # minimum value
|
||||
100: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_mapcompass: # Controla donde pueden aparecer los mapas y las brujulas.
|
||||
remove: 0
|
||||
startwith: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_smallkeys: # Controla donde pueden aparecer las llaves pequeñas.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_fortresskeys: # Controla donde pueden aparecer las llaves de la fortaleza Gerudo.
|
||||
vanilla: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_bosskeys: # Controla donde pueden aparecer las llaves de jefe (excepto la llave del castillo de ganon).
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_ganon_bosskey: # Controla donde puede aparecer la llave del jefe del castillo de Ganon.
|
||||
remove: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
on_lacs: 0
|
||||
enhance_map_compass: # El mapa indica si una dungeon es clasica o Master Quest. Las brujulas indican la recompensa de mazmorra.
|
||||
false: 50
|
||||
true: 0
|
||||
lacs_condition: # Marca el requerimiento para la escena de las flechas de luz (LACS) en el templo del tiempo.
|
||||
vanilla: 50
|
||||
stones: 0
|
||||
medallions: 0
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
lacs_stones: # Marca el numero de piedras espirituales requeridas para LACS
|
||||
0: 0 # minimum value
|
||||
3: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_medallions: # Marca el numero de medallones requeridas para LACS.
|
||||
0: 0 # minimum value
|
||||
6: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_rewards: # Marca el numero de recompensas de mazmorra requeridas para LACS.
|
||||
0: 0 # minimum value
|
||||
9: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_tokens: # Marca el numero de Skulltulas de oro requeridas para LACS.
|
||||
0: 0 # minimum value
|
||||
100: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_song_items: # Marca donde pueden aparecer las canciones.
|
||||
song: 50
|
||||
dungeon: 0
|
||||
any: 0
|
||||
shopsanity: # Aleatoriza el contenido de las tiendas. "off" para no mezclar las tiendas; "0" mezcla las tiendas pero no permite objetos unicos en ellas.
|
||||
0: 0
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
random_value: 0
|
||||
off: 50
|
||||
tokensanity: # Indica si las Skulltulas de oro pueden tener objetos que no sean su ficha.
|
||||
off: 50
|
||||
dungeons: 0
|
||||
overworld: 0
|
||||
all: 0
|
||||
shuffle_scrubs: # Aleatoriza los objetos de los Scrubs vendedores y marca su precio.
|
||||
off: 50
|
||||
low: 0
|
||||
regular: 0
|
||||
random_prices: 0
|
||||
shuffle_cows: # Las vacas dan objetos cuando les tocas las cancion de Epona.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_kokiri_sword: # Aleatoriza la posicion de la espada Kokiri.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_ocarinas: # Aleatoriza la posicion de las ocarinas.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_weird_egg: # Aleatoriza la posicion del huevo extraño.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_gerudo_card: # Aleatoriza la posicion de la tarjeta de membresia Gerudo.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_beans: # Añade un pack de 10 judias magicas al juego y el vendedor vende un solo objeto por 60 rupias.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_medigoron_carpet_salesman: # Aleatoriza el objeto que vende Medigoron y el vendedor de la alfombra voladora del paramo maldito.
|
||||
false: 50
|
||||
true: 0
|
||||
skip_child_zelda: # Empieza el juego con la carta de zelda, el objeto que daria impa al enseñar la nana de zelda. Y zelda se considera ya visitada (puedes ir directamente a ver a Saria al bosque y a Malon al rancho)
|
||||
false: 50
|
||||
true: 0
|
||||
no_escape_sequence: # Elimina la huida de link y zelda despues de ganar a Ganondorf.
|
||||
false: 0
|
||||
true: 50
|
||||
no_guard_stealth: # Elimina la escena de sigilo antes de ver a Zelda.
|
||||
false: 0
|
||||
true: 50
|
||||
no_epona_race: # No necesitas hacer la carrera para invocar a Epona.
|
||||
false: 0
|
||||
true: 50
|
||||
skip_some_minigame_phases: # La carrera de Dampe y el minijuego de arco a caballo dan ambras recompensas a la vez si se cumplen las condiciones.
|
||||
false: 0
|
||||
true: 50
|
||||
complete_mask_quest: # Todas las mascaras estan disponibles.
|
||||
false: 50
|
||||
true: 0
|
||||
useful_cutscenes: # Ciertas escenas se mantienen (como los Poes del templo del bosque, Darunia o Twinrova. Principalmente util para modos con Glitches.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_chests: # Los cofres siempre se cogen rapido. Si se desactiva, los objetos importantes tienen animacion lenta. (IMPORTANTE: TODOS LOS OBJETOS QUE VAYAN A OTROS MUNDOS SE CONSIDERAN IMPORTANTES)
|
||||
false: 0
|
||||
true: 50
|
||||
free_scarecrow: # Sacara la ocraina cerca de un punto con espantapajaros invoca a Pierre sin necesidad de la cancion.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_bunny_hood: # La capucha conejo mejora tu velocidad como en Majora's Mask.
|
||||
false: 50
|
||||
true: 0
|
||||
chicken_count: # Numero de Cuccos que Anju necesita en el corral para que te de el objeto.
|
||||
0: 0 # minimum value
|
||||
7: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
hints: # Marca el requerimiento para que las piedras chivatas den pistas.
|
||||
none: 0
|
||||
mask: 0
|
||||
agony: 0
|
||||
always: 50
|
||||
hint_dist: # Elije la distribucion de pistas
|
||||
balanced: 50
|
||||
ddr: 0
|
||||
league: 0
|
||||
mw2: 0
|
||||
scrubs: 0
|
||||
strong: 0
|
||||
tournament: 0
|
||||
useless: 0
|
||||
very_strong: 0
|
||||
damage_multiplier: # Controla el daño que recibe Link.
|
||||
half: 0
|
||||
normal: 50
|
||||
double: 0
|
||||
quadruple: 0
|
||||
ohko: 0
|
||||
no_collectible_hearts: # No caen corazones de enemigos u objetos.
|
||||
false: 50
|
||||
true: 0
|
||||
starting_tod: # Cambia el momento del dia al empezar el juego.
|
||||
default: 50
|
||||
sunrise: 0
|
||||
morning: 0
|
||||
noon: 0
|
||||
afternoon: 0
|
||||
sunset: 0
|
||||
evening: 0
|
||||
midnight: 0
|
||||
witching_hour: 0
|
||||
start_with_consumables: # Empieza el juego con el maximo de palos y nueves Deku que pueda llevar Link.
|
||||
false: 50
|
||||
true: 0
|
||||
start_with_rupees: # Empieza el juego con la cartera llena. Las mejoras de cartera vienen llenas.
|
||||
false: 50
|
||||
true: 0
|
||||
item_pool_value: # Cambia el numero de objetos disponibles en el juego.
|
||||
plentiful: 0
|
||||
balanced: 50
|
||||
scarce: 0
|
||||
minimal: 0
|
||||
junk_ice_traps: # Añade trampas de hielo.
|
||||
off: 0
|
||||
normal: 50
|
||||
on: 0
|
||||
mayhem: 0
|
||||
onslaught: 0
|
||||
ice_trap_appearance: # Cambia la apariencia de las trampas de hielo cuando aparecen como objetos fuera de cofres.
|
||||
major_only: 50
|
||||
junk_only: 0
|
||||
anything: 0
|
||||
logic_earliest_adult_trade: # Objeto mas bajo que puede aparecer en la secuencia de cambios de Link Adulto.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 50
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 0
|
||||
logic_latest_adult_trade: # Objeto mas tardio que puede aparecer en la secuencia de cambios de Link Adulto.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 0
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 50
|
||||
|
||||
```
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
### Obten tu parche
|
||||
|
||||
|
||||
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld.
|
||||
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 `.z5ap`.
|
||||
|
||||
Haz doble click en tu fichero `.z5ap` para que se arranque el Z5Client y realize el parcheado de la ROM. Una vez acabe el parcheado de la rom (esto puede llevar un tiempo) se abrira automaticamente el emulador (Si se ha asociado la extensión al emulador tal como hemos recomendado)
|
||||
|
||||
### Conectar al multiserver
|
||||
Una vez arrancado tanto el Z5Client como el emulador hay que conectarlo entre ellos, para ello simplemente accede al menú "Tools" y selecciona "Lua console". En la nueva ventana, dale al icono de la carpeta y busca el fichero ootMulti.lua. Al cargar dicho fichero se conectara automaticamente con el cliente.
|
||||
|
||||
Nota: Es muy recomendable que no se abra ningún menú del emulador mientras esten emulador y Z5Client conectados, ya que el script de conexión se para en ese caso y pueden provocar desconexiones. Si se pierde la conexion, simplemente haz doble click en el script de nuevo.
|
||||
|
||||
Para conectar el cliente con el servidor simplemente pon la direccion_IP:puerto en la caja de texto de arriba y presiona enter (si el servidor tiene contraseña, en la caja de texto de abajo escribir /connect direccion:puerto contraseña, para conectar)
|
||||
|
||||
Y ya estas listo, para emprender tu aventura por Hyrule.
|
||||
@@ -1,486 +0,0 @@
|
||||
let spriteData = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const gameSettings = document.getElementById('weighted-settings');
|
||||
Promise.all([fetchWeightedSettingsYaml(), fetchWeightedSettingsJson(), fetchSpriteData()]).then((results) => {
|
||||
// Load YAML into object
|
||||
const sourceData = jsyaml.safeLoad(results[0], { json: true });
|
||||
const wsVersion = sourceData.ws_version;
|
||||
delete sourceData.ws_version; // Do not include the settings version number in the export
|
||||
|
||||
// Check if settings exist in localStorage. If no settings are present, this is a first load (or reset to default)
|
||||
// and the version number should be silently updated
|
||||
if (!localStorage.getItem('weightedSettings1')) {
|
||||
localStorage.setItem('wsVersion', wsVersion);
|
||||
}
|
||||
|
||||
// Update localStorage with three settings objects. Preserve original objects if present.
|
||||
for (let i=1; i<=3; i++) {
|
||||
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
|
||||
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
|
||||
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
|
||||
}
|
||||
|
||||
// Build the entire UI
|
||||
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
|
||||
|
||||
// Populate the UI and add event listeners
|
||||
populateSettings();
|
||||
document.getElementById('preset-number').addEventListener('change', populateSettings);
|
||||
gameSettings.addEventListener('change', handleOptionChange);
|
||||
gameSettings.addEventListener('keyup', handleOptionChange);
|
||||
|
||||
document.getElementById('export-button').addEventListener('click', exportSettings);
|
||||
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
|
||||
adjustHeaderWidth();
|
||||
|
||||
if (localStorage.getItem('wsVersion') !== wsVersion) {
|
||||
const userWarning = document.getElementById('user-warning');
|
||||
const messageSpan = document.createElement('span');
|
||||
messageSpan.innerHTML = "A new version of the weighted settings file is available. Click here to update!" +
|
||||
"<br />Be aware this will also reset your presets, so you should export them now if you want to save them.";
|
||||
userWarning.appendChild(messageSpan);
|
||||
userWarning.style.display = 'block';
|
||||
userWarning.addEventListener('click', resetToDefaults);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
gameSettings.innerHTML = `
|
||||
<h2>Something went wrong while loading your game settings page.</h2>
|
||||
<h2>${error}</h2>
|
||||
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
|
||||
`
|
||||
});
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
});
|
||||
|
||||
const fetchWeightedSettingsYaml = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject("Unable to fetch source yaml file.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const fetchWeightedSettingsJson = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject('Unable to fetch JSON schema file');
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const fetchSpriteData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject('Unable to fetch sprite data.');
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/spriteData.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const handleOptionChange = (event) => {
|
||||
if(!event.target.matches('.setting')) { return; }
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||
const settingString = event.target.getAttribute('data-setting');
|
||||
document.getElementById(settingString).innerText = event.target.value;
|
||||
if(getSettingValue(settings, settingString) !== false){
|
||||
const keys = settingString.split('.');
|
||||
switch (keys.length) {
|
||||
case 1:
|
||||
settings[keys[0]] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
case 2:
|
||||
settings[keys[0]][keys[1]] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
case 3:
|
||||
settings[keys[0]][keys[1]][keys[2]] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the updated settings object bask to localStorage
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
|
||||
}else{
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
}
|
||||
};
|
||||
|
||||
const populateSettings = () => {
|
||||
buildSpriteOptions();
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
|
||||
settingsInputs.forEach((input) => {
|
||||
const settingString = input.getAttribute('data-setting');
|
||||
const settingValue = getSettingValue(settings, settingString);
|
||||
if(settingValue !== false){
|
||||
input.value = settingValue;
|
||||
document.getElementById(settingString).innerText = settingValue;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value of the settings object, or false if the settings object does not exist
|
||||
* @param settings
|
||||
* @param keyString
|
||||
* @returns {string} | bool
|
||||
*/
|
||||
const getSettingValue = (settings, keyString) => {
|
||||
const keys = keyString.split('.');
|
||||
let currentVal = settings;
|
||||
keys.forEach((key) => {
|
||||
if(typeof(key) === 'string' && currentVal.hasOwnProperty(key)){
|
||||
currentVal = currentVal[key];
|
||||
}else{
|
||||
currentVal = false;
|
||||
}
|
||||
});
|
||||
return currentVal;
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${settings.description}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
const resetToDefaults = () => {
|
||||
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
|
||||
location.reload();
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const buildUI = (settings, spriteData) => {
|
||||
const settingsWrapper = document.getElementById('settings-wrapper');
|
||||
const settingTypes = {
|
||||
gameOptions: 'Game Options',
|
||||
romOptions: 'ROM Options',
|
||||
}
|
||||
|
||||
Object.keys(settingTypes).forEach((settingTypeKey) => {
|
||||
const sectionHeader = document.createElement('h2');
|
||||
sectionHeader.innerText = settingTypes[settingTypeKey];
|
||||
settingsWrapper.appendChild(sectionHeader);
|
||||
|
||||
Object.values(settings[settingTypeKey]).forEach((setting) => {
|
||||
if (typeof(setting.inputType) === 'undefined' || !setting.inputType){
|
||||
console.error(setting);
|
||||
throw new Error('Setting with no inputType specified.');
|
||||
}
|
||||
|
||||
switch(setting.inputType){
|
||||
case 'text':
|
||||
// Currently, all text input is handled manually because there is very little of it
|
||||
return;
|
||||
case 'range':
|
||||
buildRangeSettings(settingsWrapper, setting);
|
||||
return;
|
||||
default:
|
||||
console.error(setting);
|
||||
throw new Error('Unhandled inputType specified.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build sprite options
|
||||
const spriteOptionsHeader = document.createElement('h2');
|
||||
spriteOptionsHeader.innerText = 'Sprite Options';
|
||||
settingsWrapper.appendChild(spriteOptionsHeader);
|
||||
|
||||
const spriteOptionsWrapper = document.createElement('div');
|
||||
spriteOptionsWrapper.setAttribute('id', 'sprite-options-wrapper');
|
||||
spriteOptionsWrapper.className = 'setting-wrapper';
|
||||
settingsWrapper.appendChild(spriteOptionsWrapper);
|
||||
|
||||
// Append sprite picker
|
||||
settingsWrapper.appendChild(buildSpritePicker(spriteData));
|
||||
};
|
||||
|
||||
const buildSpriteOptions = () => {
|
||||
const spriteOptionsWrapper = document.getElementById('sprite-options-wrapper');
|
||||
|
||||
// Clear the contents of the wrapper div
|
||||
while(spriteOptionsWrapper.firstChild){
|
||||
spriteOptionsWrapper.removeChild(spriteOptionsWrapper.lastChild);
|
||||
}
|
||||
|
||||
const spriteOptionsTitle = document.createElement('span');
|
||||
spriteOptionsTitle.className = 'title-span';
|
||||
spriteOptionsTitle.innerText = 'Alternate Sprites';
|
||||
spriteOptionsWrapper.appendChild(spriteOptionsTitle);
|
||||
|
||||
const spriteOptionsDescription = document.createElement('span');
|
||||
spriteOptionsDescription.className = 'description-span';
|
||||
spriteOptionsDescription.innerHTML = 'Choose an alternate sprite to play the game with. Additional randomization ' +
|
||||
'options are documented in the ' +
|
||||
'<a href="https://github.com/Berserker66/MultiWorld-Utilities/blob/main/playerSettings.yaml#L374">settings file</a>.';
|
||||
spriteOptionsWrapper.appendChild(spriteOptionsDescription);
|
||||
|
||||
const spriteOptionsTable = document.createElement('table');
|
||||
spriteOptionsTable.setAttribute('id', 'sprite-options-table');
|
||||
spriteOptionsTable.className = 'option-set';
|
||||
const tbody = document.createElement('tbody');
|
||||
tbody.setAttribute('id', 'sprites-tbody');
|
||||
|
||||
const currentPreset = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
|
||||
|
||||
// Manually add a row for random sprites
|
||||
addSpriteRow(tbody, playerSettings, 'random');
|
||||
|
||||
// Add a row for each sprite currently present in the player's settings
|
||||
Object.keys(playerSettings.rom.sprite).forEach((spriteName) => {
|
||||
if(['random'].indexOf(spriteName) > -1) return;
|
||||
addSpriteRow(tbody, playerSettings, spriteName)
|
||||
});
|
||||
|
||||
spriteOptionsTable.appendChild(tbody);
|
||||
spriteOptionsWrapper.appendChild(spriteOptionsTable);
|
||||
};
|
||||
|
||||
const buildRangeSettings = (parentElement, settings) => {
|
||||
// Ensure we are operating on a range-specific setting
|
||||
if(typeof(settings.inputType) === 'undefined' || settings.inputType !== 'range'){
|
||||
throw new Error('Invalid input type provided to buildRangeSettings func.');
|
||||
}
|
||||
|
||||
const settingWrapper = document.createElement('div');
|
||||
settingWrapper.className = 'setting-wrapper';
|
||||
|
||||
if(typeof(settings.friendlyName) !== 'undefined' && settings.friendlyName){
|
||||
const sectionTitle = document.createElement('span');
|
||||
sectionTitle.className = 'title-span';
|
||||
sectionTitle.innerText = settings.friendlyName;
|
||||
settingWrapper.appendChild(sectionTitle);
|
||||
}
|
||||
|
||||
if(settings.description){
|
||||
const description = document.createElement('span');
|
||||
description.className = 'description-span';
|
||||
description.innerText = settings.description;
|
||||
settingWrapper.appendChild(description);
|
||||
}
|
||||
|
||||
// Create table
|
||||
const optionSetTable = document.createElement('table');
|
||||
optionSetTable.className = 'option-set';
|
||||
|
||||
// Create table body
|
||||
const tbody = document.createElement('tbody');
|
||||
Object.keys(settings.subOptions).forEach((setting) => {
|
||||
// Overwrite setting key name with real object
|
||||
setting = settings.subOptions[setting];
|
||||
const settingId = (Math.random() * 1000000).toString();
|
||||
|
||||
// Create rows for each option
|
||||
const optionRow = document.createElement('tr');
|
||||
|
||||
// Option name td
|
||||
const optionName = document.createElement('td');
|
||||
optionName.className = 'option-name';
|
||||
const optionLabel = document.createElement('label');
|
||||
optionLabel.setAttribute('for', settingId);
|
||||
optionLabel.setAttribute('data-tooltip', setting.description);
|
||||
optionLabel.innerText = setting.friendlyName;
|
||||
optionName.appendChild(optionLabel);
|
||||
optionRow.appendChild(optionName);
|
||||
|
||||
// Option value td
|
||||
const optionValue = document.createElement('td');
|
||||
optionValue.className = 'option-value';
|
||||
const input = document.createElement('input');
|
||||
input.className = 'setting';
|
||||
input.setAttribute('id', settingId);
|
||||
input.setAttribute('type', 'range');
|
||||
input.setAttribute('min', '0');
|
||||
input.setAttribute('max', '100');
|
||||
input.setAttribute('data-setting', setting.keyString);
|
||||
input.value = setting.defaultValue;
|
||||
optionValue.appendChild(input);
|
||||
const valueDisplay = document.createElement('span');
|
||||
valueDisplay.setAttribute('id', setting.keyString);
|
||||
valueDisplay.innerText = setting.defaultValue;
|
||||
optionValue.appendChild(valueDisplay);
|
||||
optionRow.appendChild(optionValue);
|
||||
tbody.appendChild(optionRow);
|
||||
});
|
||||
|
||||
optionSetTable.appendChild(tbody);
|
||||
settingWrapper.appendChild(optionSetTable);
|
||||
parentElement.appendChild(settingWrapper);
|
||||
};
|
||||
|
||||
const addSpriteRow = (tbody, playerSettings, spriteName) => {
|
||||
const rowId = (Math.random() * 1000000).toString();
|
||||
const optionId = (Math.random() * 1000000).toString();
|
||||
const tr = document.createElement('tr');
|
||||
tr.setAttribute('id', rowId);
|
||||
|
||||
// Option Name
|
||||
const optionName = document.createElement('td');
|
||||
optionName.className = 'option-name';
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = optionId;
|
||||
label.innerText = spriteName;
|
||||
optionName.appendChild(label);
|
||||
|
||||
if(['random', 'random_sprite_on_event'].indexOf(spriteName) === -1) {
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.setAttribute('data-sprite', spriteName);
|
||||
deleteButton.setAttribute('data-row-id', rowId);
|
||||
deleteButton.innerText = ' (❌)';
|
||||
deleteButton.className = 'delete-button';
|
||||
optionName.appendChild(deleteButton);
|
||||
deleteButton.addEventListener('click', removeSpriteOption);
|
||||
}
|
||||
|
||||
tr.appendChild(optionName);
|
||||
|
||||
// Option Value
|
||||
const optionValue = document.createElement('td');
|
||||
optionValue.className = 'option-value';
|
||||
const input = document.createElement('input');
|
||||
input.className = 'setting';
|
||||
input.setAttribute('id', optionId);
|
||||
input.setAttribute('type', 'range');
|
||||
input.setAttribute('min', '0');
|
||||
input.setAttribute('max', '100');
|
||||
input.setAttribute('data-setting', `rom.sprite.${spriteName}`);
|
||||
input.value = "50";
|
||||
optionValue.appendChild(input);
|
||||
|
||||
// Value display
|
||||
const valueDisplay = document.createElement('span');
|
||||
valueDisplay.setAttribute('id', `rom.sprite.${spriteName}`);
|
||||
valueDisplay.innerText = playerSettings.rom.sprite.hasOwnProperty(spriteName) ?
|
||||
playerSettings.rom.sprite[spriteName] : '0';
|
||||
optionValue.appendChild(valueDisplay);
|
||||
|
||||
tr.appendChild(optionValue);
|
||||
tbody.appendChild(tr);
|
||||
};
|
||||
|
||||
const addSpriteOption = (event) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const spriteName = event.target.getAttribute('data-sprite');
|
||||
|
||||
if (Object.keys(playerSettings.rom.sprite).indexOf(spriteName) !== -1) {
|
||||
// Do not add the same sprite twice
|
||||
return;
|
||||
}
|
||||
|
||||
// Add option to playerSettings object
|
||||
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
|
||||
// Add <tr> to #sprite-options-table
|
||||
const tbody = document.getElementById('sprites-tbody');
|
||||
addSpriteRow(tbody, playerSettings, spriteName);
|
||||
};
|
||||
|
||||
const removeSpriteOption = (event) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const spriteName = event.target.getAttribute('data-sprite');
|
||||
|
||||
// Remove option from playerSettings object
|
||||
delete playerSettings.rom.sprite[spriteName];
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
|
||||
// Remove <tr> from #sprite-options-table
|
||||
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
|
||||
tr.parentNode.removeChild(tr);
|
||||
};
|
||||
|
||||
const buildSpritePicker = (spriteData) => {
|
||||
const spritePicker = document.createElement('div');
|
||||
spritePicker.setAttribute('id', 'sprite-picker');
|
||||
|
||||
// Build description
|
||||
const description = document.createElement('span');
|
||||
description.innerText = 'To add a sprite to your playable list, click the one you want below.';
|
||||
spritePicker.appendChild(description);
|
||||
|
||||
const sprites = document.createElement('div');
|
||||
sprites.setAttribute('id', 'sprite-picker-sprites');
|
||||
spriteData.sprites.forEach((sprite) => {
|
||||
const spriteImg = document.createElement('img');
|
||||
let spriteGifFile = sprite.file.split('.');
|
||||
spriteGifFile.pop();
|
||||
spriteGifFile = spriteGifFile.join('.') + '.gif';
|
||||
spriteImg.setAttribute('src', `static/generated/sprites/${spriteGifFile}`);
|
||||
spriteImg.setAttribute('data-sprite', sprite.file.split('.')[0]);
|
||||
spriteImg.setAttribute('alt', sprite.name);
|
||||
|
||||
// Wrap the image in a span to allow for tooltip presence
|
||||
const imgWrapper = document.createElement('span');
|
||||
imgWrapper.className = 'sprite-img-wrapper';
|
||||
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
|
||||
imgWrapper.appendChild(spriteImg);
|
||||
imgWrapper.setAttribute('data-sprite', sprite.name);
|
||||
sprites.appendChild(imgWrapper);
|
||||
imgWrapper.addEventListener('click', addSpriteOption);
|
||||
});
|
||||
|
||||
spritePicker.appendChild(sprites);
|
||||
return spritePicker;
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||
playerCount: 1,
|
||||
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.';
|
||||
if (error.response.data.text) {
|
||||
userMessage.innerText += ' ' + error.response.data.text;
|
||||
}
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"gameOptions": {
|
||||
"description": {
|
||||
"keyString": "description",
|
||||
"friendlyName": "Description",
|
||||
"inputType": "text",
|
||||
"description": "A short description of this preset. Useful if you have multiple files",
|
||||
"defaultValue": "Preset Name"
|
||||
},
|
||||
"keyString": "description",
|
||||
"friendlyName": "Description",
|
||||
"inputType": "text",
|
||||
"description": "A short description of this preset. Useful if you have multiple files",
|
||||
"defaultValue": "Preset Name"
|
||||
},
|
||||
"name": {
|
||||
"keyString": "name",
|
||||
"friendlyName": "Player Name",
|
||||
@@ -476,8 +476,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"triforce_pieces_mode": {
|
||||
"keyString": "triforce_pieces_mode",
|
||||
"triforce_pieces_mode": {
|
||||
"keyString": "triforce_pieces_mode",
|
||||
"friendlyName": "Triforce Piece Availability Mode",
|
||||
"description": "Determines which of the following three options will be used to determine the total available triforce pieces.",
|
||||
"inputType": "range",
|
||||
@@ -501,7 +501,7 @@
|
||||
"defaultValue": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"triforce_pieces_available": {
|
||||
"keyString": "triforce_pieces_available",
|
||||
"friendlyName": "Exact Number (Triforce Hunt)",
|
||||
@@ -534,7 +534,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"triforce_pieces_extra": {
|
||||
"triforce_pieces_extra": {
|
||||
"keyString": "triforce_pieces_extra",
|
||||
"friendlyName": "Required Plus (Triforce Hunt)",
|
||||
"description": "Only used if enabled in Triforce Piece Availability Mode.",
|
||||
@@ -564,7 +564,7 @@
|
||||
"description": "15 extra Triforce pieces will be hidden throughout Hyrule",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"20": {
|
||||
"20": {
|
||||
"keyString": "triforce_pieces_extra.20",
|
||||
"friendlyName": 20,
|
||||
"description": "20 extra Triforce pieces will be hidden throughout Hyrule",
|
||||
@@ -572,7 +572,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"triforce_pieces_percentage": {
|
||||
"triforce_pieces_percentage": {
|
||||
"keyString": "triforce_pieces_percentage",
|
||||
"friendlyName": "Percentage (Triforce Hunt)",
|
||||
"description": "Only used if enabled in Triforce Piece Availability Mode.",
|
||||
@@ -1164,41 +1164,79 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"beemizer": {
|
||||
"keyString": "beemizer",
|
||||
"friendlyName": "Beemizer",
|
||||
"description": "Remove non-health items from the global item pool and replace them with single bees and bee traps.",
|
||||
"beemizer_total_chance": {
|
||||
"keyString": "beemizer_total_chance",
|
||||
"friendlyName": "Beemizer - Total Chance",
|
||||
"description": "Chance to replace junk-fill items in the global item pool with single bees and bee traps.",
|
||||
"inputType": "range",
|
||||
"subOptions": {
|
||||
"0": {
|
||||
"keyString": "beemizer.0",
|
||||
"keyString": "beemizer_total_chance.0",
|
||||
"friendlyName": "Level 0",
|
||||
"description": "No bee traps are placed.",
|
||||
"defaultValue": 50
|
||||
},
|
||||
"1": {
|
||||
"keyString": "beemizer.1",
|
||||
"25": {
|
||||
"keyString": "beemizer_total_chance.25",
|
||||
"friendlyName": "Level 1",
|
||||
"description": "25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees",
|
||||
"defaultValue": 1
|
||||
"description": "25% chance for each junk-fill item (rupees, bombs and arrows) to be replaced with bees.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"2": {
|
||||
"keyString": "beemizer.2",
|
||||
"50": {
|
||||
"keyString": "beemizer_total_chance.50",
|
||||
"friendlyName": "Level 2",
|
||||
"description": "50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees",
|
||||
"defaultValue": 2
|
||||
"description": "50% chance for each junk-fill item (rupees, bombs and arrows) to be replaced with bees.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"3": {
|
||||
"keyString": "beemizer.3",
|
||||
"75": {
|
||||
"keyString": "beemizer_total_chance.75",
|
||||
"friendlyName": "Level 3",
|
||||
"description": "75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees",
|
||||
"defaultValue": 3
|
||||
"description": "75% chance for each junk-fill item (rupees, bombs and arrows) to be replaced with bees.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"4": {
|
||||
"keyString": "beemizer.4",
|
||||
"100": {
|
||||
"keyString": "beemizer_total_chance.100",
|
||||
"friendlyName": "Level 4",
|
||||
"description": "100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees",
|
||||
"defaultValue": 4
|
||||
"description": "All junk-fill items (rupees, bombs and arrows) are replaced with bees.",
|
||||
"defaultValue": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"beemizer_trap_chance": {
|
||||
"keyString": "beemizer_trap_chance",
|
||||
"friendlyName": "Beemizer - Trap Chance",
|
||||
"description": "Chance that replaced junk-fill items are bee traps.",
|
||||
"inputType": "range",
|
||||
"subOptions": {
|
||||
"60": {
|
||||
"keyString": "beemizer_trap_chance.60",
|
||||
"friendlyName": "Level 0",
|
||||
"description": "60% chance for each beemizer replacement to be a trap (40% chance of a single bee).",
|
||||
"defaultValue": 50
|
||||
},
|
||||
"70": {
|
||||
"keyString": "beemizer_trap_chance.70",
|
||||
"friendlyName": "Level 1",
|
||||
"description": "70% chance for each beemizer replacement to be a trap (30% chance of a single bee).",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"80": {
|
||||
"keyString": "beemizer_trap_chance.80",
|
||||
"friendlyName": "Level 2",
|
||||
"description": "80% chance for each beemizer replacement to be a trap (20% chance of a single bee).",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"90": {
|
||||
"keyString": "beemizer_trap_chance.90",
|
||||
"friendlyName": "Level 3",
|
||||
"description": "90% chance for each beemizer replacement to be a trap (10% chance of a single bee).",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"100": {
|
||||
"keyString": "beemizer_trap_chance.100",
|
||||
"friendlyName": "Level 4",
|
||||
"description": "All beemizer replacements are traps (no single bees).",
|
||||
"defaultValue": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1818,37 +1856,37 @@
|
||||
"description": "Never use this. Makes all overworld palette colors black.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"grayscale": {
|
||||
"grayscale": {
|
||||
"keyString": "rom.ow_palettes.grayscale",
|
||||
"friendlyName": "Grayscale",
|
||||
"description": "Removes all saturation of colors.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"negative": {
|
||||
"negative": {
|
||||
"keyString": "rom.ow_palettes.negative",
|
||||
"friendlyName": "Negative",
|
||||
"description": "Invert all colors",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"classic": {
|
||||
"classic": {
|
||||
"keyString": "rom.ow_palettes.classic",
|
||||
"friendlyName": "Classic",
|
||||
"description": "Produces results similar to the website.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"dizzy": {
|
||||
"dizzy": {
|
||||
"keyString": "rom.ow_palettes.dizzy",
|
||||
"friendlyName": "Dizzy",
|
||||
"description": "No logic in colors but saturation and lightness are conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"sick": {
|
||||
"sick": {
|
||||
"keyString": "rom.ow_palettes.sick",
|
||||
"friendlyName": "Sick",
|
||||
"description": "No logic in colors but lightness is conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"puke": {
|
||||
"puke": {
|
||||
"keyString": "rom.ow_palettes.puke",
|
||||
"friendlyName": "Puke",
|
||||
"description": "No logic at all.",
|
||||
@@ -1880,37 +1918,37 @@
|
||||
"description": "Never use this. Makes all underworld palette colors black.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"grayscale": {
|
||||
"grayscale": {
|
||||
"keyString": "rom.uw_palettes.grayscale",
|
||||
"friendlyName": "Grayscale",
|
||||
"description": "Removes all saturation of colors.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"negative": {
|
||||
"negative": {
|
||||
"keyString": "rom.uw_palettes.negative",
|
||||
"friendlyName": "Negative",
|
||||
"description": "Invert all colors",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"classic": {
|
||||
"classic": {
|
||||
"keyString": "rom.uw_palettes.classic",
|
||||
"friendlyName": "Classic",
|
||||
"description": "Produces results similar to the website.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"dizzy": {
|
||||
"dizzy": {
|
||||
"keyString": "rom.uw_palettes.dizzy",
|
||||
"friendlyName": "Dizzy",
|
||||
"description": "No logic in colors but saturation and lightness are conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"sick": {
|
||||
"sick": {
|
||||
"keyString": "rom.uw_palettes.sick",
|
||||
"friendlyName": "Sick",
|
||||
"description": "No logic in colors but lightness is conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"puke": {
|
||||
"puke": {
|
||||
"keyString": "rom.uw_palettes.puke",
|
||||
"friendlyName": "Puke",
|
||||
"description": "No logic at all.",
|
||||
@@ -1918,7 +1956,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"hud_palettes": {
|
||||
"hud_palettes": {
|
||||
"keyString": "rom.hud_palettes",
|
||||
"friendlyName": "HUD Palettes",
|
||||
"description": "Randomize the colors of the HUD (user interface), within reason.",
|
||||
@@ -1942,37 +1980,37 @@
|
||||
"description": "Never use this. Makes all HUD palette colors black.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"grayscale": {
|
||||
"grayscale": {
|
||||
"keyString": "rom.hud_palettes.grayscale",
|
||||
"friendlyName": "Grayscale",
|
||||
"description": "Removes all saturation of colors.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"negative": {
|
||||
"negative": {
|
||||
"keyString": "rom.hud_palettes.negative",
|
||||
"friendlyName": "Negative",
|
||||
"description": "Invert all colors",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"classic": {
|
||||
"classic": {
|
||||
"keyString": "rom.hud_palettes.classic",
|
||||
"friendlyName": "Classic",
|
||||
"description": "Produces results similar to the website.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"dizzy": {
|
||||
"dizzy": {
|
||||
"keyString": "rom.hud_palettes.dizzy",
|
||||
"friendlyName": "Dizzy",
|
||||
"description": "No logic in colors but saturation and lightness are conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"sick": {
|
||||
"sick": {
|
||||
"keyString": "rom.hud_palettes.sick",
|
||||
"friendlyName": "Sick",
|
||||
"description": "No logic in colors but lightness is conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"puke": {
|
||||
"puke": {
|
||||
"keyString": "rom.hud_palettes.puke",
|
||||
"friendlyName": "Puke",
|
||||
"description": "No logic at all.",
|
||||
@@ -1980,7 +2018,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shield_palettes": {
|
||||
"shield_palettes": {
|
||||
"keyString": "rom.shield_palettes",
|
||||
"friendlyName": "Shield Palettes",
|
||||
"description": "Randomize the colors of the shield, within reason.",
|
||||
@@ -2004,37 +2042,37 @@
|
||||
"description": "Never use this. Makes all shield palette colors black.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"grayscale": {
|
||||
"grayscale": {
|
||||
"keyString": "rom.shield_palettes.grayscale",
|
||||
"friendlyName": "Grayscale",
|
||||
"description": "Removes all saturation of colors.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"negative": {
|
||||
"negative": {
|
||||
"keyString": "rom.shield_palettes.negative",
|
||||
"friendlyName": "Negative",
|
||||
"description": "Invert all colors",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"classic": {
|
||||
"classic": {
|
||||
"keyString": "rom.shield_palettes.classic",
|
||||
"friendlyName": "Classic",
|
||||
"description": "Produces results similar to the website.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"dizzy": {
|
||||
"dizzy": {
|
||||
"keyString": "rom.shield_palettes.dizzy",
|
||||
"friendlyName": "Dizzy",
|
||||
"description": "No logic in colors but saturation and lightness are conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"sick": {
|
||||
"sick": {
|
||||
"keyString": "rom.shield_palettes.sick",
|
||||
"friendlyName": "Sick",
|
||||
"description": "No logic in colors but lightness is conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"puke": {
|
||||
"puke": {
|
||||
"keyString": "rom.shield_palettes.puke",
|
||||
"friendlyName": "Puke",
|
||||
"description": "No logic at all.",
|
||||
@@ -2042,7 +2080,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sword_palettes": {
|
||||
"sword_palettes": {
|
||||
"keyString": "rom.sword_palettes",
|
||||
"friendlyName": "Sword Palettes",
|
||||
"description": "Randomize the colors of the sword, within reason.",
|
||||
@@ -2066,37 +2104,37 @@
|
||||
"description": "Never use this. Makes all sword palette colors black.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"grayscale": {
|
||||
"grayscale": {
|
||||
"keyString": "rom.sword_palettes.grayscale",
|
||||
"friendlyName": "Grayscale",
|
||||
"description": "Removes all saturation of colors.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"negative": {
|
||||
"negative": {
|
||||
"keyString": "rom.sword_palettes.negative",
|
||||
"friendlyName": "Negative",
|
||||
"description": "Invert all colors",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"classic": {
|
||||
"classic": {
|
||||
"keyString": "rom.sword_palettes.classic",
|
||||
"friendlyName": "Classic",
|
||||
"description": "Produces results similar to the website.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"dizzy": {
|
||||
"dizzy": {
|
||||
"keyString": "rom.sword_palettes.dizzy",
|
||||
"friendlyName": "Dizzy",
|
||||
"description": "No logic in colors but saturation and lightness are conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"sick": {
|
||||
"sick": {
|
||||
"keyString": "rom.sword_palettes.sick",
|
||||
"friendlyName": "Sick",
|
||||
"description": "No logic in colors but lightness is conserved.",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"puke": {
|
||||
"puke": {
|
||||
"keyString": "rom.sword_palettes.puke",
|
||||
"friendlyName": "Puke",
|
||||
"description": "No logic at all.",
|
||||
@@ -2105,4 +2143,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,12 +226,19 @@ pot_shuffle:
|
||||
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
|
||||
'off': 50 # Default pot item locations
|
||||
### End of Enemizer Section ###
|
||||
beemizer: # Remove items from the global item pool and replace them with single bees and bee traps
|
||||
0: 50 # No bee traps are placed
|
||||
1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
|
||||
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
|
||||
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
|
||||
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
|
||||
# can add weights for any whole number between 0 and 100
|
||||
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
|
||||
0: 50 # No junk fill items are replaced (Beemizer is off)
|
||||
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
|
||||
beemizer_trap_chance:
|
||||
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
|
||||
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
|
||||
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
|
||||
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
|
||||
100: 0 # All beemizer replacements are traps
|
||||
### Shop Settings ###
|
||||
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
|
||||
@@ -25,6 +25,21 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#generate-game-form{
|
||||
#generate-game-form-wrapper table td{
|
||||
text-align: left;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
#generate-form-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#file-input{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.interactive{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ html{
|
||||
width: 200px;
|
||||
height: calc(156px - 40px);
|
||||
padding-top: 40px;
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mid-left-button{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#tutorial-wrapper{
|
||||
.markdown{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70rem;
|
||||
@@ -10,20 +10,20 @@
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#tutorial-wrapper img{
|
||||
.markdown img{
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#tutorial-wrapper p{
|
||||
.markdown p{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#tutorial-wrapper a{
|
||||
.markdown a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h1{
|
||||
.markdown h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
@@ -34,7 +34,7 @@
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h2{
|
||||
.markdown h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
@@ -46,7 +46,7 @@
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h3{
|
||||
.markdown h3{
|
||||
font-size: 1.70rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
@@ -55,44 +55,50 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h4{
|
||||
.markdown h4{
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h5{
|
||||
.markdown h5{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h6{
|
||||
.markdown h6{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h3, #tutorial-wrapper h4, #tutorial-wrapper h5,#tutorial-wrapper h6{
|
||||
.markdown h3, .markdown h4, .markdown h5,.markdown h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#tutorial-wrapper ul{
|
||||
.markdown h4, .markdown h5,.markdown h6{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown ul{
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown ol{
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown li{
|
||||
|
||||
}
|
||||
|
||||
#tutorial-wrapper ol{
|
||||
|
||||
}
|
||||
|
||||
#tutorial-wrapper li{
|
||||
|
||||
}
|
||||
|
||||
#tutorial-wrapper pre{
|
||||
.markdown pre{
|
||||
margin-top: 0;
|
||||
padding: 0.5rem 0.25rem;
|
||||
background-color: #ffeeab;
|
||||
@@ -101,7 +107,7 @@
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#tutorial-wrapper code{
|
||||
.markdown code{
|
||||
background-color: #ffeeab;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
@@ -109,12 +115,30 @@
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#tutorial-wrapper #tutorial-video-container{
|
||||
.markdown #tutorial-video-container{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#tutorial-wrapper #language-selector-wrapper{
|
||||
.markdown #language-selector-wrapper{
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.markdown table{
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown table th{
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
border: 1px solid #eeffeb;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.markdown table td{
|
||||
text-align: left;
|
||||
border: 1px solid #eeffeb;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
136
WebHostLib/static/styles/ootTracker.css
Normal file
136
WebHostLib/static/styles/ootTracker.css
Normal file
@@ -0,0 +1,136 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 448px;
|
||||
background-color: rgb(60, 114, 157);
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
bottom: 0px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 448px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: rgb(60, 114, 157);
|
||||
padding: 0 3px 3px;
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-align {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table td:first-child {
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.location-category td:first-child {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#lullaby{
|
||||
filter: sepia(100%) hue-rotate(-60deg); /* css trick to hue-shift a static image */
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#epona{
|
||||
filter: sepia(100%) hue-rotate(-20deg) saturate(250%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#saria{
|
||||
filter: sepia(100%) hue-rotate(60deg) saturate(150%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#sun{
|
||||
filter: sepia(100%) hue-rotate(15deg) saturate(200%) brightness(120%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#time{
|
||||
filter: sepia(100%) hue-rotate(160deg) saturate(150%);
|
||||
}
|
||||
12
WebHostLib/static/styles/startPlaying.css
Normal file
12
WebHostLib/static/styles/startPlaying.css
Normal file
@@ -0,0 +1,12 @@
|
||||
#start-playing-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#start-playing{
|
||||
width: 700px;
|
||||
min-height: 240px;
|
||||
text-align: center;
|
||||
}
|
||||
101
WebHostLib/static/styles/timespinnerTracker.css
Normal file
101
WebHostLib/static/styles/timespinnerTracker.css
Normal file
@@ -0,0 +1,101 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 384px;
|
||||
background-color: #8d60a7;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-weight: bold;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 384px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #8d60a7;
|
||||
padding: 0 3px 3px;
|
||||
font-size: 14px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
#weighted-settings{
|
||||
width: 60rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#user-warning, #weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#weighted-settings .instructions{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper .setting-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper .setting-wrapper .title-span{
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper{
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker{
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker #sprite-picker-sprites{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* Center tooltip text for sprite images */
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper::after{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper img{
|
||||
width: 32px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set{
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-name{
|
||||
width: 150px;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-name .delete-button{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-value{
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-value input[type=range]{
|
||||
width: 90%;
|
||||
min-width: 300px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
17
WebHostLib/templates/faq.html
Normal file
17
WebHostLib/templates/faq.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Frequently Asked Questions</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.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="{{ url_for('static', filename="assets/faq.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="faq-wrapper" data-lang="{{ lang }}" class="markdown">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
WebHostLib/templates/gameInfo.html
Normal file
17
WebHostLib/templates/gameInfo.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Info</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.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="{{ url_for('static', filename="assets/gameInfo.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="game-info" class="markdown" data-lang="{{ lang }}" data-game="{{ game }}">
|
||||
<!-- Populated my JS / MD -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>A Link to the Past</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/zelda3.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="zelda3">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Factorio</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/factorio/factorio.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="factorio">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Minecraft</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/minecraft/minecraft.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="minecraft">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Subnautica</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/subnautica/subnautica.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="subnautica">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -11,34 +11,67 @@
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="generate-game-wrapper">
|
||||
<div id="generate-game" class="grass-island">
|
||||
<h1>Upload Config{% if race %} (Race Mode){% endif %}</h1>
|
||||
<h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
|
||||
<p>
|
||||
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
|
||||
If you do not have a config (yaml) file yet, you may create one on the
|
||||
<a href="/player-settings">Player Settings</a> page. If you would like more advanced options,
|
||||
the <a href="/weighted-settings">Weighted Settings</a> page might be what you're looking for.
|
||||
This page allows you to generate a game by uploading a config file or a zip file containing config
|
||||
files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page,
|
||||
which you can find via the <a href="{{ url_for("games") }}">supported games list</a>.
|
||||
</p>
|
||||
<p>
|
||||
{% if race -%}
|
||||
This game will be generated in race mode, meaning the spoiler log will be unavailable,
|
||||
roms will be encrypted, and single-player games will have no multidata files.
|
||||
This game will be generated in race mode,
|
||||
meaning the spoiler log will be unavailable and game specific protections will be in place,
|
||||
like ROM encryption or cheat mode removal.
|
||||
{%- else -%}
|
||||
If you would like to generate a race game,
|
||||
<a href="{{ url_for("generate", race=True) }}">click here.</a> Race games are generated without
|
||||
a spoiler log, the ROMs are encrypted, and single-player games will not include a multidata file.
|
||||
<a href="{{ url_for("generate", race=True) }}">click here.</a><br />
|
||||
Race games are generated without a spoiler log and game specific protections will be in place,
|
||||
like ROM encryption or cheat mode removal.
|
||||
{%- endif -%}
|
||||
</p>
|
||||
<p>
|
||||
After generation is complete, you will have the option to download a patch file.
|
||||
This patch file can be opened with the
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
|
||||
used to to create a rom file. In-browser patching is planned for the future.
|
||||
</p>
|
||||
<div id="generate-game-form-wrapper">
|
||||
<form id="generate-game-form" method="post" enctype="multipart/form-data">
|
||||
<input id="file-input" type="file" name="file">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
|
||||
<td>
|
||||
<select name="forfeit_mode" id="forfeit_mode">
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !forfeit after goal completion</option>
|
||||
<option value="auto-enabled">Automatic on goal completion and manual !forfeit</option>
|
||||
<option value="enabled">Manual !forfeit</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<label for="hint_cost"> Hint Cost:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||
to get the location of that hint item.">(?)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<select name="hint_cost" id="hint_cost">
|
||||
{% for n in range(0, 110, 5) %}
|
||||
<option {% if n == 10 %}selected="selected" {% endif %} value="{{ n }}">
|
||||
{% if n > 100 %}Off{% else %}{{ n }}%{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="generate-form-button-row">
|
||||
<input id="file-input" type="file" name="file">
|
||||
</div>
|
||||
</form>
|
||||
<button id="generate-game-button">Upload</button>
|
||||
<button id="generate-game-button">Upload File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Amount</th>
|
||||
<th>Order Received</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -28,6 +29,7 @@
|
||||
<tr>
|
||||
<td>{{ name | item_name }}</td>
|
||||
<td>{{ count }}</td>
|
||||
<td>{{received_items[name]}}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
<a href="/">archipelago</a>
|
||||
</div>
|
||||
<div id="base-header-right">
|
||||
<a href="/games">games</a>
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2">discord</a>
|
||||
<a href="/start-playing">start playing</a>
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,17 +15,17 @@
|
||||
<h1>Host Game</h1>
|
||||
<p>
|
||||
This page allows you to host a game which was not generated by the website. For example, if you have
|
||||
generated a doors game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide the tracker, and the ability for your players to download
|
||||
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||
their patch files.
|
||||
<br /><br />
|
||||
In addition to a zip file created by the generator, you may upload a multidata file here as well.
|
||||
In addition to the zip file created by the generator, you may upload a multidata file here as well.
|
||||
</p>
|
||||
<div id="host-game-form-wrapper">
|
||||
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
||||
<input id="file-input" type="file" name="file">
|
||||
</form>
|
||||
<button id="host-game-button">Upload</button>
|
||||
<button id="host-game-button">Upload File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<div id="landing-wrapper">
|
||||
<div id="landing-header">
|
||||
<h1>ARCHIPELAGO</h1>
|
||||
<h4>multiworld randomizer ecosystem</h4>
|
||||
<h4>multiworld multi-game randomizer</h4>
|
||||
</div>
|
||||
<div id="landing-links">
|
||||
<a href="/games" id="mid-button">start<br />playing</a>
|
||||
<a id="far-left-button"></a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
||||
<a href="/uploads" id="far-right-button">Host Game</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
||||
<a href="/start-playing" id="mid-button">start<br />playing</a>
|
||||
<a href="/games" id="far-left-button">supported<br />games</a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
||||
<a href="/faq/en/" id="mid-right-button">f.a.q.</a>
|
||||
</div>
|
||||
<div id="landing-clouds">
|
||||
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
|
||||
@@ -50,7 +50,7 @@
|
||||
</p>
|
||||
<p>
|
||||
<span class="variable">{{ seeds }}</span>
|
||||
games were created and
|
||||
games were generated and
|
||||
<span class="variable">{{ rooms }}</span>
|
||||
were hosted in the last 7 days.
|
||||
</p>
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% elif patch.game == "Ocarina of Time" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
APZ5 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>
|
||||
|
||||
180
WebHostLib/templates/ootTracker.html
Normal file
180
WebHostLib/templates/ootTracker.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
|
||||
<td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
|
||||
<td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
|
||||
<td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
|
||||
<td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
|
||||
<td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
|
||||
<td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
|
||||
<div class="item-count">{{ hookshot_length }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
|
||||
<td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
|
||||
<td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
|
||||
<td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
|
||||
<td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
|
||||
<td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
|
||||
<td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
|
||||
<td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
|
||||
<td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
|
||||
<td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
|
||||
<div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
|
||||
<td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
|
||||
<td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
|
||||
<div class="item-count">{{ wallet_size }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
|
||||
<td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
|
||||
<div class="item-count">Zelda</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
|
||||
<div class="item-count">Epona</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
|
||||
<div class="item-count">Saria</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
|
||||
<div class="item-count">Sun</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
|
||||
<div class="item-count">Time</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
|
||||
<div class="item-count">Storms</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
|
||||
<div class="item-count">{{ token_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
|
||||
<div class="item-count">Min</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
|
||||
<div class="item-count">Bol</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
|
||||
<div class="item-count">Ser</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
|
||||
<div class="item-count">Req</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
|
||||
<div class="item-count">Noc</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
|
||||
<div class="item-count">Pre</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
|
||||
<div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
|
||||
<td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
|
||||
<td class="right-align">Items</td>
|
||||
</tr>
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
|
||||
<td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,7 +18,8 @@
|
||||
# http://www.yamllint.com/
|
||||
|
||||
description: Default {{ game }} Template # Used to describe your yaml. Useful if you have multiple files
|
||||
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
name: YourName{number}
|
||||
#{player} will be replaced with the player's slot number.
|
||||
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
|
||||
#{number} will be replaced with the counter value of the name.
|
||||
@@ -35,37 +36,25 @@ accessibility:
|
||||
progression_balancing:
|
||||
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
# The following 4 options can be uncommented and moved into a game's section they should affect
|
||||
# start_inventory: # Begin the file with the listed items/upgrades
|
||||
# Please only use items for the correct game, use triggers if need to be have seperated lists.
|
||||
# Pegasus Boots: on
|
||||
# Bomb Upgrade (+10): 4
|
||||
# Arrow Upgrade (+10): 4
|
||||
# start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client.
|
||||
# - Moon Pearl
|
||||
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
|
||||
# - "Moon Pearl"
|
||||
# - "Small Keys"
|
||||
# - "Big Keys"
|
||||
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
|
||||
# - "Progressive Weapons"
|
||||
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
|
||||
# - "Master Sword Pedestal"
|
||||
{{ game }}:
|
||||
{%- for option_name, option in options.items() %}
|
||||
{{ option_name }}:{% if option.__doc__ %} # {{ option.__doc__ }}{% endif %}
|
||||
{%- if option.range_start is defined %}
|
||||
|
||||
{%- macro range_option(option) %}
|
||||
# you can add additional values between minimum and maximum
|
||||
{{ option.range_start }}: 0 # minimum value
|
||||
{{ option.range_end }}: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
{%- set data, notes = dictify_range(option) %}
|
||||
{%- for entry, default in data.items() %}
|
||||
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
|
||||
{%- endfor -%}
|
||||
{% endmacro %}
|
||||
{{ game }}:
|
||||
{%- for option_key, option in options.items() %}
|
||||
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
|
||||
{%- if option.range_start is defined and option.range_start is number %}
|
||||
{{- range_option(option) -}}
|
||||
{%- elif option.options -%}
|
||||
{%- for sub_option_name, suboption_option_id in option.options.items() %}
|
||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
||||
{%- endfor -%}
|
||||
{%- else %}
|
||||
{{ yaml_dump(option.default) | indent(4, first=False) }}
|
||||
{{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
{% if not options %}{}{% endif %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<title>{{ game }} Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script>
|
||||
{% endblock %}
|
||||
@@ -14,11 +15,14 @@
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||
page.</p>
|
||||
or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
|
||||
<p>
|
||||
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
||||
<br />
|
||||
Advanced users can download a template file for this game
|
||||
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
|
||||
</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
|
||||
32
WebHostLib/templates/startPlaying.html
Normal file
32
WebHostLib/templates/startPlaying.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Start Playing</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/startPlaying.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
|
||||
<div id="start-playing-wrapper">
|
||||
<div id="start-playing" class="grass-island {% if rooms %}wider{% endif %}">
|
||||
<h1>Start Playing</h1>
|
||||
<p>
|
||||
If you're ready to start playing but don't know where to begin, check out the
|
||||
<a href="/tutorial">tutorials</a> page. It has all the resources you need to create a config file
|
||||
and get started. If you already have a config file, or a zip file containing them, read on.
|
||||
<br /><br />
|
||||
|
||||
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||
You'll need to upload either a config file or a zip file containing one more more config files.
|
||||
<br /><br />
|
||||
|
||||
If you have already generated a game and just need to host it, this site can<br />
|
||||
<a href="uploads">host a pre-generated game</a> for you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
@@ -9,9 +9,13 @@
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="games">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game, (display_name, description) in games_list.items() %}
|
||||
<h3><a href="{{ url_for("game_page", game=game) }}">{{ display_name}}</a></h3>
|
||||
<p>{{ description}}</p>
|
||||
{% for game, description in worlds.items() %}
|
||||
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
||||
<p>
|
||||
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
||||
<br />
|
||||
{{ description }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
WebHostLib/templates/templates.html
Normal file
21
WebHostLib/templates/templates.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Option Templates (YAML)</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="markdown">
|
||||
<h1>Option Templates (YAML)</h1>
|
||||
<ul>
|
||||
{% for file in files %}
|
||||
<li><a href="{{ url_for('static', filename="generated/"+file+".yaml") }}">{{ file }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
82
WebHostLib/templates/timespinnerTracker.html
Normal file
82
WebHostLib/templates/timespinnerTracker.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/timespinnerTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/timespinnerTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ icons['Timespinner Wheel'] }}" class="{{ 'acquired' if 'Timespinner Wheel' in acquired_items }}" title="Timespinner Wheel" /></td>
|
||||
<td><img src="{{ icons['Timespinner Spindle'] }}" class="{{ 'acquired' if 'Timespinner Spindle' in acquired_items }}" title="Timespinner Spindle" /></td>
|
||||
<td><img src="{{ icons['Timespinner Gear 1'] }}" class="{{ 'acquired' if 'Timespinner Gear 1' in acquired_items }}" title="Timespinner Gear 1" /></td>
|
||||
<td><img src="{{ icons['Timespinner Gear 2'] }}" class="{{ 'acquired' if 'Timespinner Gear 2' in acquired_items }}" title="Timespinner Gear 2" /></td>
|
||||
<td><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items }}" title="Talaria Attachment" /></td>
|
||||
<td><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></td>
|
||||
<td><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></td>
|
||||
<td><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></td>
|
||||
<td><img src="{{ icons['Twin Pyramid Key'] }}" class="{{ 'acquired' if 'Twin Pyramid Key' in acquired_items }}" title="Twin Pyramid Key" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Security Keycard A'] }}" class="{{ 'acquired' if 'Security Keycard A' in acquired_items }}" title="Security Keycard A" /></td>
|
||||
<td><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></td>
|
||||
<td><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></td>
|
||||
<td><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></td>
|
||||
<td><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></td>
|
||||
<td><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></td>
|
||||
<td><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></td>
|
||||
<td><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></td>
|
||||
<td><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% if 'Fire Orb' in acquired_items %}
|
||||
<td><img src="{{ icons['Fire Orb'] }}" class="acquired" title="Fire Orb" /></td>
|
||||
{% elif 'Infernal Flames' in acquired_items %}
|
||||
<td><img src="{{ icons['Infernal Flames'] }}" class="acquired" title="Infernal Flames" /></td>
|
||||
{% elif 'Pyro Ring' in acquired_items %}
|
||||
<td><img src="{{ icons['Pyro Ring'] }}" class="acquired" title="Pyro Ring" /></td>
|
||||
{% elif 'Pyro Ring' in acquired_items %}
|
||||
<td><img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" /></td>
|
||||
{% else %}
|
||||
<td><img src="{{ icons['Fire Orb'] }}" title="Fire Orb" /></td>
|
||||
{% endif %}
|
||||
|
||||
{% if 'Plasma Orb' in acquired_items %}
|
||||
<td><img src="{{ icons['Plasma Orb'] }}" class="acquired" title="Plasma Orb" /></td>
|
||||
{% elif 'Plasma Geyser' in acquired_items %}
|
||||
<td><img src="{{ icons['Plasma Geyser'] }}" class="acquired" title="Plasma Geyser" /></td>
|
||||
{% elif 'Royal Ring' in acquired_items %}
|
||||
<td><img src="{{ icons['Royal Ring'] }}" class="acquired" title="Royal Ring" /></td>
|
||||
{% else %}
|
||||
<td><img src="{{ icons['Plasma Orb'] }}" title="Plasma Orb" /></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Archipelago</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorial.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
@@ -11,7 +11,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="tutorial-wrapper" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
|
||||
<div id="tutorial-wrapper" class="markdown" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
<div id="view-seed-wrapper">
|
||||
<div id="view-seed" class="grass-island">
|
||||
<h1>Seed Info</h1>
|
||||
{% if not seed.multidata and not seed.spoiler %}
|
||||
<p>Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms
|
||||
cannot be created.</p>
|
||||
{% endif %}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -32,46 +28,16 @@
|
||||
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if seed.multidata %}
|
||||
<tr>
|
||||
<td>Players: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% 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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
{% call macros.list_rooms(rooms) %}
|
||||
<li>
|
||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||
</li>
|
||||
{% endcall %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>Files: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for slot in seed.slots %}
|
||||
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
{% call macros.list_rooms(rooms) %}
|
||||
<li>
|
||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=slot.player_id) }}">Player {{ slot.player_name }}</a>
|
||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||
</li>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endcall %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Player Settings</title>
|
||||
<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="{{ 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
|
||||
you can access using the dropdown menu below. These settings will be usable when generating a
|
||||
single player game, or you can export them to a <code>.yaml</code> file and use them in a multiworld.
|
||||
If you already have a settings file you would like to validate, you may do so on the
|
||||
<a href="/mysterycheck">verification page</a>.
|
||||
</div>
|
||||
|
||||
<div id="settings-wrapper">
|
||||
<div class="setting-wrapper">
|
||||
Choose a preset and optionally assign it a nickname, which will be used as the file's description if
|
||||
you download it.
|
||||
<table class="option-set">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="preset-number">Preset Number:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<select id="preset-number">
|
||||
<option value="1">Preset 1</option>
|
||||
<option value="2">Preset 2</option>
|
||||
<option value="3">Preset 3</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="description">Preset Name:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<input id="description" class="setting" data-setting="description" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Choose a name you want to represent you in-game. This will appear when you send items
|
||||
to other people in multiworld games.
|
||||
<table class="option-set">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="name">Player Name:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<input id="name" maxlength="16" class="setting" data-setting="name" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="reset-to-default">Reset to Defaults</button>
|
||||
<button id="export-button">Export Settings</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,5 @@
|
||||
import collections
|
||||
from typing import Counter, Optional, Dict, Any, Tuple
|
||||
|
||||
from flask import render_template
|
||||
from werkzeug.exceptions import abort
|
||||
@@ -10,14 +11,7 @@ from WebHostLib import app, cache, Room
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
|
||||
def get_alttp_id(item_name):
|
||||
return Items.item_table[item_name][2]
|
||||
|
||||
|
||||
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 = {
|
||||
alttp_icons = {
|
||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
@@ -32,101 +26,61 @@ icons = {
|
||||
"Red Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove":
|
||||
r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers":
|
||||
r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
|
||||
"Fire Rod":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Small Key":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
|
||||
"Light World":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace":
|
||||
r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace":
|
||||
r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods":
|
||||
r"https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74"
|
||||
"Progressive Sword": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
|
||||
"Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74"
|
||||
}
|
||||
|
||||
def get_alttp_id(item_name):
|
||||
return Items.item_table[item_name][2]
|
||||
|
||||
|
||||
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)
|
||||
|
||||
links = {"Bow": "Progressive Bow",
|
||||
"Silver Arrows": "Progressive Bow",
|
||||
"Silver Bow": "Progressive Bow",
|
||||
@@ -211,12 +165,6 @@ key_only_locations = {
|
||||
'Total': set()
|
||||
}
|
||||
|
||||
key_locations = {"Desert Palace", "Eastern Palace", "Hyrule Castle", "Agahnims Tower", "Tower of Hera", "Swamp Palace",
|
||||
"Thieves Town", "Skull Woods", "Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness",
|
||||
"Ganons Tower"}
|
||||
|
||||
big_key_locations = {"Desert Palace", "Eastern Palace", "Tower of Hera", "Swamp Palace", "Thieves Town", "Skull Woods",
|
||||
"Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness", "Ganons Tower"}
|
||||
location_to_area = {}
|
||||
for area, locations in default_locations.items():
|
||||
for location in locations:
|
||||
@@ -283,6 +231,7 @@ def render_timedelta(delta: datetime.timedelta):
|
||||
|
||||
_multidata_cache = {}
|
||||
|
||||
|
||||
def get_location_table(checks_table: dict) -> dict:
|
||||
loc_to_area = {}
|
||||
for area, locations in checks_table.items():
|
||||
@@ -292,14 +241,15 @@ def get_location_table(checks_table: dict) -> dict:
|
||||
loc_to_area[location] = area
|
||||
return loc_to_area
|
||||
|
||||
|
||||
def get_static_room_data(room: Room):
|
||||
result = _multidata_cache.get(room.seed.id, None)
|
||||
if result:
|
||||
return result
|
||||
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 = multidata['locations']
|
||||
names = multidata["names"]
|
||||
locations: Dict[int, Dict[int, Tuple[int, int]]] = multidata['locations']
|
||||
names: Dict[int, Dict[int, str]] = multidata["names"]
|
||||
seed_checks_in_area = checks_in_area.copy()
|
||||
|
||||
use_door_tracker = False
|
||||
@@ -311,23 +261,14 @@ def get_static_room_data(room: Room):
|
||||
seed_checks_in_area["Total"] = 249
|
||||
|
||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||
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 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, multidata["precollected_items"], \
|
||||
multidata["precollected_items"], \
|
||||
multidata["games"]
|
||||
_multidata_cache[room.seed.id] = result
|
||||
return result
|
||||
@@ -340,13 +281,13 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
if tracked_team < 0 or tracked_player < 1:
|
||||
abort(404)
|
||||
|
||||
room = Room.get(tracker=tracker)
|
||||
room: Optional[Room] = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
|
||||
# Collect seed information and pare it down to a single player
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
player_big_key_locations, player_small_key_locations, precollected_items, games = get_static_room_data(room)
|
||||
precollected_items, games = get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
inventory = collections.Counter()
|
||||
@@ -359,13 +300,12 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
attribute_item_solo(inventory, item_id)
|
||||
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
multisave: Dict[str, Any] = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
multisave: Dict[str, Any] = {}
|
||||
|
||||
# Add items to player inventory
|
||||
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:
|
||||
@@ -373,168 +313,480 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
for location in locations_checked:
|
||||
if location in player_locations:
|
||||
item, recipient = player_locations[location]
|
||||
if recipient == tracked_player: # a check done for the tracked 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
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
|
||||
if games[tracked_player] == "A Link to the Past":
|
||||
# Note the presence of the triforce item
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
if game_state == 30:
|
||||
inventory[106] = 1 # Triforce
|
||||
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
"Progressive Sword": 94,
|
||||
"Progressive Glove": 97,
|
||||
"Progressive Bow": 100,
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
}
|
||||
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level+1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name+"_acquired"] = acquired
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
|
||||
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
|
||||
return render_template("lttpTracker.html", inventory=inventory,
|
||||
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
|
||||
checks_in_area=seed_checks_in_area[tracked_player], acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
**display_data)
|
||||
elif games[tracked_player] == "Minecraft":
|
||||
minecraft_icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fa/Brewing_Stand.png",
|
||||
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/dc/Red_Bed_JE4_BE3.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
|
||||
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
|
||||
42036, 42057, 42063, 42053, 42083, 42084, 42091]
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Tools": 45013,
|
||||
"Progressive Weapons": 45012,
|
||||
"Progressive Armor": 45014,
|
||||
"Progressive Resource Crafting": 45001
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = minecraft_icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"3 Ender Pearls": 45029,
|
||||
"8 Netherite Scrap": 45015
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name+"_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("minecraftTracker.html",
|
||||
inventory=inventory, icons=minecraft_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name},
|
||||
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
return __renderAlttpTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, \
|
||||
seed_checks_in_area, checks_done)
|
||||
elif games[tracked_player] == "Minecraft":
|
||||
return __renderMinecraftTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
|
||||
elif games[tracked_player] == "Ocarina of Time":
|
||||
return __renderOoTTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
|
||||
elif games[tracked_player] == "Timespinner":
|
||||
return __renderTimespinnerTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
|
||||
else:
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
return render_template("genericTracker.html",
|
||||
inventory=inventory,
|
||||
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
|
||||
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
|
||||
|
||||
|
||||
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
|
||||
|
||||
# Note the presence of the triforce item
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
if game_state == 30:
|
||||
inventory[106] = 1 # Triforce
|
||||
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
"Progressive Sword": 94,
|
||||
"Progressive Glove": 97,
|
||||
"Progressive Bow": 100,
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
}
|
||||
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level + 1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name + "_acquired"] = acquired
|
||||
display_data[base_name + "_url"] = alttp_icons[display_name]
|
||||
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
|
||||
player_big_key_locations = set()
|
||||
player_small_key_locations = set()
|
||||
for loc_data in locations.values():
|
||||
for item_id, item_player in loc_data.values():
|
||||
if item_player == player:
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations.add(ids_big_key[item_id])
|
||||
elif item_id in ids_small_key:
|
||||
player_small_key_locations.add(ids_small_key[item_id])
|
||||
|
||||
return render_template("lttpTracker.html", inventory=inventory,
|
||||
player_name=playerName, room=room, icons=alttp_icons, checks_done=checks_done,
|
||||
checks_in_area=seed_checks_in_area[player],
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations,
|
||||
big_key_locations=player_big_key_locations,
|
||||
**display_data)
|
||||
|
||||
|
||||
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str) -> str:
|
||||
|
||||
icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fa/Brewing_Stand.png",
|
||||
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/dc/Red_Bed_JE4_BE3.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
|
||||
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
|
||||
42036, 42057, 42063, 42053, 42083, 42084, 42091]
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Tools": 45013,
|
||||
"Progressive Weapons": 45012,
|
||||
"Progressive Armor": 45014,
|
||||
"Progressive Resource Crafting": 45001
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
display_data[base_name + "_url"] = icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"3 Ender Pearls": 45029,
|
||||
"8 Netherite Scrap": 45015
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name + "_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("minecraftTracker.html",
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
|
||||
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str) -> str:
|
||||
|
||||
icons = {
|
||||
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
||||
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
|
||||
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
|
||||
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
|
||||
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
|
||||
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
|
||||
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
|
||||
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
|
||||
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
|
||||
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
|
||||
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
|
||||
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
|
||||
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
|
||||
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
|
||||
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
|
||||
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
|
||||
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
|
||||
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
|
||||
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
|
||||
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
|
||||
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
|
||||
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
|
||||
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
|
||||
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
|
||||
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
|
||||
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
|
||||
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
|
||||
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
|
||||
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
|
||||
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
|
||||
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
|
||||
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
|
||||
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
|
||||
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
|
||||
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
|
||||
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
|
||||
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
|
||||
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
|
||||
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
|
||||
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
|
||||
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
|
||||
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
|
||||
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
|
||||
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
|
||||
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Hookshot": 66128,
|
||||
"Progressive Strength Upgrade": 66129,
|
||||
"Progressive Wallet": 66133,
|
||||
"Progressive Scale": 66134,
|
||||
"Magic Meter": 66138,
|
||||
"Ocarina": 66139,
|
||||
}
|
||||
|
||||
progressive_names = {
|
||||
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
|
||||
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
|
||||
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
|
||||
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
|
||||
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
|
||||
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
|
||||
}
|
||||
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
if item_name.startswith("Progressive"):
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
else:
|
||||
base_name = item_name.lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
|
||||
if base_name == "hookshot":
|
||||
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
||||
if base_name == "wallet":
|
||||
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
||||
|
||||
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
||||
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
|
||||
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
|
||||
display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle']
|
||||
|
||||
# Determine bombchu display
|
||||
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"Gold Skulltula Token": 66091,
|
||||
"Triforce Piece": 66202,
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name+"_count"] = inventory[item_id]
|
||||
|
||||
# Gather dungeon locations
|
||||
area_id_ranges = {
|
||||
"Overworld": (67000, 67280),
|
||||
"Deku Tree": (67281, 67303),
|
||||
"Dodongo's Cavern": (67304, 67334),
|
||||
"Jabu Jabu's Belly": (67335, 67359),
|
||||
"Bottom of the Well": (67360, 67384),
|
||||
"Forest Temple": (67385, 67420),
|
||||
"Fire Temple": (67421, 67457),
|
||||
"Water Temple": (67458, 67484),
|
||||
"Shadow Temple": (67485, 67532),
|
||||
"Spirit Temple": (67533, 67582),
|
||||
"Ice Cavern": (67583, 67596),
|
||||
"Gerudo Training Grounds": (67597, 67635),
|
||||
"Ganon's Castle": (67636, 67673),
|
||||
}
|
||||
def lookup_and_trim(id, area):
|
||||
full_name = lookup_any_location_id_to_name[id]
|
||||
if id == 67673:
|
||||
return full_name[13:] # Ganons Tower Boss Key Chest
|
||||
if area != 'Overworld':
|
||||
return full_name[len(area):] # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
|
||||
return full_name
|
||||
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
|
||||
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[player]}
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
|
||||
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[player]])
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done['Total'] = sum(checks_done.values())
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
# Give skulltulas on non-tracked locations
|
||||
non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player]))
|
||||
for id in non_tracked_locations:
|
||||
if "GS" in lookup_and_trim(id, ''):
|
||||
display_data["token_count"] += 1
|
||||
|
||||
# Gather small and boss key info
|
||||
small_key_counts = {
|
||||
"Forest Temple": inventory[66175],
|
||||
"Fire Temple": inventory[66176],
|
||||
"Water Temple": inventory[66177],
|
||||
"Spirit Temple": inventory[66178],
|
||||
"Shadow Temple": inventory[66179],
|
||||
"Bottom of the Well": inventory[66180],
|
||||
"Gerudo Training Grounds": inventory[66181],
|
||||
"Ganon's Castle": inventory[66183],
|
||||
}
|
||||
boss_key_counts = {
|
||||
"Forest Temple": '✔' if inventory[66149] else '✕',
|
||||
"Fire Temple": '✔' if inventory[66150] else '✕',
|
||||
"Water Temple": '✔' if inventory[66151] else '✕',
|
||||
"Spirit Temple": '✔' if inventory[66152] else '✕',
|
||||
"Shadow Temple": '✔' if inventory[66153] else '✕',
|
||||
"Ganon's Castle": '✔' if inventory[66154] else '✕',
|
||||
}
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
return render_template("ootTracker.html",
|
||||
inventory=inventory, player=player, team=team, room=room, player_name=playerName,
|
||||
icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
|
||||
**display_data)
|
||||
|
||||
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str) -> str:
|
||||
|
||||
icons = {
|
||||
"Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
|
||||
"Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png",
|
||||
"Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png",
|
||||
"Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png",
|
||||
"Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png",
|
||||
"Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png",
|
||||
"Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png",
|
||||
"Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png",
|
||||
"Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png",
|
||||
"Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png",
|
||||
"Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png",
|
||||
"Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png",
|
||||
"Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png",
|
||||
"Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png",
|
||||
"Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png",
|
||||
"Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png",
|
||||
"Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png",
|
||||
"Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png",
|
||||
"Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png",
|
||||
"Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png",
|
||||
"Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png",
|
||||
"Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png",
|
||||
"Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png",
|
||||
"Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png",
|
||||
"Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png",
|
||||
"Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png",
|
||||
"Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
|
||||
}
|
||||
|
||||
timespinner_location_ids = {
|
||||
"Present": [
|
||||
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
|
||||
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
|
||||
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
|
||||
1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039,
|
||||
1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049,
|
||||
1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059,
|
||||
1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069,
|
||||
1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079,
|
||||
1337080, 1337081, 1337082, 1337083, 1337084, 1337085, 1337156, 1337157, 1337159,
|
||||
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
||||
1337170],
|
||||
"Past": [
|
||||
1337086, 1337087, 1337088, 1337089,
|
||||
1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099,
|
||||
1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109,
|
||||
1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119,
|
||||
1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129,
|
||||
1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139,
|
||||
1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149,
|
||||
1337150, 1337151, 1337152, 1337153, 1337154, 1337155],
|
||||
"Ancient Pyramid": [1337246, 1337247, 1337248, 1337249]
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in timespinner_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in timespinner_location_ids.items()}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("timespinnerTracker.html",
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
|
||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str) -> str:
|
||||
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||
player_received_items = {}
|
||||
|
||||
for order_index, networkItem in enumerate(
|
||||
multisave.get('received_items', {}).get((team, player), []),
|
||||
start=1
|
||||
):
|
||||
player_received_items[networkItem.item] = order_index
|
||||
|
||||
return render_template("genericTracker.html",
|
||||
inventory=inventory,
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checked_locations=checked_locations,
|
||||
not_checked_locations=set(locations[player]) - checked_locations,
|
||||
received_items=player_received_items)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def getTracker(tracker: UUID):
|
||||
room = Room.get(tracker=tracker)
|
||||
room: 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, precollected_items, games = get_static_room_data(room)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
precollected_items, games = get_static_room_data(room)
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
@@ -571,6 +823,15 @@ def getTracker(tracker: UUID):
|
||||
if game_state == 30:
|
||||
inventory[team][player][106] = 1 # Triforce
|
||||
|
||||
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 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])
|
||||
|
||||
group_big_key_locations = set()
|
||||
group_key_locations = set()
|
||||
for player in range(1, len(names[0]) + 1):
|
||||
@@ -597,9 +858,9 @@ def getTracker(tracker: UUID):
|
||||
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
|
||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||
checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
|
||||
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
|
||||
video=video, big_key_locations=group_big_key_locations,
|
||||
hints=hints, long_player_names = long_player_names)
|
||||
hints=hints, long_player_names=long_player_names)
|
||||
|
||||
@@ -3,20 +3,82 @@ import lzma
|
||||
import json
|
||||
import base64
|
||||
import MultiServer
|
||||
import uuid
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from pony.orm import flush, select
|
||||
|
||||
from WebHostLib import app, Seed, Room, Slot
|
||||
from Utils import parse_yaml
|
||||
|
||||
accepted_zip_contents = {"patches": ".apbp",
|
||||
"spoiler": ".txt",
|
||||
"multidata": ".archipelago"}
|
||||
from Patch import preferred_endings
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
|
||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||
if not owner:
|
||||
owner = session["_id"]
|
||||
infolist = zfile.infolist()
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
multidata = None
|
||||
for file in infolist:
|
||||
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(tuple(preferred_endings.values())):
|
||||
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)"
|
||||
metadata = yaml_data["meta"]
|
||||
|
||||
slots.add(Slot(data=data,
|
||||
player_name=metadata["player_name"],
|
||||
player_id=metadata["player_id"],
|
||||
game=yaml_data["game"]))
|
||||
|
||||
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 need a specific name or they do not function
|
||||
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-", 3)
|
||||
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(".apz5"):
|
||||
# .apz5 must be named specifically since they don't contain any metadata
|
||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
||||
player_id=int(slot_id[1:]), game="Ocarina of Time"))
|
||||
|
||||
elif file.filename.endswith(".txt"):
|
||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||
elif file.filename.endswith(".archipelago"):
|
||||
try:
|
||||
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:
|
||||
flush() # commit slots
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
||||
id=sid if sid else uuid.uuid4())
|
||||
flush() # create seed
|
||||
for slot in slots:
|
||||
slot.seed = seed
|
||||
return seed
|
||||
else:
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
|
||||
|
||||
@app.route('/uploads', methods=['GET', 'POST'])
|
||||
def uploads():
|
||||
if request.method == 'POST':
|
||||
@@ -31,58 +93,12 @@ def uploads():
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
multidata = None
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
|
||||
for file in infolist:
|
||||
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"):
|
||||
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 = 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:
|
||||
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.")
|
||||
res = upload_zip_to_db(zfile)
|
||||
if type(res) == str:
|
||||
return res
|
||||
elif res:
|
||||
return redirect(url_for("viewSeed", seed=res.id))
|
||||
else:
|
||||
try:
|
||||
multidata = file.read()
|
||||
@@ -95,7 +111,7 @@ def uploads():
|
||||
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.")
|
||||
flash("Not recognized file format. Awaiting a .archipelago file or .zip containing one.")
|
||||
return render_template("hostGame.html")
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -22,4 +22,21 @@
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: dp(3)
|
||||
spacing: dp(3)
|
||||
<ServerLabel>:
|
||||
text: "Server:"
|
||||
size_hint_x: None
|
||||
<ContainerLayout>:
|
||||
size_hint_x: 1
|
||||
size_hint_y: 1
|
||||
pos: (0, 0)
|
||||
<ServerToolTip>:
|
||||
size: self.texture_size
|
||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||
halign: "left"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.2, 0.2, 0.2, 1
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user