Compare commits
253 Commits
tests_apwo
...
misc-webho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c06d1193a | ||
|
|
6b48f9aac5 | ||
|
|
bc11c9dfd4 | ||
|
|
24403eba1b | ||
|
|
e377068d1f | ||
|
|
c7c94eebeb | ||
|
|
9d38725688 | ||
|
|
18bf7425c4 | ||
|
|
17127a4117 | ||
|
|
5d9b47355e | ||
|
|
f9761ad4e5 | ||
|
|
485aa23afd | ||
|
|
e08deff6f9 | ||
|
|
d5d630dcf0 | ||
|
|
58b696e986 | ||
|
|
a3907e800b | ||
|
|
5c640c6c52 | ||
|
|
fe6096464c | ||
|
|
5bf3de45f4 | ||
|
|
f33babc420 | ||
|
|
1c9199761b | ||
|
|
368fa64914 | ||
|
|
e114ed5566 | ||
|
|
5d47c5b316 | ||
|
|
812dc413e5 | ||
|
|
4aed2be93b | ||
|
|
974bab2b24 | ||
|
|
98d61b32af | ||
|
|
4141a50d8c | ||
|
|
93c18cd9a7 | ||
|
|
5af47425b0 | ||
|
|
b41a1e69b4 | ||
|
|
124113f3d3 | ||
|
|
2b69820619 | ||
|
|
f147f9e5a0 | ||
|
|
db7c0c9db9 | ||
|
|
b40fba0840 | ||
|
|
ea799c494e | ||
|
|
b4b8426def | ||
|
|
39a50da55c | ||
|
|
9931605f94 | ||
|
|
8834ba88aa | ||
|
|
5e46967b7d | ||
|
|
638d6807db | ||
|
|
d471dcc067 | ||
|
|
4a27fae1ab | ||
|
|
794959e182 | ||
|
|
aff852fb45 | ||
|
|
a0eea3a650 | ||
|
|
0012584e51 | ||
|
|
6e02a4ca3c | ||
|
|
2ef05a1799 | ||
|
|
fa2891f785 | ||
|
|
d5d13a6d4d | ||
|
|
b24037e9d9 | ||
|
|
6d6de4a98e | ||
|
|
0e7c7bd1bf | ||
|
|
9312f14ffb | ||
|
|
ce8f07b347 | ||
|
|
cff6c7c4da | ||
|
|
f9120c620f | ||
|
|
44f1a93d31 | ||
|
|
6d61eae522 | ||
|
|
f05a9ecd2f | ||
|
|
648d682add | ||
|
|
47cf3e06c0 | ||
|
|
fdac50523b | ||
|
|
7522a32ad6 | ||
|
|
8ee743ac8a | ||
|
|
c3cfbf8e1c | ||
|
|
1756a30acc | ||
|
|
57c13ff273 | ||
|
|
3d9837678c | ||
|
|
3e95ccd06c | ||
|
|
0e21a3e121 | ||
|
|
5eef7a34d3 | ||
|
|
6c844750ae | ||
|
|
8649b15787 | ||
|
|
fbd64651e4 | ||
|
|
e01eb4e00c | ||
|
|
72b44be41c | ||
|
|
2bdb1b2029 | ||
|
|
bf685dc850 | ||
|
|
faf4887616 | ||
|
|
a1418ccb66 | ||
|
|
29f8053d6e | ||
|
|
f6dafa2b56 | ||
|
|
2b9e8fa273 | ||
|
|
5368451867 | ||
|
|
77a349c1c6 | ||
|
|
c4a3204af7 | ||
|
|
9323f7d892 | ||
|
|
30e747bb4c | ||
|
|
9d29c6d301 | ||
|
|
aa19a79d26 | ||
|
|
5a34471266 | ||
|
|
ae96010ff1 | ||
|
|
944fe6cb8c | ||
|
|
21baa302d4 | ||
|
|
9ad0032eb4 | ||
|
|
09fd65209c | ||
|
|
d8d9a49564 | ||
|
|
41a34b140c | ||
|
|
b235ba2c52 | ||
|
|
7ce9f20bc7 | ||
|
|
6c7a7d2be5 | ||
|
|
8af4fda7b6 | ||
|
|
e30f364bbd | ||
|
|
be07634b15 | ||
|
|
5cd837256f | ||
|
|
26b4ff1df2 | ||
|
|
61ff94259a | ||
|
|
4cb4c254dc | ||
|
|
3a4b157363 | ||
|
|
7a494d637b | ||
|
|
ca06a4b836 | ||
|
|
3643b1de2c | ||
|
|
d0c6eaf239 | ||
|
|
64d1722acd | ||
|
|
01e8e9576c | ||
|
|
d5514c4635 | ||
|
|
d5474128e3 | ||
|
|
8d6b2dfc9c | ||
|
|
c9404d75b0 | ||
|
|
eb50e0781e | ||
|
|
6864f28f3e | ||
|
|
6befc91773 | ||
|
|
1d6a2bff4f | ||
|
|
898558b121 | ||
|
|
a9fb7e2ace | ||
|
|
f29d5c8cae | ||
|
|
cacfd4ffae | ||
|
|
62315e304a | ||
|
|
cc39eec646 | ||
|
|
de1ec4a18f | ||
|
|
40c9287eba | ||
|
|
5869f78ea7 | ||
|
|
6c908de13f | ||
|
|
29d67ac456 | ||
|
|
6d93a6234e | ||
|
|
b579dbfdf8 | ||
|
|
6ad33bb16e | ||
|
|
7b8f8918fc | ||
|
|
a90825eac3 | ||
|
|
280ebf9c34 | ||
|
|
672a97c9ae | ||
|
|
b684ba4822 | ||
|
|
0d28eeb3c5 | ||
|
|
cf37a69e53 | ||
|
|
a99a407c41 | ||
|
|
8f447487fb | ||
|
|
eb8855afb9 | ||
|
|
09c3a99be8 | ||
|
|
3bf86cd8f0 | ||
|
|
2333ddeaf7 | ||
|
|
0e8ad7b9bc | ||
|
|
9d1a31004f | ||
|
|
f2d0d1e895 | ||
|
|
6a96f33ad2 | ||
|
|
bb069443a4 | ||
|
|
fa3d69cf48 | ||
|
|
6107749cbe | ||
|
|
60289666dc | ||
|
|
5b8c3425c8 | ||
|
|
85b92e2696 | ||
|
|
cf8ac49f76 | ||
|
|
d9594b049c | ||
|
|
caa8d478f5 | ||
|
|
7279de0605 | ||
|
|
d49860fbeb | ||
|
|
591661ca79 | ||
|
|
e1374492de | ||
|
|
5843f71447 | ||
|
|
9b1de8fea8 | ||
|
|
86a55c7837 | ||
|
|
8405b35a94 | ||
|
|
889a4f4db9 | ||
|
|
191dcb505c | ||
|
|
ecb1e0b74b | ||
|
|
f8e2d7f503 | ||
|
|
8015734fcf | ||
|
|
21228f9c63 | ||
|
|
57c1bc800c | ||
|
|
7f180a6d5a | ||
|
|
9839164817 | ||
|
|
3c1950dd40 | ||
|
|
e8bf471dcd | ||
|
|
210d6f81eb | ||
|
|
6797216eb8 | ||
|
|
1e72851b28 | ||
|
|
75463193ab | ||
|
|
257774c31b | ||
|
|
ca46a64abc | ||
|
|
1a29caffcb | ||
|
|
8fd805235d | ||
|
|
62657df3fb | ||
|
|
1f6db12797 | ||
|
|
18c9779815 | ||
|
|
09f4b7ec38 | ||
|
|
d14131c3be | ||
|
|
8360435607 | ||
|
|
83387da6a4 | ||
|
|
f318ca8886 | ||
|
|
1630529d58 | ||
|
|
60b8daa3af | ||
|
|
a77739ba18 | ||
|
|
60586aa284 | ||
|
|
f1d09d2282 | ||
|
|
48746f6c62 | ||
|
|
8c5688e5e2 | ||
|
|
bad79ee11a | ||
|
|
afed1dc558 | ||
|
|
8df08b53d9 | ||
|
|
dfe08298ef | ||
|
|
48ffad867a | ||
|
|
a88e75f3a1 | ||
|
|
087cc334f4 | ||
|
|
11278d0e61 | ||
|
|
bff2b80acf | ||
|
|
5b606e53fc | ||
|
|
9af56ec0dd | ||
|
|
ab22b11bac | ||
|
|
07d74ac186 | ||
|
|
36474c3ccc | ||
|
|
736945658a | ||
|
|
cfe14aec76 | ||
|
|
feaa30d808 | ||
|
|
1338d7a968 | ||
|
|
f2117be7d9 | ||
|
|
5f2c226b43 | ||
|
|
81b956408e | ||
|
|
354a182859 | ||
|
|
827444f5a4 | ||
|
|
d8a8997684 | ||
|
|
e920692ec3 | ||
|
|
6fd16ecced | ||
|
|
50537a9161 | ||
|
|
cbb7616f03 | ||
|
|
85a2193f35 | ||
|
|
857364fa78 | ||
|
|
153125a5ea | ||
|
|
b6e78bd1a3 | ||
|
|
d35d3b629e | ||
|
|
532c4c068f | ||
|
|
b077b2aeef | ||
|
|
e9e18054cf | ||
|
|
d94bee20d0 | ||
|
|
c321c5d256 | ||
|
|
ee40312384 | ||
|
|
a6ba185c55 | ||
|
|
6a88d5aa79 | ||
|
|
4a60d8a4c1 | ||
|
|
9b15278de8 |
9
.github/workflows/build.yml
vendored
@@ -38,12 +38,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
python setup.py build_exe --yes
|
python setup.py build_exe --yes
|
||||||
$NAME="$(ls build)".Split('.',2)[1]
|
$NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1]
|
||||||
$ZIP_NAME="Archipelago_$NAME.7z"
|
$ZIP_NAME="Archipelago_$NAME.7z"
|
||||||
|
echo "$NAME -> $ZIP_NAME"
|
||||||
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
|
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
|
||||||
New-Item -Path dist -ItemType Directory -Force
|
New-Item -Path dist -ItemType Directory -Force
|
||||||
cd build
|
cd build
|
||||||
Rename-Item exe.$NAME Archipelago
|
Rename-Item "exe.$NAME" Archipelago
|
||||||
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
||||||
- name: Store 7z
|
- name: Store 7z
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -65,10 +66,10 @@ jobs:
|
|||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.11'
|
||||||
- name: Install build-time dependencies
|
- name: Install build-time dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
echo "PYTHON=python3.11" >> $GITHUB_ENV
|
||||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||||
chmod a+rx appimagetool-x86_64.AppImage
|
chmod a+rx appimagetool-x86_64.AppImage
|
||||||
./appimagetool-x86_64.AppImage --appimage-extract
|
./appimagetool-x86_64.AppImage --appimage-extract
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
@@ -44,10 +44,10 @@ jobs:
|
|||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.11'
|
||||||
- name: Install build-time dependencies
|
- name: Install build-time dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
echo "PYTHON=python3.11" >> $GITHUB_ENV
|
||||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||||
chmod a+rx appimagetool-x86_64.AppImage
|
chmod a+rx appimagetool-x86_64.AppImage
|
||||||
./appimagetool-x86_64.AppImage --appimage-extract
|
./appimagetool-x86_64.AppImage --appimage-extract
|
||||||
|
|||||||
10
.github/workflows/unittests.yml
vendored
@@ -36,12 +36,13 @@ jobs:
|
|||||||
- {version: '3.8'}
|
- {version: '3.8'}
|
||||||
- {version: '3.9'}
|
- {version: '3.9'}
|
||||||
- {version: '3.10'}
|
- {version: '3.10'}
|
||||||
|
- {version: '3.11'}
|
||||||
include:
|
include:
|
||||||
- python: {version: '3.8'} # win7 compat
|
- python: {version: '3.8'} # win7 compat
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.10'} # current
|
- python: {version: '3.11'} # current
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.10'} # current
|
- python: {version: '3.11'} # current
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -53,8 +54,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install pytest pytest-subtests
|
pip install pytest pytest-subtests pytest-xdist
|
||||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||||
|
python Launcher.py --update_settings # make sure host.yaml exists for tests
|
||||||
- name: Unittests
|
- name: Unittests
|
||||||
run: |
|
run: |
|
||||||
pytest
|
pytest -n auto
|
||||||
|
|||||||
10
.gitignore
vendored
@@ -27,16 +27,21 @@
|
|||||||
*.archipelago
|
*.archipelago
|
||||||
*.apsave
|
*.apsave
|
||||||
*.BIN
|
*.BIN
|
||||||
|
*.puml
|
||||||
|
|
||||||
setups
|
setups
|
||||||
build
|
build
|
||||||
bundle/components.wxs
|
bundle/components.wxs
|
||||||
dist
|
dist
|
||||||
|
/prof/
|
||||||
README.html
|
README.html
|
||||||
.vs/
|
.vs/
|
||||||
EnemizerCLI/
|
EnemizerCLI/
|
||||||
/Players/
|
/Players/
|
||||||
/SNI/
|
/SNI/
|
||||||
|
/sni-*/
|
||||||
|
/appimagetool*
|
||||||
|
/host.yaml
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/logs/
|
/logs/
|
||||||
@@ -138,6 +143,7 @@ ipython_config.py
|
|||||||
.venv*
|
.venv*
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
/venv*/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
@@ -168,6 +174,10 @@ dmypy.json
|
|||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
|
# Cython intermediates
|
||||||
|
_speedups.cpp
|
||||||
|
_speedups.html
|
||||||
|
|
||||||
# minecraft server stuff
|
# minecraft server stuff
|
||||||
jdk*/
|
jdk*/
|
||||||
minecraft*/
|
minecraft*/
|
||||||
|
|||||||
189
BaseClasses.py
@@ -8,8 +8,10 @@ import secrets
|
|||||||
import typing # this can go away when Python 3.8 support is dropped
|
import typing # this can go away when Python 3.8 support is dropped
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from collections import ChainMap, Counter, deque
|
from collections import ChainMap, Counter, deque
|
||||||
|
from collections.abc import Collection
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
|
||||||
|
Type, ClassVar
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
import Options
|
import Options
|
||||||
@@ -81,6 +83,7 @@ class MultiWorld():
|
|||||||
|
|
||||||
random: random.Random
|
random: random.Random
|
||||||
per_slot_randoms: Dict[int, random.Random]
|
per_slot_randoms: Dict[int, random.Random]
|
||||||
|
"""Deprecated. Please use `self.random` instead."""
|
||||||
|
|
||||||
class AttributeProxy():
|
class AttributeProxy():
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
@@ -200,14 +203,7 @@ class MultiWorld():
|
|||||||
self.player_types[new_id] = NetUtils.SlotType.group
|
self.player_types[new_id] = NetUtils.SlotType.group
|
||||||
self._region_cache[new_id] = {}
|
self._region_cache[new_id] = {}
|
||||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||||
for option_key, option in world_type.option_definitions.items():
|
self.worlds[new_id] = world_type.create_group(self, new_id, players)
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
|
||||||
for option_key, option in Options.common_options.items():
|
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
|
||||||
for option_key, option in Options.per_game_common_options.items():
|
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
|
||||||
|
|
||||||
self.worlds[new_id] = world_type(self, new_id)
|
|
||||||
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
||||||
self.player_name[new_id] = name
|
self.player_name[new_id] = name
|
||||||
|
|
||||||
@@ -242,6 +238,7 @@ class MultiWorld():
|
|||||||
setattr(self, option_key, getattr(args, option_key, {}))
|
setattr(self, option_key, getattr(args, option_key, {}))
|
||||||
|
|
||||||
self.worlds[player] = world_type(self, player)
|
self.worlds[player] = world_type(self, player)
|
||||||
|
self.worlds[player].random = self.per_slot_randoms[player]
|
||||||
|
|
||||||
def set_item_links(self):
|
def set_item_links(self):
|
||||||
item_links = {}
|
item_links = {}
|
||||||
@@ -361,7 +358,7 @@ class MultiWorld():
|
|||||||
for r_location in region.locations:
|
for r_location in region.locations:
|
||||||
self._location_cache[r_location.name, player] = r_location
|
self._location_cache[r_location.name, player] = r_location
|
||||||
|
|
||||||
def get_regions(self, player=None):
|
def get_regions(self, player: Optional[int] = None) -> Collection[Region]:
|
||||||
return self.regions if player is None else self._region_cache[player].values()
|
return self.regions if player is None else self._region_cache[player].values()
|
||||||
|
|
||||||
def get_region(self, regionname: str, player: int) -> Region:
|
def get_region(self, regionname: str, player: int) -> Region:
|
||||||
@@ -484,8 +481,10 @@ class MultiWorld():
|
|||||||
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
|
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
|
||||||
for player in players:
|
for player in players:
|
||||||
if not location_names:
|
if not location_names:
|
||||||
location_names = [location.name for location in self.get_unfilled_locations(player)]
|
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
|
||||||
for location_name in location_names:
|
else:
|
||||||
|
valid_locations = location_names
|
||||||
|
for location_name in valid_locations:
|
||||||
location = self._location_cache.get((location_name, player), None)
|
location = self._location_cache.get((location_name, player), None)
|
||||||
if location is not None and location.item is None:
|
if location is not None and location.item is None:
|
||||||
yield location
|
yield location
|
||||||
@@ -786,78 +785,6 @@ class CollectionState():
|
|||||||
self.stale[item.player] = True
|
self.stale[item.player] = True
|
||||||
|
|
||||||
|
|
||||||
class Region:
|
|
||||||
name: str
|
|
||||||
_hint_text: str
|
|
||||||
player: int
|
|
||||||
multiworld: Optional[MultiWorld]
|
|
||||||
entrances: List[Entrance]
|
|
||||||
exits: List[Entrance]
|
|
||||||
locations: List[Location]
|
|
||||||
|
|
||||||
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
|
||||||
self.name = name
|
|
||||||
self.entrances = []
|
|
||||||
self.exits = []
|
|
||||||
self.locations = []
|
|
||||||
self.multiworld = multiworld
|
|
||||||
self._hint_text = hint
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
def can_reach(self, state: CollectionState) -> bool:
|
|
||||||
if state.stale[self.player]:
|
|
||||||
state.update_reachable_regions(self.player)
|
|
||||||
return self in state.reachable_regions[self.player]
|
|
||||||
|
|
||||||
def can_reach_private(self, state: CollectionState) -> bool:
|
|
||||||
for entrance in self.entrances:
|
|
||||||
if entrance.can_reach(state):
|
|
||||||
if not self in state.path:
|
|
||||||
state.path[self] = (self.name, state.path.get(entrance, None))
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hint_text(self) -> str:
|
|
||||||
return self._hint_text if self._hint_text else self.name
|
|
||||||
|
|
||||||
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
|
|
||||||
for entrance in self.entrances:
|
|
||||||
if is_main_entrance(entrance):
|
|
||||||
return entrance
|
|
||||||
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
|
|
||||||
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
|
||||||
|
|
||||||
def add_locations(self, locations: Dict[str, Optional[int]], location_type: Optional[typing.Type[Location]] = None) -> None:
|
|
||||||
"""Adds locations to the Region object, where location_type is your Location class and locations is a dict of
|
|
||||||
location names to address."""
|
|
||||||
if location_type is None:
|
|
||||||
location_type = Location
|
|
||||||
for location, address in locations.items():
|
|
||||||
self.locations.append(location_type(self.player, location, address, self))
|
|
||||||
|
|
||||||
def add_exits(self, exits: Dict[str, Optional[str]], rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
|
||||||
|
|
||||||
:param exits: exits from the region. format is {"connecting_region", "exit_name"}
|
|
||||||
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
|
|
||||||
"""
|
|
||||||
for exiting_region, name in exits.items():
|
|
||||||
ret = Entrance(self.player, name, self) if name \
|
|
||||||
else Entrance(self.player, f"{self.name} -> {exiting_region}", self)
|
|
||||||
if rules and exiting_region in rules:
|
|
||||||
ret.access_rule = rules[exiting_region]
|
|
||||||
self.exits.append(ret)
|
|
||||||
ret.connect(self.multiworld.get_region(exiting_region, self.player))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.__str__()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
|
||||||
|
|
||||||
|
|
||||||
class Entrance:
|
class Entrance:
|
||||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||||
hide_path: bool = False
|
hide_path: bool = False
|
||||||
@@ -896,6 +823,100 @@ class Entrance:
|
|||||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
|
|
||||||
|
class Region:
|
||||||
|
name: str
|
||||||
|
_hint_text: str
|
||||||
|
player: int
|
||||||
|
multiworld: Optional[MultiWorld]
|
||||||
|
entrances: List[Entrance]
|
||||||
|
exits: List[Entrance]
|
||||||
|
locations: List[Location]
|
||||||
|
entrance_type: ClassVar[Type[Entrance]] = Entrance
|
||||||
|
|
||||||
|
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
||||||
|
self.name = name
|
||||||
|
self.entrances = []
|
||||||
|
self.exits = []
|
||||||
|
self.locations = []
|
||||||
|
self.multiworld = multiworld
|
||||||
|
self._hint_text = hint
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
def can_reach(self, state: CollectionState) -> bool:
|
||||||
|
if state.stale[self.player]:
|
||||||
|
state.update_reachable_regions(self.player)
|
||||||
|
return self in state.reachable_regions[self.player]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hint_text(self) -> str:
|
||||||
|
return self._hint_text if self._hint_text else self.name
|
||||||
|
|
||||||
|
def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) -> Entrance:
|
||||||
|
for entrance in self.entrances:
|
||||||
|
if is_main_entrance(entrance):
|
||||||
|
return entrance
|
||||||
|
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
|
||||||
|
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
||||||
|
|
||||||
|
def add_locations(self, locations: Dict[str, Optional[int]],
|
||||||
|
location_type: Optional[Type[Location]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
|
||||||
|
location names to address.
|
||||||
|
|
||||||
|
:param locations: dictionary of locations to be created and added to this Region `{name: ID}`
|
||||||
|
:param location_type: Location class to be used to create the locations with"""
|
||||||
|
if location_type is None:
|
||||||
|
location_type = Location
|
||||||
|
for location, address in locations.items():
|
||||||
|
self.locations.append(location_type(self.player, location, address, self))
|
||||||
|
|
||||||
|
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||||
|
rule: Optional[Callable[[CollectionState], bool]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Connects this Region to another Region, placing the provided rule on the connection.
|
||||||
|
|
||||||
|
:param connecting_region: Region object to connect to path is `self -> exiting_region`
|
||||||
|
:param name: name of the connection being created
|
||||||
|
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
|
||||||
|
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
|
||||||
|
if rule:
|
||||||
|
exit_.access_rule = rule
|
||||||
|
exit_.connect(connecting_region)
|
||||||
|
|
||||||
|
def create_exit(self, name: str) -> Entrance:
|
||||||
|
"""
|
||||||
|
Creates and returns an Entrance object as an exit of this region.
|
||||||
|
|
||||||
|
:param name: name of the Entrance being created
|
||||||
|
"""
|
||||||
|
exit_ = self.entrance_type(self.player, name, self)
|
||||||
|
self.exits.append(exit_)
|
||||||
|
return exit_
|
||||||
|
|
||||||
|
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
|
||||||
|
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||||
|
|
||||||
|
:param exits: exits from the region. format is {"connecting_region": "exit_name"}. if a non dict is provided,
|
||||||
|
created entrances will be named "self.name -> connecting_region"
|
||||||
|
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
|
||||||
|
"""
|
||||||
|
if not isinstance(exits, Dict):
|
||||||
|
exits = dict.fromkeys(exits)
|
||||||
|
for connecting_region, name in exits.items():
|
||||||
|
self.connect(self.multiworld.get_region(connecting_region, self.player),
|
||||||
|
name,
|
||||||
|
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
|
|
||||||
class LocationProgressType(IntEnum):
|
class LocationProgressType(IntEnum):
|
||||||
DEFAULT = 1
|
DEFAULT = 1
|
||||||
PRIORITY = 2
|
PRIORITY = 2
|
||||||
|
|||||||
9
BizHawkClient.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ModuleUpdate
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
from worlds._bizhawk.context import launch
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
launch()
|
||||||
@@ -833,7 +833,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
|
|
||||||
elif cmd == "SetReply":
|
elif cmd == "SetReply":
|
||||||
ctx.stored_data[args["key"]] = args["value"]
|
ctx.stored_data[args["key"]] = args["value"]
|
||||||
if args["key"] == "EnergyLink":
|
if args["key"].startswith("EnergyLink"):
|
||||||
ctx.current_energy_link_value = args["value"]
|
ctx.current_energy_link_value = args["value"]
|
||||||
if ctx.ui:
|
if ctx.ui:
|
||||||
ctx.ui.set_new_energy_link_value()
|
ctx.ui.set_new_energy_link_value()
|
||||||
|
|||||||
21
Fill.py
@@ -51,7 +51,10 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
items_to_place = [items.pop()
|
items_to_place = [items.pop()
|
||||||
for items in reachable_items.values() if items]
|
for items in reachable_items.values() if items]
|
||||||
for item in items_to_place:
|
for item in items_to_place:
|
||||||
item_pool.remove(item)
|
for p, pool_item in enumerate(item_pool):
|
||||||
|
if pool_item is item:
|
||||||
|
item_pool.pop(p)
|
||||||
|
break
|
||||||
maximum_exploration_state = sweep_from_pool(
|
maximum_exploration_state = sweep_from_pool(
|
||||||
base_state, item_pool + unplaced_items)
|
base_state, item_pool + unplaced_items)
|
||||||
|
|
||||||
@@ -152,8 +155,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
|
|
||||||
if cleanup_required:
|
if cleanup_required:
|
||||||
# validate all placements and remove invalid ones
|
# validate all placements and remove invalid ones
|
||||||
for placement in placements:
|
|
||||||
state = sweep_from_pool(base_state, [])
|
state = sweep_from_pool(base_state, [])
|
||||||
|
for placement in placements:
|
||||||
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
||||||
placement.item.location = None
|
placement.item.location = None
|
||||||
unplaced_items.append(placement.item)
|
unplaced_items.append(placement.item)
|
||||||
@@ -750,8 +753,6 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
else: # not reachable with swept state
|
else: # not reachable with swept state
|
||||||
non_early_locations[loc.player].append(loc.name)
|
non_early_locations[loc.player].append(loc.name)
|
||||||
|
|
||||||
# TODO: remove. Preferably by implementing key drop
|
|
||||||
from worlds.alttp.Regions import key_drop_data
|
|
||||||
world_name_lookup = world.world_name_lookup
|
world_name_lookup = world.world_name_lookup
|
||||||
|
|
||||||
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
||||||
@@ -837,12 +838,12 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
|
|
||||||
if "early_locations" in locations:
|
if "early_locations" in locations:
|
||||||
locations.remove("early_locations")
|
locations.remove("early_locations")
|
||||||
for player in worlds:
|
for target_player in worlds:
|
||||||
locations += early_locations[player]
|
locations += early_locations[target_player]
|
||||||
if "non_early_locations" in locations:
|
if "non_early_locations" in locations:
|
||||||
locations.remove("non_early_locations")
|
locations.remove("non_early_locations")
|
||||||
for player in worlds:
|
for target_player in worlds:
|
||||||
locations += non_early_locations[player]
|
locations += non_early_locations[target_player]
|
||||||
|
|
||||||
block['locations'] = locations
|
block['locations'] = locations
|
||||||
|
|
||||||
@@ -894,10 +895,6 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
for item_name in items:
|
for item_name in items:
|
||||||
item = world.worlds[player].create_item(item_name)
|
item = world.worlds[player].create_item(item_name)
|
||||||
for location in reversed(candidates):
|
for location in reversed(candidates):
|
||||||
if location in key_drop_data:
|
|
||||||
warn(
|
|
||||||
f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
|
||||||
continue
|
|
||||||
if not location.item:
|
if not location.item:
|
||||||
if location.item_rule(item):
|
if location.item_rule(item):
|
||||||
if location.can_fill(world.state, item, False):
|
if location.can_fill(world.state, item, False):
|
||||||
|
|||||||
54
Generate.py
@@ -14,44 +14,42 @@ import ModuleUpdate
|
|||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
import copy
|
||||||
import Utils
|
import Utils
|
||||||
from worlds.alttp import Options as LttPOptions
|
|
||||||
from worlds.generic import PlandoConnection
|
|
||||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
|
|
||||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
|
||||||
from Main import main as ERmain
|
|
||||||
from BaseClasses import seeddigits, get_seed, PlandoOptions
|
|
||||||
import Options
|
import Options
|
||||||
|
from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||||
|
from Main import main as ERmain
|
||||||
|
from settings import get_settings
|
||||||
|
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, user_path
|
||||||
|
from worlds.alttp import Options as LttPOptions
|
||||||
|
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||||
from worlds.alttp.Text import TextTable
|
from worlds.alttp.Text import TextTable
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
import copy
|
from worlds.generic import PlandoConnection
|
||||||
|
|
||||||
|
|
||||||
def mystery_argparse():
|
def mystery_argparse():
|
||||||
options = get_options()
|
options = get_settings()
|
||||||
defaults = options["generator"]
|
defaults = options.generator
|
||||||
|
|
||||||
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
|
|
||||||
return path if os.path.isabs(path) else resolver(path)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
|
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
|
||||||
parser.add_argument('--weights_file_path', default=defaults["weights_file_path"],
|
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
|
||||||
help='Path to the weights file to use for rolling game settings, urls are also valid')
|
help='Path to the weights file to use for rolling game settings, urls are also valid')
|
||||||
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
|
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
|
||||||
action='store_true')
|
action='store_true')
|
||||||
parser.add_argument('--player_files_path', default=resolve_path(defaults["player_files_path"], user_path),
|
parser.add_argument('--player_files_path', default=defaults.player_files_path,
|
||||||
help="Input directory for player files.")
|
help="Input directory for player files.")
|
||||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||||
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
|
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('--spoiler', type=int, default=defaults.spoiler)
|
||||||
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
|
parser.add_argument('--outputpath', default=options.general_options.output_path,
|
||||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||||
parser.add_argument('--race', action='store_true', default=defaults["race"])
|
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||||
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
|
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||||
parser.add_argument('--log_level', default='info', help='Sets log level')
|
parser.add_argument('--log_level', default='info', help='Sets log level')
|
||||||
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
|
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)')
|
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||||
parser.add_argument('--plando', default=defaults["plando_options"],
|
parser.add_argument('--plando', default=defaults.plando_options,
|
||||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||||
parser.add_argument("--skip_prog_balancing", action="store_true",
|
parser.add_argument("--skip_prog_balancing", action="store_true",
|
||||||
help="Skip progression balancing step during generation.")
|
help="Skip progression balancing step during generation.")
|
||||||
@@ -71,6 +69,8 @@ def get_seed_name(random_source) -> str:
|
|||||||
def main(args=None, callback=ERmain):
|
def main(args=None, callback=ERmain):
|
||||||
if not args:
|
if not args:
|
||||||
args, options = mystery_argparse()
|
args, options = mystery_argparse()
|
||||||
|
else:
|
||||||
|
options = get_settings()
|
||||||
|
|
||||||
seed = get_seed(args.seed)
|
seed = get_seed(args.seed)
|
||||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||||
@@ -86,7 +86,7 @@ def main(args=None, callback=ERmain):
|
|||||||
try:
|
try:
|
||||||
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.weights_file_path} is invalid. Please fix your yaml.") from e
|
||||||
logging.info(f"Weights: {args.weights_file_path} >> "
|
logging.info(f"Weights: {args.weights_file_path} >> "
|
||||||
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
|
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ def main(args=None, callback=ERmain):
|
|||||||
try:
|
try:
|
||||||
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.meta_file_path} is invalid. Please fix your yaml.") from e
|
||||||
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
||||||
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
||||||
del(meta_weights["meta_description"])
|
del(meta_weights["meta_description"])
|
||||||
@@ -114,7 +114,7 @@ def main(args=None, callback=ERmain):
|
|||||||
try:
|
try:
|
||||||
weights_cache[fname] = read_weights_yamls(path)
|
weights_cache[fname] = read_weights_yamls(path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
|
||||||
|
|
||||||
# sort dict for consistent results across platforms:
|
# sort dict for consistent results across platforms:
|
||||||
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
|
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
|
||||||
@@ -137,7 +137,7 @@ def main(args=None, callback=ERmain):
|
|||||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||||
erargs.seed = seed
|
erargs.seed = seed
|
||||||
erargs.plando_options = args.plando
|
erargs.plando_options = args.plando
|
||||||
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
|
erargs.glitch_triforce = options.generator.glitch_triforce_room
|
||||||
erargs.spoiler = args.spoiler
|
erargs.spoiler = args.spoiler
|
||||||
erargs.race = args.race
|
erargs.race = args.race
|
||||||
erargs.outputname = seed_name
|
erargs.outputname = seed_name
|
||||||
@@ -195,7 +195,7 @@ def main(args=None, callback=ERmain):
|
|||||||
|
|
||||||
player += 1
|
player += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'No weights specified for player {player}')
|
raise RuntimeError(f'No weights specified for player {player}')
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ def roll_linked_options(weights: dict) -> dict:
|
|||||||
else:
|
else:
|
||||||
logging.debug(f"linked option {option_set['name']} skipped.")
|
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
|
raise ValueError(f"Linked option {option_set['name']} is invalid. "
|
||||||
f"Please fix your linked option.") from e
|
f"Please fix your linked option.") from e
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
|||||||
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
|
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Your trigger number {i + 1} is destroyed. "
|
raise ValueError(f"Your trigger number {i + 1} is invalid. "
|
||||||
f"Please fix your triggers.") from e
|
f"Please fix your triggers.") from e
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|||||||
52
Launcher.py
@@ -22,6 +22,7 @@ from shutil import which
|
|||||||
from typing import Sequence, Union, Optional
|
from typing import Sequence, Union, Optional
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
|
import settings
|
||||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -33,7 +34,8 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
|
|||||||
|
|
||||||
|
|
||||||
def open_host_yaml():
|
def open_host_yaml():
|
||||||
file = user_path('host.yaml')
|
file = settings.get_settings().filename
|
||||||
|
assert file, "host.yaml missing"
|
||||||
if is_linux:
|
if is_linux:
|
||||||
exe = which('sensible-editor') or which('gedit') or \
|
exe = which('sensible-editor') or which('gedit') or \
|
||||||
which('xdg-open') or which('gnome-open') or which('kde-open')
|
which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||||
@@ -48,17 +50,22 @@ def open_host_yaml():
|
|||||||
def open_patch():
|
def open_patch():
|
||||||
suffixes = []
|
suffixes = []
|
||||||
for c in components:
|
for c in components:
|
||||||
if isfile(get_exe(c)[-1]):
|
if c.type == Type.CLIENT and \
|
||||||
suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
|
isinstance(c.file_identifier, SuffixIdentifier) and \
|
||||||
isinstance(c.file_identifier, SuffixIdentifier) else []
|
(c.script_name is None or isfile(get_exe(c)[-1])):
|
||||||
|
suffixes += c.file_identifier.suffixes
|
||||||
try:
|
try:
|
||||||
filename = open_filename('Select patch', (('Patches', suffixes),))
|
filename = open_filename("Select patch", (("Patches", suffixes),))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox('Error', str(e), error=True)
|
messagebox("Error", str(e), error=True)
|
||||||
else:
|
else:
|
||||||
file, component = identify(filename)
|
file, component = identify(filename)
|
||||||
if file and component:
|
if file and component:
|
||||||
launch([*get_exe(component), file], component.cli)
|
exe = get_exe(component)
|
||||||
|
if exe is None or not isfile(exe[-1]):
|
||||||
|
exe = get_exe("Launcher")
|
||||||
|
|
||||||
|
launch([*exe, file], component.cli)
|
||||||
|
|
||||||
|
|
||||||
def generate_yamls():
|
def generate_yamls():
|
||||||
@@ -84,6 +91,11 @@ def open_folder(folder_path):
|
|||||||
webbrowser.open(folder_path)
|
webbrowser.open(folder_path)
|
||||||
|
|
||||||
|
|
||||||
|
def update_settings():
|
||||||
|
from settings import get_settings
|
||||||
|
get_settings().save()
|
||||||
|
|
||||||
|
|
||||||
components.extend([
|
components.extend([
|
||||||
# Functions
|
# Functions
|
||||||
Component("Open host.yaml", func=open_host_yaml),
|
Component("Open host.yaml", func=open_host_yaml),
|
||||||
@@ -110,25 +122,25 @@ def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
|
|||||||
if isinstance(component, str):
|
if isinstance(component, str):
|
||||||
name = component
|
name = component
|
||||||
component = None
|
component = None
|
||||||
if name.startswith('Archipelago'):
|
if name.startswith("Archipelago"):
|
||||||
name = name[11:]
|
name = name[11:]
|
||||||
if name.endswith('.exe'):
|
if name.endswith(".exe"):
|
||||||
name = name[:-4]
|
name = name[:-4]
|
||||||
if name.endswith('.py'):
|
if name.endswith(".py"):
|
||||||
name = name[:-3]
|
name = name[:-3]
|
||||||
if not name:
|
if not name:
|
||||||
return None
|
return None
|
||||||
for c in components:
|
for c in components:
|
||||||
if c.script_name == name or c.frozen_name == f'Archipelago{name}':
|
if c.script_name == name or c.frozen_name == f"Archipelago{name}":
|
||||||
component = c
|
component = c
|
||||||
break
|
break
|
||||||
if not component:
|
if not component:
|
||||||
return None
|
return None
|
||||||
if is_frozen():
|
if is_frozen():
|
||||||
suffix = '.exe' if is_windows else ''
|
suffix = ".exe" if is_windows else ""
|
||||||
return [local_path(f'{component.frozen_name}{suffix}')]
|
return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None
|
||||||
else:
|
else:
|
||||||
return [sys.executable, local_path(f'{component.script_name}.py')]
|
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
||||||
|
|
||||||
|
|
||||||
def launch(exe, in_terminal=False):
|
def launch(exe, in_terminal=False):
|
||||||
@@ -256,11 +268,13 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
if not component:
|
if not component:
|
||||||
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
||||||
|
|
||||||
|
if args["update_settings"]:
|
||||||
|
update_settings()
|
||||||
if 'file' in args:
|
if 'file' in args:
|
||||||
run_component(args["component"], args["file"], *args["args"])
|
run_component(args["component"], args["file"], *args["args"])
|
||||||
elif 'component' in args:
|
elif 'component' in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
else:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui()
|
||||||
|
|
||||||
|
|
||||||
@@ -269,9 +283,13 @@ if __name__ == '__main__':
|
|||||||
Utils.freeze_support()
|
Utils.freeze_support()
|
||||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||||
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
||||||
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
|
run_group = parser.add_argument_group("Run")
|
||||||
|
run_group.add_argument("--update_settings", action="store_true",
|
||||||
|
help="Update host.yaml and exit.")
|
||||||
|
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
|
||||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
help="Pass either a patch file, a generated game or the name of a component to run.")
|
||||||
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
|
run_group.add_argument("args", nargs="*",
|
||||||
|
help="Arguments to pass to component.")
|
||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|
||||||
from worlds.LauncherComponents import processes
|
from worlds.LauncherComponents import processes
|
||||||
|
|||||||
@@ -9,16 +9,19 @@ if __name__ == "__main__":
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
|
import colorama
|
||||||
import io
|
import io
|
||||||
import logging
|
import os
|
||||||
|
import re
|
||||||
import select
|
import select
|
||||||
|
import shlex
|
||||||
import socket
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
import urllib
|
|
||||||
|
|
||||||
import colorama
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||||
server_loop)
|
server_loop)
|
||||||
@@ -30,6 +33,7 @@ from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
|||||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
||||||
|
|
||||||
|
|
||||||
class GameboyException(Exception):
|
class GameboyException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -115,17 +119,17 @@ class RAGameboy():
|
|||||||
assert (self.socket)
|
assert (self.socket)
|
||||||
self.socket.setblocking(False)
|
self.socket.setblocking(False)
|
||||||
|
|
||||||
def get_retroarch_version(self):
|
async def send_command(self, command, timeout=1.0):
|
||||||
self.send(b'VERSION\n')
|
self.send(f'{command}\n')
|
||||||
select.select([self.socket], [], [])
|
response_str = await self.async_recv()
|
||||||
response_str, addr = self.socket.recvfrom(16)
|
self.check_command_response(command, response_str)
|
||||||
return response_str.rstrip()
|
return response_str.rstrip()
|
||||||
|
|
||||||
def get_retroarch_status(self, timeout):
|
async def get_retroarch_version(self):
|
||||||
self.send(b'GET_STATUS\n')
|
return await self.send_command("VERSION")
|
||||||
select.select([self.socket], [], [], timeout)
|
|
||||||
response_str, addr = self.socket.recvfrom(1000, )
|
async def get_retroarch_status(self):
|
||||||
return response_str.rstrip()
|
return await self.send_command("GET_STATUS")
|
||||||
|
|
||||||
def set_cache_limits(self, cache_start, cache_size):
|
def set_cache_limits(self, cache_start, cache_size):
|
||||||
self.cache_start = cache_start
|
self.cache_start = cache_start
|
||||||
@@ -141,8 +145,8 @@ class RAGameboy():
|
|||||||
response, _ = self.socket.recvfrom(4096)
|
response, _ = self.socket.recvfrom(4096)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def async_recv(self):
|
async def async_recv(self, timeout=1.0):
|
||||||
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
|
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), timeout)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def check_safe_gameplay(self, throw=True):
|
async def check_safe_gameplay(self, throw=True):
|
||||||
@@ -169,6 +173,8 @@ class RAGameboy():
|
|||||||
raise InvalidEmulatorStateError()
|
raise InvalidEmulatorStateError()
|
||||||
return False
|
return False
|
||||||
if not await check_wram():
|
if not await check_wram():
|
||||||
|
if throw:
|
||||||
|
raise InvalidEmulatorStateError()
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -227,20 +233,30 @@ class RAGameboy():
|
|||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def check_command_response(self, command: str, response: bytes):
|
||||||
|
if command == "VERSION":
|
||||||
|
ok = re.match("\d+\.\d+\.\d+", response.decode('ascii')) is not None
|
||||||
|
else:
|
||||||
|
ok = response.startswith(command.encode())
|
||||||
|
if not ok:
|
||||||
|
logger.warning(f"Bad response to command {command} - {response}")
|
||||||
|
raise BadRetroArchResponse()
|
||||||
|
|
||||||
def read_memory(self, address, size=1):
|
def read_memory(self, address, size=1):
|
||||||
command = "READ_CORE_MEMORY"
|
command = "READ_CORE_MEMORY"
|
||||||
|
|
||||||
self.send(f'{command} {hex(address)} {size}\n')
|
self.send(f'{command} {hex(address)} {size}\n')
|
||||||
response = self.recv()
|
response = self.recv()
|
||||||
|
|
||||||
|
self.check_command_response(command, response)
|
||||||
|
|
||||||
splits = response.decode().split(" ", 2)
|
splits = response.decode().split(" ", 2)
|
||||||
|
|
||||||
assert (splits[0] == command)
|
|
||||||
# Ignore the address for now
|
# Ignore the address for now
|
||||||
|
if splits[2][:2] == "-1":
|
||||||
# TODO: transform to bytes
|
|
||||||
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
|
|
||||||
raise BadRetroArchResponse()
|
raise BadRetroArchResponse()
|
||||||
|
|
||||||
|
# TODO: check response address, check hex behavior between RA and BH
|
||||||
|
|
||||||
return bytearray.fromhex(splits[2])
|
return bytearray.fromhex(splits[2])
|
||||||
|
|
||||||
async def async_read_memory(self, address, size=1):
|
async def async_read_memory(self, address, size=1):
|
||||||
@@ -248,14 +264,21 @@ class RAGameboy():
|
|||||||
|
|
||||||
self.send(f'{command} {hex(address)} {size}\n')
|
self.send(f'{command} {hex(address)} {size}\n')
|
||||||
response = await self.async_recv()
|
response = await self.async_recv()
|
||||||
|
self.check_command_response(command, response)
|
||||||
response = response[:-1]
|
response = response[:-1]
|
||||||
splits = response.decode().split(" ", 2)
|
splits = response.decode().split(" ", 2)
|
||||||
|
try:
|
||||||
|
response_addr = int(splits[1], 16)
|
||||||
|
except ValueError:
|
||||||
|
raise BadRetroArchResponse()
|
||||||
|
|
||||||
assert (splits[0] == command)
|
if response_addr != address:
|
||||||
# Ignore the address for now
|
raise BadRetroArchResponse()
|
||||||
|
|
||||||
# TODO: transform to bytes
|
ret = bytearray.fromhex(splits[2])
|
||||||
return bytearray.fromhex(splits[2])
|
if len(ret) > size:
|
||||||
|
raise BadRetroArchResponse()
|
||||||
|
return ret
|
||||||
|
|
||||||
def write_memory(self, address, bytes):
|
def write_memory(self, address, bytes):
|
||||||
command = "WRITE_CORE_MEMORY"
|
command = "WRITE_CORE_MEMORY"
|
||||||
@@ -263,7 +286,7 @@ class RAGameboy():
|
|||||||
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
||||||
select.select([self.socket], [], [])
|
select.select([self.socket], [], [])
|
||||||
response, _ = self.socket.recvfrom(4096)
|
response, _ = self.socket.recvfrom(4096)
|
||||||
|
self.check_command_response(command, response)
|
||||||
splits = response.decode().split(" ", 3)
|
splits = response.decode().split(" ", 3)
|
||||||
|
|
||||||
assert (splits[0] == command)
|
assert (splits[0] == command)
|
||||||
@@ -281,6 +304,9 @@ class LinksAwakeningClient():
|
|||||||
pending_deathlink = False
|
pending_deathlink = False
|
||||||
deathlink_debounce = True
|
deathlink_debounce = True
|
||||||
recvd_checks = {}
|
recvd_checks = {}
|
||||||
|
retroarch_address = None
|
||||||
|
retroarch_port = None
|
||||||
|
gameboy = None
|
||||||
|
|
||||||
def msg(self, m):
|
def msg(self, m):
|
||||||
logger.info(m)
|
logger.info(m)
|
||||||
@@ -288,24 +314,29 @@ class LinksAwakeningClient():
|
|||||||
self.gameboy.send(s)
|
self.gameboy.send(s)
|
||||||
|
|
||||||
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
|
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
|
||||||
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
|
self.retroarch_address = retroarch_address
|
||||||
|
self.retroarch_port = retroarch_port
|
||||||
|
pass
|
||||||
|
|
||||||
|
stop_bizhawk_spam = False
|
||||||
async def wait_for_retroarch_connection(self):
|
async def wait_for_retroarch_connection(self):
|
||||||
|
if not self.stop_bizhawk_spam:
|
||||||
logger.info("Waiting on connection to Retroarch...")
|
logger.info("Waiting on connection to Retroarch...")
|
||||||
|
self.stop_bizhawk_spam = True
|
||||||
|
self.gameboy = RAGameboy(self.retroarch_address, self.retroarch_port)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
version = self.gameboy.get_retroarch_version()
|
version = await self.gameboy.get_retroarch_version()
|
||||||
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
||||||
status = NO_CONTENT
|
status = NO_CONTENT
|
||||||
core_type = None
|
core_type = None
|
||||||
GAME_BOY = b"game_boy"
|
GAME_BOY = b"game_boy"
|
||||||
while status == NO_CONTENT or core_type != GAME_BOY:
|
while status == NO_CONTENT or core_type != GAME_BOY:
|
||||||
try:
|
status = await self.gameboy.get_retroarch_status()
|
||||||
status = self.gameboy.get_retroarch_status(0.1)
|
|
||||||
if status.count(b" ") < 2:
|
if status.count(b" ") < 2:
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
||||||
if status.count(b",") < 2:
|
if status.count(b",") < 2:
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
@@ -316,22 +347,15 @@ class LinksAwakeningClient():
|
|||||||
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
continue
|
continue
|
||||||
except (BlockingIOError, TimeoutError) as e:
|
self.stop_bizhawk_spam = False
|
||||||
await asyncio.sleep(0.1)
|
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
|
||||||
pass
|
|
||||||
logger.info(f"Connected to Retroarch {version} {info}")
|
|
||||||
self.gameboy.read_memory(0x1000)
|
|
||||||
return
|
return
|
||||||
except ConnectionResetError:
|
except (BlockingIOError, TimeoutError, ConnectionResetError):
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def reset_auth(self):
|
async def reset_auth(self):
|
||||||
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
|
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
|
||||||
|
|
||||||
if self.auth:
|
|
||||||
assert (auth == self.auth)
|
|
||||||
|
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
|
|
||||||
async def wait_and_init_tracker(self):
|
async def wait_and_init_tracker(self):
|
||||||
@@ -367,11 +391,14 @@ class LinksAwakeningClient():
|
|||||||
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
||||||
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
|
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
|
||||||
|
|
||||||
|
should_reset_auth = False
|
||||||
async def wait_for_game_ready(self):
|
async def wait_for_game_ready(self):
|
||||||
logger.info("Waiting on game to be in valid state...")
|
logger.info("Waiting on game to be in valid state...")
|
||||||
while not await self.gameboy.check_safe_gameplay(throw=False):
|
while not await self.gameboy.check_safe_gameplay(throw=False):
|
||||||
pass
|
if self.should_reset_auth:
|
||||||
logger.info("Ready!")
|
self.should_reset_auth = False
|
||||||
|
raise GameboyException("Resetting due to wrong archipelago server")
|
||||||
|
logger.info("Game connection ready!")
|
||||||
|
|
||||||
async def is_victory(self):
|
async def is_victory(self):
|
||||||
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||||
@@ -398,7 +425,7 @@ class LinksAwakeningClient():
|
|||||||
if await self.is_victory():
|
if await self.is_victory():
|
||||||
await win_cb()
|
await win_cb()
|
||||||
|
|
||||||
recv_index = struct.unpack(">H", self.gameboy.read_memory(LAClientConstants.wRecvIndex, 2))[0]
|
recv_index = struct.unpack(">H", await self.gameboy.async_read_memory(LAClientConstants.wRecvIndex, 2))[0]
|
||||||
|
|
||||||
# Play back one at a time
|
# Play back one at a time
|
||||||
if recv_index in self.recvd_checks:
|
if recv_index in self.recvd_checks:
|
||||||
@@ -480,6 +507,15 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||||
await self.send_msgs(message)
|
await self.send_msgs(message)
|
||||||
|
|
||||||
|
had_invalid_slot_data = None
|
||||||
|
def event_invalid_slot(self):
|
||||||
|
# The next time we try to connect, reset the game loop for new auth
|
||||||
|
self.had_invalid_slot_data = True
|
||||||
|
self.auth = None
|
||||||
|
# Don't try to autoreconnect, it will just fail
|
||||||
|
self.disconnected_intentionally = True
|
||||||
|
CommonContext.event_invalid_slot(self)
|
||||||
|
|
||||||
ENABLE_DEATHLINK = False
|
ENABLE_DEATHLINK = False
|
||||||
async def send_deathlink(self):
|
async def send_deathlink(self):
|
||||||
if self.ENABLE_DEATHLINK:
|
if self.ENABLE_DEATHLINK:
|
||||||
@@ -511,8 +547,17 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
await super(LinksAwakeningContext, self).server_auth(password_requested)
|
await super(LinksAwakeningContext, self).server_auth(password_requested)
|
||||||
|
|
||||||
|
if self.had_invalid_slot_data:
|
||||||
|
# We are connecting when previously we had the wrong ROM or server - just in case
|
||||||
|
# re-read the ROM so that if the user had the correct address but wrong ROM, we
|
||||||
|
# allow a successful reconnect
|
||||||
|
self.client.should_reset_auth = True
|
||||||
|
self.had_invalid_slot_data = False
|
||||||
|
|
||||||
|
while self.client.auth == None:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
self.auth = self.client.auth
|
self.auth = self.client.auth
|
||||||
await self.get_username()
|
|
||||||
await self.send_connect()
|
await self.send_connect()
|
||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
@@ -520,9 +565,13 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
self.game = self.slot_info[self.slot].game
|
self.game = self.slot_info[self.slot].game
|
||||||
# TODO - use watcher_event
|
# TODO - use watcher_event
|
||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
for index, item in enumerate(args["items"], args["index"]):
|
for index, item in enumerate(args["items"], start=args["index"]):
|
||||||
self.client.recvd_checks[index] = item
|
self.client.recvd_checks[index] = item
|
||||||
|
|
||||||
|
async def sync(self):
|
||||||
|
sync_msg = [{'cmd': 'Sync'}]
|
||||||
|
await self.send_msgs(sync_msg)
|
||||||
|
|
||||||
item_id_lookup = get_locations_to_id()
|
item_id_lookup = get_locations_to_id()
|
||||||
|
|
||||||
async def run_game_loop(self):
|
async def run_game_loop(self):
|
||||||
@@ -546,10 +595,24 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# TODO: cancel all client tasks
|
# TODO: cancel all client tasks
|
||||||
|
if not self.client.stop_bizhawk_spam:
|
||||||
logger.info("(Re)Starting game loop")
|
logger.info("(Re)Starting game loop")
|
||||||
self.found_checks.clear()
|
self.found_checks.clear()
|
||||||
|
# On restart of game loop, clear all checks, just in case we swapped ROMs
|
||||||
|
# this isn't totally neccessary, but is extra safety against cross-ROM contamination
|
||||||
|
self.client.recvd_checks.clear()
|
||||||
await self.client.wait_for_retroarch_connection()
|
await self.client.wait_for_retroarch_connection()
|
||||||
self.client.reset_auth()
|
await self.client.reset_auth()
|
||||||
|
# If we find ourselves with new auth after the reset, reconnect
|
||||||
|
if self.auth and self.client.auth != self.auth:
|
||||||
|
# It would be neat to reconnect here, but connection needs this loop to be running
|
||||||
|
logger.info("Detected new ROM, disconnecting...")
|
||||||
|
await self.disconnect()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self.client.recvd_checks:
|
||||||
|
await self.sync()
|
||||||
|
|
||||||
await self.client.wait_and_init_tracker()
|
await self.client.wait_and_init_tracker()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -560,39 +623,59 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
self.last_resend = now
|
self.last_resend = now
|
||||||
await self.send_checks()
|
await self.send_checks()
|
||||||
if self.magpie_enabled:
|
if self.magpie_enabled:
|
||||||
|
try:
|
||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||||
await self.magpie.send_gps(self.client.gps_tracker)
|
await self.magpie.send_gps(self.client.gps_tracker)
|
||||||
|
except Exception:
|
||||||
except GameboyException:
|
# Don't let magpie errors take out the client
|
||||||
time.sleep(1.0)
|
|
||||||
pass
|
pass
|
||||||
|
if self.client.should_reset_auth:
|
||||||
|
self.client.should_reset_auth = False
|
||||||
|
raise GameboyException("Resetting due to wrong archipelago server")
|
||||||
|
except (GameboyException, asyncio.TimeoutError, TimeoutError, ConnectionResetError):
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
|
def run_game(romfile: str) -> None:
|
||||||
|
auto_start = typing.cast(typing.Union[bool, str],
|
||||||
|
Utils.get_options()["ladx_options"].get("rom_start", True))
|
||||||
|
if auto_start is True:
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(romfile)
|
||||||
|
elif isinstance(auto_start, str):
|
||||||
|
args = shlex.split(auto_start)
|
||||||
|
# Specify full path to ROM as we are going to cd in popen
|
||||||
|
full_rom_path = os.path.realpath(romfile)
|
||||||
|
args.append(full_rom_path)
|
||||||
|
try:
|
||||||
|
# set cwd so that paths to lua scripts are always relative to our client
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# The application is frozen
|
||||||
|
script_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=script_dir)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error(f"Couldn't launch ROM, {args[0]} is missing")
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
parser = get_base_parser(description="Link's Awakening Client.")
|
parser = get_base_parser(description="Link's Awakening Client.")
|
||||||
parser.add_argument("--url", help="Archipelago connection url")
|
parser.add_argument("--url", help="Archipelago connection url")
|
||||||
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
|
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
|
||||||
|
|
||||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
help='Path to a .apladx Archipelago Binary Patch file')
|
help='Path to a .apladx Archipelago Binary Patch file')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
logger.info(args)
|
|
||||||
|
|
||||||
if args.diff_file:
|
if args.diff_file:
|
||||||
import Patch
|
import Patch
|
||||||
logger.info("patch file was supplied - creating rom...")
|
logger.info("patch file was supplied - creating rom...")
|
||||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||||
if "server" in meta:
|
if "server" in meta and not args.connect:
|
||||||
args.url = meta["server"]
|
args.connect = meta["server"]
|
||||||
logger.info(f"wrote rom file to {rom_file}")
|
logger.info(f"wrote rom file to {rom_file}")
|
||||||
|
|
||||||
if args.url:
|
|
||||||
url = urllib.parse.urlparse(args.url)
|
|
||||||
args.connect = url.netloc
|
|
||||||
if url.password:
|
|
||||||
args.password = urllib.parse.unquote(url.password)
|
|
||||||
|
|
||||||
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
|
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
|
||||||
|
|
||||||
@@ -604,6 +687,10 @@ async def main():
|
|||||||
ctx.run_gui()
|
ctx.run_gui()
|
||||||
ctx.run_cli()
|
ctx.run_cli()
|
||||||
|
|
||||||
|
# Down below run_gui so that we get errors out of the process
|
||||||
|
if args.diff_file:
|
||||||
|
run_game(rom_file)
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
await ctx.shutdown()
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ ModuleUpdate.update()
|
|||||||
|
|
||||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||||
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
|
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
|
||||||
get_adjuster_settings, tkinter_center_window, init_logging
|
get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging
|
||||||
|
|
||||||
|
|
||||||
GAME_ALTTP = "A Link to the Past"
|
GAME_ALTTP = "A Link to the Past"
|
||||||
@@ -43,6 +43,47 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
|||||||
def _get_help_string(self, action):
|
def _get_help_string(self, action):
|
||||||
return textwrap.dedent(action.help)
|
return textwrap.dedent(action.help)
|
||||||
|
|
||||||
|
# See argparse.BooleanOptionalAction
|
||||||
|
class BooleanOptionalActionWithDisable(argparse.Action):
|
||||||
|
def __init__(self,
|
||||||
|
option_strings,
|
||||||
|
dest,
|
||||||
|
default=None,
|
||||||
|
type=None,
|
||||||
|
choices=None,
|
||||||
|
required=False,
|
||||||
|
help=None,
|
||||||
|
metavar=None):
|
||||||
|
|
||||||
|
_option_strings = []
|
||||||
|
for option_string in option_strings:
|
||||||
|
_option_strings.append(option_string)
|
||||||
|
|
||||||
|
if option_string.startswith('--'):
|
||||||
|
option_string = '--disable' + option_string[2:]
|
||||||
|
_option_strings.append(option_string)
|
||||||
|
|
||||||
|
if help is not None and default is not None:
|
||||||
|
help += " (default: %(default)s)"
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
option_strings=_option_strings,
|
||||||
|
dest=dest,
|
||||||
|
nargs=0,
|
||||||
|
default=default,
|
||||||
|
type=type,
|
||||||
|
choices=choices,
|
||||||
|
required=required,
|
||||||
|
help=help,
|
||||||
|
metavar=metavar)
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
if option_string in self.option_strings:
|
||||||
|
setattr(namespace, self.dest, not option_string.startswith('--disable'))
|
||||||
|
|
||||||
|
def format_usage(self):
|
||||||
|
return ' | '.join(self.option_strings)
|
||||||
|
|
||||||
|
|
||||||
def get_argparser() -> argparse.ArgumentParser:
|
def get_argparser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||||
@@ -52,6 +93,8 @@ def get_argparser() -> argparse.ArgumentParser:
|
|||||||
help='Path to an ALttP Japan(1.0) rom to use as a base.')
|
help='Path to an ALttP Japan(1.0) rom to use as a base.')
|
||||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||||
|
parser.add_argument('--auto_apply', default='ask',
|
||||||
|
choices=['ask', 'always', 'never'], help='Whether or not to apply settings automatically in the future.')
|
||||||
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
|
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
|
||||||
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||||
help='''\
|
help='''\
|
||||||
@@ -61,7 +104,7 @@ def get_argparser() -> argparse.ArgumentParser:
|
|||||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
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('--deathlink', help='Enable DeathLink system.', action='store_true')
|
||||||
parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true')
|
parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true')
|
||||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
parser.add_argument('--music', default=True, help='Enables/Disables game music.', action=BooleanOptionalActionWithDisable)
|
||||||
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
|
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
|
||||||
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||||
help='''\
|
help='''\
|
||||||
@@ -104,21 +147,23 @@ def get_argparser() -> argparse.ArgumentParser:
|
|||||||
Alternatively, can be a ALttP Rom patched with a Link
|
Alternatively, can be a ALttP Rom patched with a Link
|
||||||
sprite that will be extracted.
|
sprite that will be extracted.
|
||||||
''')
|
''')
|
||||||
|
parser.add_argument('--sprite_pool', nargs='+', default=[], help='''
|
||||||
|
A list of sprites to pull from.
|
||||||
|
''')
|
||||||
parser.add_argument('--oof', help='''\
|
parser.add_argument('--oof', help='''\
|
||||||
Path to a sound effect to replace Link's "oof" sound.
|
Path to a sound effect to replace Link's "oof" sound.
|
||||||
Needs to be in a .brr format and have a length of no
|
Needs to be in a .brr format and have a length of no
|
||||||
more than 2673 bytes, created from a 16-bit signed PCM
|
more than 2673 bytes, created from a 16-bit signed PCM
|
||||||
.wav at 12khz. https://github.com/boldowa/snesbrr
|
.wav at 12khz. https://github.com/boldowa/snesbrr
|
||||||
''')
|
''')
|
||||||
parser.add_argument('--names', default='', type=str)
|
|
||||||
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = get_argparser()
|
parser = get_argparser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(namespace=get_adjuster_settings_no_defaults(GAME_ALTTP))
|
||||||
args.music = not args.disablemusic
|
|
||||||
# set up logger
|
# set up logger
|
||||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
||||||
args.loglevel]
|
args.loglevel]
|
||||||
@@ -530,9 +575,6 @@ class AttachTooltip(object):
|
|||||||
|
|
||||||
def get_rom_frame(parent=None):
|
def get_rom_frame(parent=None):
|
||||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||||
if not adjuster_settings:
|
|
||||||
adjuster_settings = Namespace()
|
|
||||||
adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
|
||||||
|
|
||||||
romFrame = Frame(parent)
|
romFrame = Frame(parent)
|
||||||
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
||||||
@@ -560,33 +602,8 @@ def get_rom_frame(parent=None):
|
|||||||
|
|
||||||
return romFrame, romVar
|
return romFrame, romVar
|
||||||
|
|
||||||
|
|
||||||
def get_rom_options_frame(parent=None):
|
def get_rom_options_frame(parent=None):
|
||||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||||
defaults = {
|
|
||||||
"auto_apply": 'ask',
|
|
||||||
"music": True,
|
|
||||||
"reduceflashing": True,
|
|
||||||
"deathlink": False,
|
|
||||||
"sprite": None,
|
|
||||||
"oof": None,
|
|
||||||
"quickswap": True,
|
|
||||||
"menuspeed": 'normal',
|
|
||||||
"heartcolor": 'red',
|
|
||||||
"heartbeep": 'normal',
|
|
||||||
"ow_palettes": 'default',
|
|
||||||
"uw_palettes": 'default',
|
|
||||||
"hud_palettes": 'default',
|
|
||||||
"sword_palettes": 'default',
|
|
||||||
"shield_palettes": 'default',
|
|
||||||
"sprite_pool": [],
|
|
||||||
"allowcollect": False,
|
|
||||||
}
|
|
||||||
if not adjuster_settings:
|
|
||||||
adjuster_settings = Namespace()
|
|
||||||
for key, defaultvalue in defaults.items():
|
|
||||||
if not hasattr(adjuster_settings, key):
|
|
||||||
setattr(adjuster_settings, key, defaultvalue)
|
|
||||||
|
|
||||||
romOptionsFrame = LabelFrame(parent, text="Rom options")
|
romOptionsFrame = LabelFrame(parent, text="Rom options")
|
||||||
romOptionsFrame.columnconfigure(0, weight=1)
|
romOptionsFrame.columnconfigure(0, weight=1)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class MMBN3Context(CommonContext):
|
|||||||
self.auth_name = None
|
self.auth_name = None
|
||||||
self.slot_data = dict()
|
self.slot_data = dict()
|
||||||
self.patching_error = False
|
self.patching_error = False
|
||||||
|
self.sent_hints = []
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
@@ -175,8 +176,11 @@ async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool):
|
|||||||
|
|
||||||
# If trade hinting is enabled, send scout checks
|
# If trade hinting is enabled, send scout checks
|
||||||
if ctx.slot_data.get("trade_quest_hinting", 0) == 2:
|
if ctx.slot_data.get("trade_quest_hinting", 0) == 2:
|
||||||
scouted_locs = [loc.id for loc in scoutable_locations
|
trade_bits = [loc.id for loc in scoutable_locations
|
||||||
if check_location_scouted(loc, payload["locations"])]
|
if check_location_scouted(loc, payload["locations"])]
|
||||||
|
scouted_locs = [loc for loc in trade_bits if loc not in ctx.sent_hints]
|
||||||
|
if len(scouted_locs) > 0:
|
||||||
|
ctx.sent_hints.extend(scouted_locs)
|
||||||
await ctx.send_msgs([{
|
await ctx.send_msgs([{
|
||||||
"cmd": "LocationScouts",
|
"cmd": "LocationScouts",
|
||||||
"locations": scouted_locs,
|
"locations": scouted_locs,
|
||||||
|
|||||||
75
Main.py
@@ -7,31 +7,24 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
import zlib
|
import zlib
|
||||||
from typing import Dict, List, Optional, Set, Tuple
|
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
import worlds
|
import worlds
|
||||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||||
from Options import StartInventoryPool
|
from Options import StartInventoryPool
|
||||||
from Utils import __version__, get_options, output_path, version_tuple
|
from settings import get_settings
|
||||||
|
from Utils import __version__, output_path, version_tuple
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
from worlds.alttp.Regions import is_main_entrance
|
|
||||||
from worlds.alttp.Shops import FillDisabledShopSlots
|
|
||||||
from worlds.alttp.SubClasses import LTTPRegionType
|
|
||||||
from worlds.generic.Rules import exclusion_rules, locality_rules
|
from worlds.generic.Rules import exclusion_rules, locality_rules
|
||||||
|
|
||||||
__all__ = ["main"]
|
__all__ = ["main"]
|
||||||
|
|
||||||
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 main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
||||||
if not baked_server_options:
|
if not baked_server_options:
|
||||||
baked_server_options = get_options()["server_options"]
|
baked_server_options = get_settings().server_options.as_dict()
|
||||||
|
assert isinstance(baked_server_options, dict)
|
||||||
if args.outputpath:
|
if args.outputpath:
|
||||||
os.makedirs(args.outputpath, exist_ok=True)
|
os.makedirs(args.outputpath, exist_ok=True)
|
||||||
output_path.cached_path = args.outputpath
|
output_path.cached_path = args.outputpath
|
||||||
@@ -140,19 +133,26 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
world.non_local_items[player].value -= world.local_items[player].value
|
world.non_local_items[player].value -= world.local_items[player].value
|
||||||
world.non_local_items[player].value -= set(world.local_early_items[player])
|
world.non_local_items[player].value -= set(world.local_early_items[player])
|
||||||
|
|
||||||
if world.players > 1:
|
|
||||||
locality_rules(world)
|
|
||||||
else:
|
|
||||||
world.non_local_items[1].value = set()
|
|
||||||
world.local_items[1].value = set()
|
|
||||||
|
|
||||||
AutoWorld.call_all(world, "set_rules")
|
AutoWorld.call_all(world, "set_rules")
|
||||||
|
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
exclusion_rules(world, player, world.exclude_locations[player].value)
|
||||||
world.priority_locations[player].value -= world.exclude_locations[player].value
|
world.priority_locations[player].value -= world.exclude_locations[player].value
|
||||||
for location_name in world.priority_locations[player].value:
|
for location_name in world.priority_locations[player].value:
|
||||||
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
|
try:
|
||||||
|
location = world.get_location(location_name, player)
|
||||||
|
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||||
|
if location_name not in world.worlds[player].location_name_to_id:
|
||||||
|
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
|
||||||
|
else:
|
||||||
|
location.progress_type = LocationProgressType.PRIORITY
|
||||||
|
|
||||||
|
# Set local and non-local item rules.
|
||||||
|
if world.players > 1:
|
||||||
|
locality_rules(world)
|
||||||
|
else:
|
||||||
|
world.non_local_items[1].value = set()
|
||||||
|
world.local_items[1].value = set()
|
||||||
|
|
||||||
AutoWorld.call_all(world, "generate_basic")
|
AutoWorld.call_all(world, "generate_basic")
|
||||||
|
|
||||||
@@ -165,6 +165,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player, items in depletion_pool.items():
|
for player, items in depletion_pool.items():
|
||||||
player_world: AutoWorld.World = world.worlds[player]
|
player_world: AutoWorld.World = world.worlds[player]
|
||||||
for count in items.values():
|
for count in items.values():
|
||||||
|
for _ in range(count):
|
||||||
new_items.append(player_world.create_filler())
|
new_items.append(player_world.create_filler())
|
||||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||||
for i, item in enumerate(world.itempool):
|
for i, item in enumerate(world.itempool):
|
||||||
@@ -185,6 +186,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
if remaining_items:
|
if remaining_items:
|
||||||
raise Exception(f"{world.get_player_name(player)}"
|
raise Exception(f"{world.get_player_name(player)}"
|
||||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||||
|
assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||||
world.itempool[:] = new_items
|
world.itempool[:] = new_items
|
||||||
|
|
||||||
# temporary home for item links, should be moved out of Main
|
# temporary home for item links, should be moved out of Main
|
||||||
@@ -313,35 +315,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||||
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
|
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
|
||||||
|
|
||||||
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 location in world.get_filled_locations():
|
|
||||||
if type(location.address) is int:
|
|
||||||
if location.game != "A Link to the Past":
|
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
|
||||||
else:
|
|
||||||
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
|
|
||||||
if 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 location.parent_region.type == LTTPRegionType.LightWorld:
|
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
|
||||||
elif location.parent_region.type == LTTPRegionType.DarkWorld:
|
|
||||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
|
||||||
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
|
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
|
||||||
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
|
|
||||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
|
||||||
checks_in_area[location.player]["Total"] += 1
|
|
||||||
|
|
||||||
FillDisabledShopSlots(world)
|
|
||||||
|
|
||||||
def write_multidata():
|
def write_multidata():
|
||||||
import NetUtils
|
import NetUtils
|
||||||
slot_data = {}
|
slot_data = {}
|
||||||
@@ -401,10 +374,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for game_world in world.worlds.values()
|
for game_world in world.worlds.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||||
|
|
||||||
multidata = {
|
multidata = {
|
||||||
"slot_data": slot_data,
|
"slot_data": slot_data,
|
||||||
"slot_info": slot_info,
|
"slot_info": slot_info,
|
||||||
"names": names, # TODO: remove after 0.3.9
|
|
||||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||||
"locations": locations_data,
|
"locations": locations_data,
|
||||||
"checks_in_area": checks_in_area,
|
"checks_in_area": checks_in_area,
|
||||||
@@ -426,7 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
f.write(bytes([3])) # version of format
|
f.write(bytes([3])) # version of format
|
||||||
f.write(multidata)
|
f.write(multidata)
|
||||||
|
|
||||||
multidata_task = pool.submit(write_multidata)
|
output_file_futures.append(pool.submit(write_multidata))
|
||||||
if not check_accessibility_task.result():
|
if not check_accessibility_task.result():
|
||||||
if not world.can_beat_game():
|
if not world.can_beat_game():
|
||||||
raise Exception("Game appears as unbeatable. Aborting.")
|
raise Exception("Game appears as unbeatable. Aborting.")
|
||||||
@@ -434,7 +408,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||||
|
|
||||||
# retrieve exceptions via .result() if they occurred.
|
# retrieve exceptions via .result() if they occurred.
|
||||||
multidata_task.result()
|
|
||||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
||||||
if i % 10 == 0 or i == len(output_file_futures):
|
if i % 10 == 0 or i == len(output_file_futures):
|
||||||
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
versions = get_minecraft_versions(data_version, channel)
|
versions = get_minecraft_versions(data_version, channel)
|
||||||
|
|
||||||
forge_dir = Utils.user_path(options["minecraft_options"]["forge_directory"])
|
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||||
forge_version = args.forge or versions["forge"]
|
forge_version = args.forge or versions["forge"]
|
||||||
java_version = args.java or versions["java"]
|
java_version = args.java or versions["java"]
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import NetUtils
|
|||||||
import Utils
|
import Utils
|
||||||
from Utils import version_tuple, restricted_loads, Version, async_start
|
from Utils import version_tuple, restricted_loads, Version, async_start
|
||||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||||
SlotType
|
SlotType, LocationStore
|
||||||
|
|
||||||
min_client_version = Version(0, 1, 6)
|
min_client_version = Version(0, 1, 6)
|
||||||
colorama.init()
|
colorama.init()
|
||||||
@@ -152,7 +152,9 @@ class Context:
|
|||||||
"compatibility": int}
|
"compatibility": int}
|
||||||
# team -> slot id -> list of clients authenticated to slot.
|
# team -> slot id -> list of clients authenticated to slot.
|
||||||
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
|
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
|
||||||
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
|
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
|
||||||
|
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
|
||||||
|
hints_used: typing.Dict[typing.Tuple[int, int], int]
|
||||||
groups: typing.Dict[int, typing.Set[int]]
|
groups: typing.Dict[int, typing.Set[int]]
|
||||||
save_version = 2
|
save_version = 2
|
||||||
stored_data: typing.Dict[str, object]
|
stored_data: typing.Dict[str, object]
|
||||||
@@ -187,8 +189,6 @@ class Context:
|
|||||||
self.player_name_lookup: typing.Dict[str, team_slot] = {}
|
self.player_name_lookup: typing.Dict[str, team_slot] = {}
|
||||||
self.connect_names = {} # names of slots clients can connect to
|
self.connect_names = {} # names of slots clients can connect to
|
||||||
self.allow_releases = {}
|
self.allow_releases = {}
|
||||||
# player location_id item_id target_player_id
|
|
||||||
self.locations = {}
|
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.server_password = server_password
|
self.server_password = server_password
|
||||||
@@ -284,6 +284,7 @@ class Context:
|
|||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
if self.log_network:
|
if self.log_network:
|
||||||
logging.info(f"Outgoing message: {msg}")
|
logging.info(f"Outgoing message: {msg}")
|
||||||
@@ -297,6 +298,7 @@ class Context:
|
|||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
logging.exception("Exception during send_encoded_msgs")
|
logging.exception("Exception during send_encoded_msgs")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
if self.log_network:
|
if self.log_network:
|
||||||
logging.info(f"Outgoing message: {msg}")
|
logging.info(f"Outgoing message: {msg}")
|
||||||
@@ -311,6 +313,7 @@ class Context:
|
|||||||
websockets.broadcast(sockets, msg)
|
websockets.broadcast(sockets, msg)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logging.exception("Exception during broadcast_send_encoded_msgs")
|
logging.exception("Exception during broadcast_send_encoded_msgs")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
if self.log_network:
|
if self.log_network:
|
||||||
logging.info(f"Outgoing broadcast: {msg}")
|
logging.info(f"Outgoing broadcast: {msg}")
|
||||||
@@ -413,7 +416,7 @@ class Context:
|
|||||||
self.seed_name = decoded_obj["seed_name"]
|
self.seed_name = decoded_obj["seed_name"]
|
||||||
self.random.seed(self.seed_name)
|
self.random.seed(self.seed_name)
|
||||||
self.connect_names = decoded_obj['connect_names']
|
self.connect_names = decoded_obj['connect_names']
|
||||||
self.locations = decoded_obj['locations']
|
self.locations = LocationStore(decoded_obj.pop("locations")) # pre-emptively free memory
|
||||||
self.slot_data = decoded_obj['slot_data']
|
self.slot_data = decoded_obj['slot_data']
|
||||||
for slot, data in self.slot_data.items():
|
for slot, data in self.slot_data.items():
|
||||||
self.read_data[f"slot_data_{slot}"] = lambda data=data: data
|
self.read_data[f"slot_data_{slot}"] = lambda data=data: data
|
||||||
@@ -792,7 +795,7 @@ async def on_client_joined(ctx: Context, client: Client):
|
|||||||
ctx.broadcast_text_all(
|
ctx.broadcast_text_all(
|
||||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||||
f"Client({version_str}), {client.tags}).",
|
f"Client({version_str}), {client.tags}.",
|
||||||
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
|
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
|
||||||
ctx.notify_client(client, "Now that you are connected, "
|
ctx.notify_client(client, "Now that you are connected, "
|
||||||
"you can use !help to list commands to run via the server. "
|
"you can use !help to list commands to run via the server. "
|
||||||
@@ -902,11 +905,7 @@ def release_player(ctx: Context, team: int, slot: int):
|
|||||||
|
|
||||||
def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
||||||
"""register any locations that are in the multidata, pointing towards this player"""
|
"""register any locations that are in the multidata, pointing towards this player"""
|
||||||
all_locations = collections.defaultdict(set)
|
all_locations = ctx.locations.get_for_player(slot)
|
||||||
for source_slot, location_data in ctx.locations.items():
|
|
||||||
for location_id, values in location_data.items():
|
|
||||||
if values[1] == slot:
|
|
||||||
all_locations[source_slot].add(location_id)
|
|
||||||
|
|
||||||
ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
|
ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
|
||||||
% (ctx.player_names[(team, slot)], team + 1),
|
% (ctx.player_names[(team, slot)], team + 1),
|
||||||
@@ -925,11 +924,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
|||||||
|
|
||||||
|
|
||||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
items = []
|
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
||||||
for location_id in ctx.locations[slot]:
|
|
||||||
if location_id not in ctx.location_checks[team, slot]:
|
|
||||||
items.append(ctx.locations[slot][location_id][0]) # item ID
|
|
||||||
return sorted(items)
|
|
||||||
|
|
||||||
|
|
||||||
def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem):
|
def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem):
|
||||||
@@ -977,9 +972,8 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
|
|||||||
slots.add(group_id)
|
slots.add(group_id)
|
||||||
|
|
||||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||||
for finding_player, check_data in ctx.locations.items():
|
for finding_player, location_id, item_id, receiving_player, item_flags \
|
||||||
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
in ctx.locations.find_item(slots, seeked_item_id):
|
||||||
if receiving_player in slots and item_id == seeked_item_id:
|
|
||||||
found = location_id in ctx.location_checks[team, finding_player]
|
found = location_id in ctx.location_checks[team, finding_player]
|
||||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||||
@@ -1555,15 +1549,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
|
|
||||||
def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
return [location_id for
|
return ctx.locations.get_checked(ctx.location_checks, team, slot)
|
||||||
location_id in ctx.locations[slot] if
|
|
||||||
location_id in ctx.location_checks[team, slot]]
|
|
||||||
|
|
||||||
|
|
||||||
def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
return [location_id for
|
return ctx.locations.get_missing(ctx.location_checks, team, 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:
|
def get_client_points(ctx: Context, client: Client) -> int:
|
||||||
@@ -2128,13 +2118,15 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
async def console(ctx: Context):
|
async def console(ctx: Context):
|
||||||
import sys
|
import sys
|
||||||
queue = asyncio.Queue()
|
queue = asyncio.Queue()
|
||||||
Utils.stream_input(sys.stdin, queue)
|
worker = Utils.stream_input(sys.stdin, queue)
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
try:
|
try:
|
||||||
# I don't get why this while loop is needed. Works fine without it on clients,
|
# I don't get why this while loop is needed. Works fine without it on clients,
|
||||||
# but the queue.get() for server never fulfills if the queue is empty when entering the await.
|
# but the queue.get() for server never fulfills if the queue is empty when entering the await.
|
||||||
while queue.qsize() == 0:
|
while queue.qsize() == 0:
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
|
if not worker.is_alive():
|
||||||
|
return
|
||||||
input_text = await queue.get()
|
input_text = await queue.get()
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
ctx.commandprocessor(input_text)
|
ctx.commandprocessor(input_text)
|
||||||
@@ -2145,7 +2137,7 @@ async def console(ctx: Context):
|
|||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
defaults = Utils.get_options()["server_options"]
|
defaults = Utils.get_options()["server_options"].as_dict()
|
||||||
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
|
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
|
||||||
parser.add_argument('--host', default=defaults["host"])
|
parser.add_argument('--host', default=defaults["host"])
|
||||||
parser.add_argument('--port', default=defaults["port"], type=int)
|
parser.add_argument('--port', default=defaults["port"], type=int)
|
||||||
@@ -2254,12 +2246,15 @@ async def main(args: argparse.Namespace):
|
|||||||
if not isinstance(e, ImportError):
|
if not isinstance(e, ImportError):
|
||||||
logging.error(f"Failed to load tkinter ({e})")
|
logging.error(f"Failed to load tkinter ({e})")
|
||||||
logging.info("Pass a multidata filename on command line to run headless.")
|
logging.info("Pass a multidata filename on command line to run headless.")
|
||||||
exit(1)
|
# when cx_Freeze'd the built-in exit is not available, so we import sys.exit instead
|
||||||
|
import sys
|
||||||
|
sys.exit(1)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if not data_filename:
|
if not data_filename:
|
||||||
logging.info("No file selected. Exiting.")
|
logging.info("No file selected. Exiting.")
|
||||||
exit(1)
|
import sys
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ctx.load(data_filename, args.use_embedded_options)
|
ctx.load(data_filename, args.use_embedded_options)
|
||||||
|
|||||||
83
NetUtils.py
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import typing
|
import typing
|
||||||
import enum
|
import enum
|
||||||
|
import warnings
|
||||||
from json import JSONEncoder, JSONDecoder
|
from json import JSONEncoder, JSONDecoder
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
@@ -343,3 +344,85 @@ class Hint(typing.NamedTuple):
|
|||||||
@property
|
@property
|
||||||
def local(self):
|
def local(self):
|
||||||
return self.receiving_player == self.finding_player
|
return self.receiving_player == self.finding_player
|
||||||
|
|
||||||
|
|
||||||
|
class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
|
||||||
|
def __init__(self, values: typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
|
||||||
|
super().__init__(values)
|
||||||
|
|
||||||
|
if not self:
|
||||||
|
raise ValueError(f"Rejecting game with 0 players")
|
||||||
|
|
||||||
|
if len(self) != max(self):
|
||||||
|
raise ValueError("Player IDs not continuous")
|
||||||
|
|
||||||
|
if len(self.get(0, {})):
|
||||||
|
raise ValueError("Invalid player id 0 for location")
|
||||||
|
|
||||||
|
def find_item(self, slots: typing.Set[int], seeked_item_id: int
|
||||||
|
) -> typing.Generator[typing.Tuple[int, int, int, int, int], None, None]:
|
||||||
|
for finding_player, check_data in self.items():
|
||||||
|
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||||
|
if receiving_player in slots and item_id == seeked_item_id:
|
||||||
|
yield finding_player, location_id, item_id, receiving_player, item_flags
|
||||||
|
|
||||||
|
def get_for_player(self, slot: int) -> typing.Dict[int, typing.Set[int]]:
|
||||||
|
import collections
|
||||||
|
all_locations: typing.Dict[int, typing.Set[int]] = collections.defaultdict(set)
|
||||||
|
for source_slot, location_data in self.items():
|
||||||
|
for location_id, values in location_data.items():
|
||||||
|
if values[1] == slot:
|
||||||
|
all_locations[source_slot].add(location_id)
|
||||||
|
return all_locations
|
||||||
|
|
||||||
|
def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
|
||||||
|
) -> typing.List[int]:
|
||||||
|
checked = state[team, slot]
|
||||||
|
if not checked:
|
||||||
|
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||||
|
return []
|
||||||
|
return [location_id for
|
||||||
|
location_id in self[slot] if
|
||||||
|
location_id in checked]
|
||||||
|
|
||||||
|
def get_missing(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
|
||||||
|
) -> typing.List[int]:
|
||||||
|
checked = state[team, slot]
|
||||||
|
if not checked:
|
||||||
|
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||||
|
return list(self[slot])
|
||||||
|
return [location_id for
|
||||||
|
location_id in self[slot] if
|
||||||
|
location_id not in checked]
|
||||||
|
|
||||||
|
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
|
||||||
|
) -> typing.List[int]:
|
||||||
|
checked = state[team, slot]
|
||||||
|
player_locations = self[slot]
|
||||||
|
return sorted([player_locations[location_id][0] for
|
||||||
|
location_id in player_locations if
|
||||||
|
location_id not in checked])
|
||||||
|
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
||||||
|
LocationStore = _LocationStore
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from _speedups import LocationStore
|
||||||
|
import _speedups
|
||||||
|
import os.path
|
||||||
|
if os.path.isfile("_speedups.pyx") and os.path.getctime(_speedups.__file__) < os.path.getctime("_speedups.pyx"):
|
||||||
|
warnings.warn(f"{_speedups.__file__} outdated! "
|
||||||
|
f"Please rebuild with `cythonize -b -i _speedups.pyx` or delete it!")
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import pyximport
|
||||||
|
pyximport.install()
|
||||||
|
except ImportError:
|
||||||
|
pyximport = None
|
||||||
|
try:
|
||||||
|
from _speedups import LocationStore
|
||||||
|
except ImportError:
|
||||||
|
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
||||||
|
"Install a matching C++ compiler for your platform to compile _speedups.")
|
||||||
|
LocationStore = _LocationStore
|
||||||
|
|||||||
@@ -296,8 +296,6 @@ async def patch_and_run_game(apz5_file):
|
|||||||
comp_path = base_name + '.z64'
|
comp_path = base_name + '.z64'
|
||||||
# Load vanilla ROM, patch file, compress ROM
|
# Load vanilla ROM, patch file, compress ROM
|
||||||
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
|
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
|
||||||
if not os.path.exists(rom_file_name):
|
|
||||||
rom_file_name = Utils.user_path(rom_file_name)
|
|
||||||
rom = Rom(rom_file_name)
|
rom = Rom(rom_file_name)
|
||||||
|
|
||||||
sub_file = None
|
sub_file = None
|
||||||
|
|||||||
21
Options.py
@@ -1,13 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
|
||||||
import math
|
import math
|
||||||
import numbers
|
import numbers
|
||||||
import typing
|
|
||||||
import random
|
import random
|
||||||
|
import typing
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from schema import And, Optional, Or, Schema
|
||||||
|
|
||||||
from schema import Schema, And, Or, Optional
|
|
||||||
from Utils import get_fuzzy_results
|
from Utils import get_fuzzy_results
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
@@ -769,7 +771,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
|||||||
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
||||||
|
|
||||||
|
|
||||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
|
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||||
default: typing.Dict[str, typing.Any] = {}
|
default: typing.Dict[str, typing.Any] = {}
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
|
|
||||||
@@ -787,8 +789,14 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
|
|||||||
def get_option_name(self, value):
|
def get_option_name(self, value):
|
||||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __getitem__(self, item: str) -> typing.Any:
|
||||||
return item in self.value
|
return self.value.__getitem__(item)
|
||||||
|
|
||||||
|
def __iter__(self) -> typing.Iterator[str]:
|
||||||
|
return self.value.__iter__()
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return self.value.__len__()
|
||||||
|
|
||||||
|
|
||||||
class ItemDict(OptionDict):
|
class ItemDict(OptionDict):
|
||||||
@@ -949,6 +957,7 @@ class DeathLink(Toggle):
|
|||||||
|
|
||||||
class ItemLinks(OptionList):
|
class ItemLinks(OptionList):
|
||||||
"""Share part of your item pool with other players."""
|
"""Share part of your item pool with other players."""
|
||||||
|
display_name = "Item Links"
|
||||||
default = []
|
default = []
|
||||||
schema = Schema([
|
schema = Schema([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ for location in location_data:
|
|||||||
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
|
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
|
||||||
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
|
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
|
||||||
|
|
||||||
|
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
|
||||||
|
and location.address is not None}
|
||||||
|
|
||||||
SYSTEM_MESSAGE_ID = 0
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
|
||||||
@@ -72,6 +75,7 @@ class GBContext(CommonContext):
|
|||||||
self.items_handling = 0b001
|
self.items_handling = 0b001
|
||||||
self.sent_release = False
|
self.sent_release = False
|
||||||
self.sent_collect = False
|
self.sent_collect = False
|
||||||
|
self.auto_hints = set()
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
@@ -153,6 +157,33 @@ async def parse_locations(data: List, ctx: GBContext):
|
|||||||
locations.append(loc_id)
|
locations.append(loc_id)
|
||||||
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
|
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
|
||||||
locations.append(loc_id)
|
locations.append(loc_id)
|
||||||
|
|
||||||
|
hints = []
|
||||||
|
if flags["EventFlag"][280] & 16:
|
||||||
|
hints.append("Cerulean Bicycle Shop")
|
||||||
|
if flags["EventFlag"][280] & 32:
|
||||||
|
hints.append("Route 2 Gate - Oak's Aide")
|
||||||
|
if flags["EventFlag"][280] & 64:
|
||||||
|
hints.append("Route 11 Gate 2F - Oak's Aide")
|
||||||
|
if flags["EventFlag"][280] & 128:
|
||||||
|
hints.append("Route 15 Gate 2F - Oak's Aide")
|
||||||
|
if flags["EventFlag"][281] & 1:
|
||||||
|
hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2",
|
||||||
|
"Celadon Prize Corner - Item Prize 3"]
|
||||||
|
if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id["Fossil - Choice B"]
|
||||||
|
not in ctx.checked_locations):
|
||||||
|
hints.append("Fossil - Choice B")
|
||||||
|
elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"]
|
||||||
|
not in ctx.checked_locations):
|
||||||
|
hints.append("Fossil - Choice A")
|
||||||
|
hints = [
|
||||||
|
location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and
|
||||||
|
location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked
|
||||||
|
]
|
||||||
|
if hints:
|
||||||
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
|
||||||
|
ctx.auto_hints.update(hints)
|
||||||
|
|
||||||
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
|
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||||
await ctx.send_msgs([
|
await ctx.send_msgs([
|
||||||
{"cmd": "StatusUpdate",
|
{"cmd": "StatusUpdate",
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ Currently, the following games are supported:
|
|||||||
* Bumper Stickers
|
* Bumper Stickers
|
||||||
* Mega Man Battle Network 3: Blue Version
|
* Mega Man Battle Network 3: Blue Version
|
||||||
* Muse Dash
|
* Muse Dash
|
||||||
|
* DOOM 1993
|
||||||
|
* Terraria
|
||||||
|
|
||||||
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
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|||||||
15
SNIClient.py
@@ -68,12 +68,11 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
|||||||
options = snes_options.split()
|
options = snes_options.split()
|
||||||
num_options = len(options)
|
num_options = len(options)
|
||||||
|
|
||||||
if num_options > 0:
|
|
||||||
snes_device_number = int(options[0])
|
|
||||||
|
|
||||||
if num_options > 1:
|
if num_options > 1:
|
||||||
snes_address = options[0]
|
snes_address = options[0]
|
||||||
snes_device_number = int(options[1])
|
snes_device_number = int(options[1])
|
||||||
|
elif num_options > 0:
|
||||||
|
snes_device_number = int(options[0])
|
||||||
|
|
||||||
self.ctx.snes_reconnect_address = None
|
self.ctx.snes_reconnect_address = None
|
||||||
if self.ctx.snes_connect_task:
|
if self.ctx.snes_connect_task:
|
||||||
@@ -565,12 +564,14 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
|
|||||||
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||||
try:
|
try:
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
while data:
|
||||||
# REVIEW: above: `if snes_socket is None: return False`
|
# Divide the write into packets of 256 bytes.
|
||||||
# Does it need to be checked again?
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
await ctx.snes_socket.send(data)
|
await ctx.snes_socket.send(data[:256])
|
||||||
|
address += 256
|
||||||
|
data = data[256:]
|
||||||
else:
|
else:
|
||||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
|
|||||||
1050
Starcraft2Client.py
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
import typing
|
import typing
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
@@ -11,7 +12,7 @@ from NetUtils import NetworkItem, ClientStatus
|
|||||||
from worlds import undertale
|
from worlds import undertale
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
from CommonClient import CommonContext, server_loop, \
|
from CommonClient import CommonContext, server_loop, \
|
||||||
gui_enabled, ClientCommandProcessor, get_base_parser
|
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||||
from Utils import async_start
|
from Utils import async_start
|
||||||
|
|
||||||
|
|
||||||
@@ -28,25 +29,31 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_patch(self):
|
def _cmd_patch(self):
|
||||||
"""Patch the game."""
|
"""Patch the game."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patched.")
|
self.output("Patched.")
|
||||||
|
|
||||||
|
def _cmd_savepath(self, directory: str):
|
||||||
|
"""Redirect to proper save data folder. (Use before connecting!)"""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.ctx.save_game_folder = directory
|
||||||
|
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||||
"""Patch the game automatically."""
|
"""Patch the game automatically."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||||
tempInstall = steaminstall
|
tempInstall = steaminstall
|
||||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
tempInstall = None
|
tempInstall = None
|
||||||
if tempInstall is None:
|
if tempInstall is None:
|
||||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
if not os.path.exists(tempInstall):
|
||||||
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
elif not os.path.exists(tempInstall):
|
elif not os.path.exists(tempInstall):
|
||||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
if not os.path.exists(tempInstall):
|
||||||
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
||||||
@@ -54,8 +61,8 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
else:
|
else:
|
||||||
for file_name in os.listdir(tempInstall):
|
for file_name in os.listdir(tempInstall):
|
||||||
if file_name != "steam_api.dll":
|
if file_name != "steam_api.dll":
|
||||||
shutil.copy(tempInstall+"\\"+file_name,
|
shutil.copy(os.path.join(tempInstall, file_name),
|
||||||
os.getcwd() + "\\Undertale\\" + file_name)
|
os.path.join(os.getcwd(), "Undertale", file_name))
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patching successful!")
|
self.output("Patching successful!")
|
||||||
|
|
||||||
@@ -92,6 +99,7 @@ class UndertaleContext(CommonContext):
|
|||||||
def __init__(self, server_address, password):
|
def __init__(self, server_address, password):
|
||||||
super().__init__(server_address, password)
|
super().__init__(server_address, password)
|
||||||
self.pieces_needed = 0
|
self.pieces_needed = 0
|
||||||
|
self.finished_game = False
|
||||||
self.game = "Undertale"
|
self.game = "Undertale"
|
||||||
self.got_deathlink = False
|
self.got_deathlink = False
|
||||||
self.syncing = False
|
self.syncing = False
|
||||||
@@ -99,15 +107,17 @@ class UndertaleContext(CommonContext):
|
|||||||
self.tem_armor = False
|
self.tem_armor = False
|
||||||
self.completed_count = 0
|
self.completed_count = 0
|
||||||
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||||
|
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
||||||
|
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
|
||||||
def patch_game(self):
|
def patch_game(self):
|
||||||
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
|
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
|
||||||
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
|
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
|
||||||
f.write(patchedFile)
|
f.write(patchedFile)
|
||||||
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
|
||||||
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
|
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites",
|
||||||
"Which Character.txt"), "w") as f:
|
"Which Character.txt")), "w") as f:
|
||||||
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
||||||
"line other than this one.\n", "frisk"])
|
"line other than this one.\n", "frisk"])
|
||||||
f.close()
|
f.close()
|
||||||
@@ -227,7 +237,7 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
f.close()
|
f.close()
|
||||||
filename = f"check.spot"
|
filename = f"check.spot"
|
||||||
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
||||||
for ss in ctx.checked_locations:
|
for ss in set(args["checked_locations"]):
|
||||||
f.write(str(ss-12000)+"\n")
|
f.write(str(ss-12000)+"\n")
|
||||||
f.close()
|
f.close()
|
||||||
elif cmd == "LocationInfo":
|
elif cmd == "LocationInfo":
|
||||||
@@ -353,14 +363,14 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
if "checked_locations" in args:
|
if "checked_locations" in args:
|
||||||
filename = f"check.spot"
|
filename = f"check.spot"
|
||||||
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
||||||
for ss in ctx.checked_locations:
|
for ss in set(args["checked_locations"]):
|
||||||
f.write(str(ss-12000)+"\n")
|
f.write(str(ss-12000)+"\n")
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
elif cmd == "Bounced":
|
elif cmd == "Bounced":
|
||||||
tags = args.get("tags", [])
|
tags = args.get("tags", [])
|
||||||
if "Online" in tags:
|
if "Online" in tags:
|
||||||
data = args.get("worlds/undertale/data", {})
|
data = args.get("data", {})
|
||||||
if data["player"] != ctx.slot and data["player"] is not None:
|
if data["player"] != ctx.slot and data["player"] is not None:
|
||||||
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
@@ -375,7 +385,7 @@ async def multi_watcher(ctx: UndertaleContext):
|
|||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if "spots.mine" in file and "Online" in ctx.tags:
|
if "spots.mine" in file and "Online" in ctx.tags:
|
||||||
with open(root + "/" + file, "r") as mine:
|
with open(os.path.join(root, file), "r") as mine:
|
||||||
this_x = mine.readline()
|
this_x = mine.readline()
|
||||||
this_y = mine.readline()
|
this_y = mine.readline()
|
||||||
this_room = mine.readline()
|
this_room = mine.readline()
|
||||||
@@ -398,7 +408,7 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if ".item" in file:
|
if ".item" in file:
|
||||||
os.remove(root+"/"+file)
|
os.remove(os.path.join(root, file))
|
||||||
sync_msg = [{"cmd": "Sync"}]
|
sync_msg = [{"cmd": "Sync"}]
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||||
@@ -406,38 +416,42 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
ctx.syncing = False
|
ctx.syncing = False
|
||||||
if ctx.got_deathlink:
|
if ctx.got_deathlink:
|
||||||
ctx.got_deathlink = False
|
ctx.got_deathlink = False
|
||||||
with open(os.path.join(ctx.save_game_folder, "/WelcomeToTheDead.youDied"), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, "WelcomeToTheDead.youDied"), "w") as f:
|
||||||
f.close()
|
f.close()
|
||||||
sending = []
|
sending = []
|
||||||
victory = False
|
victory = False
|
||||||
found_routes = 0
|
found_routes = 0
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if "DontBeMad.mad" in file and "DeathLink" in ctx.tags:
|
if "DontBeMad.mad" in file:
|
||||||
os.remove(root+"/"+file)
|
os.remove(os.path.join(root, file))
|
||||||
|
if "DeathLink" in ctx.tags:
|
||||||
await ctx.send_death()
|
await ctx.send_death()
|
||||||
if "scout" == file:
|
if "scout" == file:
|
||||||
sending = []
|
sending = []
|
||||||
with open(root+"/"+file, "r") as f:
|
try:
|
||||||
|
with open(os.path.join(root, file), "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
for l in lines:
|
for l in lines:
|
||||||
if ctx.server_locations.__contains__(int(l)+12000):
|
if ctx.server_locations.__contains__(int(l)+12000):
|
||||||
sending = sending + [int(l)+12000]
|
sending = sending + [int(l.rstrip('\n'))+12000]
|
||||||
|
finally:
|
||||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||||
"create_as_hint": int(2)}])
|
"create_as_hint": int(2)}])
|
||||||
os.remove(root+"/"+file)
|
os.remove(os.path.join(root, file))
|
||||||
if "check.spot" in file:
|
if "check.spot" in file:
|
||||||
sending = []
|
sending = []
|
||||||
with open(root+"/"+file, "r") as f:
|
try:
|
||||||
|
with open(os.path.join(root, file), "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
for l in lines:
|
for l in lines:
|
||||||
sending = sending+[(int(l))+12000]
|
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||||
message = [{"cmd": "LocationChecks", "locations": sending}]
|
finally:
|
||||||
await ctx.send_msgs(message)
|
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
|
||||||
if "victory" in file and str(ctx.route) in file:
|
if "victory" in file and str(ctx.route) in file:
|
||||||
victory = True
|
victory = True
|
||||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||||
os.remove(root+"/"+file)
|
os.remove(os.path.join(root, file))
|
||||||
if "victory" in file:
|
if "victory" in file:
|
||||||
if str(ctx.route) == "all_routes":
|
if str(ctx.route) == "all_routes":
|
||||||
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
||||||
|
|||||||
359
Utils.py
@@ -13,8 +13,11 @@ import io
|
|||||||
import collections
|
import collections
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
import warnings
|
||||||
|
|
||||||
|
from argparse import Namespace
|
||||||
|
from settings import Settings, get_settings
|
||||||
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||||
from yaml import load, load_all, dump, SafeLoader
|
from yaml import load, load_all, dump, SafeLoader
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -27,6 +30,7 @@ except ImportError:
|
|||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
import tkinter
|
import tkinter
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from BaseClasses import Region
|
||||||
|
|
||||||
|
|
||||||
def tuplize_version(version: str) -> Version:
|
def tuplize_version(version: str) -> Version:
|
||||||
@@ -42,7 +46,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.4.2"
|
__version__ = "0.4.3"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -138,12 +142,15 @@ def user_path(*path: str) -> str:
|
|||||||
user_path.cached_path = local_path()
|
user_path.cached_path = local_path()
|
||||||
else:
|
else:
|
||||||
user_path.cached_path = home_path()
|
user_path.cached_path = home_path()
|
||||||
# populate home from local - TODO: upgrade feature
|
# populate home from local
|
||||||
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
|
if user_path.cached_path != local_path():
|
||||||
|
import filecmp
|
||||||
|
if not os.path.exists(user_path("manifest.json")) or \
|
||||||
|
not filecmp.cmp(local_path("manifest.json"), user_path("manifest.json"), shallow=True):
|
||||||
import shutil
|
import shutil
|
||||||
for dn in ("Players", "data/sprites"):
|
for dn in ("Players", "data/sprites"):
|
||||||
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
||||||
for fn in ("manifest.json", "host.yaml"):
|
for fn in ("manifest.json",):
|
||||||
shutil.copy2(local_path(fn), user_path(fn))
|
shutil.copy2(local_path(fn), user_path(fn))
|
||||||
|
|
||||||
return os.path.join(user_path.cached_path, *path)
|
return os.path.join(user_path.cached_path, *path)
|
||||||
@@ -210,7 +217,13 @@ def get_cert_none_ssl_context():
|
|||||||
def get_public_ipv4() -> str:
|
def get_public_ipv4() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
try:
|
||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
except socket.gaierror:
|
||||||
|
# if hostname or resolvconf is not set up properly, this may fail
|
||||||
|
warnings.warn("Could not resolve own hostname, falling back to 127.0.0.1")
|
||||||
|
ip = "127.0.0.1"
|
||||||
|
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
@@ -228,7 +241,13 @@ def get_public_ipv4() -> str:
|
|||||||
def get_public_ipv6() -> str:
|
def get_public_ipv6() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
try:
|
||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
except socket.gaierror:
|
||||||
|
# if hostname or resolvconf is not set up properly, this may fail
|
||||||
|
warnings.warn("Could not resolve own hostname, falling back to ::1")
|
||||||
|
ip = "::1"
|
||||||
|
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
@@ -238,155 +257,15 @@ def get_public_ipv6() -> str:
|
|||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
|
||||||
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
|
OptionsType = Settings # TODO: remove ~2 versions after 0.4.1
|
||||||
|
|
||||||
|
|
||||||
@cache_argsless
|
@cache_argsless
|
||||||
def get_default_options() -> OptionsType:
|
def get_default_options() -> Settings: # TODO: remove ~2 versions after 0.4.1
|
||||||
# Refer to host.yaml for comments as to what all these options mean.
|
return Settings(None)
|
||||||
options = {
|
|
||||||
"general_options": {
|
|
||||||
"output_path": "output",
|
|
||||||
},
|
|
||||||
"factorio_options": {
|
|
||||||
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
|
|
||||||
"filter_item_sends": False,
|
|
||||||
"bridge_chat_out": True,
|
|
||||||
},
|
|
||||||
"sni_options": {
|
|
||||||
"sni_path": "SNI",
|
|
||||||
"snes_rom_start": True,
|
|
||||||
},
|
|
||||||
"sm_options": {
|
|
||||||
"rom_file": "Super Metroid (JU).sfc",
|
|
||||||
},
|
|
||||||
"soe_options": {
|
|
||||||
"rom_file": "Secret of Evermore (USA).sfc",
|
|
||||||
},
|
|
||||||
"lttp_options": {
|
|
||||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
|
||||||
},
|
|
||||||
"ladx_options": {
|
|
||||||
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
|
|
||||||
},
|
|
||||||
"server_options": {
|
|
||||||
"host": None,
|
|
||||||
"port": 38281,
|
|
||||||
"password": None,
|
|
||||||
"multidata": None,
|
|
||||||
"savefile": None,
|
|
||||||
"disable_save": False,
|
|
||||||
"loglevel": "info",
|
|
||||||
"server_password": None,
|
|
||||||
"disable_item_cheat": False,
|
|
||||||
"location_check_points": 1,
|
|
||||||
"hint_cost": 10,
|
|
||||||
"release_mode": "goal",
|
|
||||||
"collect_mode": "disabled",
|
|
||||||
"remaining_mode": "goal",
|
|
||||||
"auto_shutdown": 0,
|
|
||||||
"compatibility": 2,
|
|
||||||
"log_network": 0
|
|
||||||
},
|
|
||||||
"generator": {
|
|
||||||
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
|
|
||||||
"player_files_path": "Players",
|
|
||||||
"players": 0,
|
|
||||||
"weights_file_path": "weights.yaml",
|
|
||||||
"meta_file_path": "meta.yaml",
|
|
||||||
"spoiler": 3,
|
|
||||||
"glitch_triforce_room": 1,
|
|
||||||
"race": 0,
|
|
||||||
"plando_options": "bosses",
|
|
||||||
},
|
|
||||||
"minecraft_options": {
|
|
||||||
"forge_directory": "Minecraft Forge server",
|
|
||||||
"max_heap_size": "2G",
|
|
||||||
"release_channel": "release"
|
|
||||||
},
|
|
||||||
"oot_options": {
|
|
||||||
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
|
||||||
"rom_start": True
|
|
||||||
},
|
|
||||||
"dkc3_options": {
|
|
||||||
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
|
|
||||||
},
|
|
||||||
"smw_options": {
|
|
||||||
"rom_file": "Super Mario World (USA).sfc",
|
|
||||||
},
|
|
||||||
"zillion_options": {
|
|
||||||
"rom_file": "Zillion (UE) [!].sms",
|
|
||||||
# RetroArch doesn't make it easy to launch a game from the command line.
|
|
||||||
# You have to know the path to the emulator core library on the user's computer.
|
|
||||||
"rom_start": "retroarch",
|
|
||||||
},
|
|
||||||
"pokemon_rb_options": {
|
|
||||||
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
|
|
||||||
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
|
|
||||||
"rom_start": True
|
|
||||||
},
|
|
||||||
"ffr_options": {
|
|
||||||
"display_msgs": True,
|
|
||||||
},
|
|
||||||
"lufia2ac_options": {
|
|
||||||
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
|
||||||
},
|
|
||||||
"tloz_options": {
|
|
||||||
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
|
|
||||||
"rom_start": True,
|
|
||||||
"display_msgs": True,
|
|
||||||
},
|
|
||||||
"wargroove_options": {
|
|
||||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
|
||||||
},
|
|
||||||
"mmbn3_options": {
|
|
||||||
"rom_file": "Mega Man Battle Network 3 - Blue Version (USA).gba",
|
|
||||||
"rom_start": True
|
|
||||||
},
|
|
||||||
"adventure_options": {
|
|
||||||
"rom_file": "ADVNTURE.BIN",
|
|
||||||
"display_msgs": True,
|
|
||||||
"rom_start": True,
|
|
||||||
"rom_args": ""
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
|
|
||||||
|
|
||||||
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
|
get_options = get_settings # TODO: add a warning ~2 versions after 0.4.1 and remove once all games are ported
|
||||||
for key, value in src.items():
|
|
||||||
new_keys = keys.copy()
|
|
||||||
new_keys.append(key)
|
|
||||||
option_name = '.'.join(new_keys)
|
|
||||||
if key not in dest:
|
|
||||||
dest[key] = value
|
|
||||||
if filename.endswith("options.yaml"):
|
|
||||||
logging.info(f"Warning: {filename} is missing {option_name}")
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
if not isinstance(dest.get(key, None), dict):
|
|
||||||
if filename.endswith("options.yaml"):
|
|
||||||
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
|
|
||||||
dest[key] = value
|
|
||||||
else:
|
|
||||||
dest[key] = update_options(value, dest[key], filename, new_keys)
|
|
||||||
return dest
|
|
||||||
|
|
||||||
|
|
||||||
@cache_argsless
|
|
||||||
def get_options() -> OptionsType:
|
|
||||||
filenames = ("options.yaml", "host.yaml")
|
|
||||||
locations: typing.List[str] = []
|
|
||||||
if os.path.join(os.getcwd()) != local_path():
|
|
||||||
locations += filenames # use files from cwd only if it's not the local_path
|
|
||||||
locations += [user_path(filename) for filename in filenames]
|
|
||||||
|
|
||||||
for location in locations:
|
|
||||||
if os.path.exists(location):
|
|
||||||
with open(location) as f:
|
|
||||||
options = parse_yaml(f.read())
|
|
||||||
return update_options(get_default_options(), options, location, list())
|
|
||||||
|
|
||||||
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
|
|
||||||
|
|
||||||
|
|
||||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||||
@@ -454,12 +333,27 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(f"Could not store data package: {e}")
|
logging.debug(f"Could not store data package: {e}")
|
||||||
|
|
||||||
|
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
||||||
|
import LttPAdjuster
|
||||||
|
adjuster_settings = Namespace()
|
||||||
|
if game_name == LttPAdjuster.GAME_ALTTP:
|
||||||
|
return LttPAdjuster.get_argparser().parse_known_args(args=[])[0]
|
||||||
|
|
||||||
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
|
|
||||||
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
|
||||||
return adjuster_settings
|
return adjuster_settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_adjuster_settings_no_defaults(game_name: str) -> Namespace:
|
||||||
|
return persistent_load().get("adjuster", {}).get(game_name, Namespace())
|
||||||
|
|
||||||
|
|
||||||
|
def get_adjuster_settings(game_name: str) -> Namespace:
|
||||||
|
adjuster_settings = get_adjuster_settings_no_defaults(game_name)
|
||||||
|
default_settings = get_default_adjuster_settings(game_name)
|
||||||
|
|
||||||
|
# Fill in any arguments from the argparser that we haven't seen before
|
||||||
|
return Namespace(**vars(adjuster_settings), **{k:v for k,v in vars(default_settings).items() if k not in vars(adjuster_settings)})
|
||||||
|
|
||||||
|
|
||||||
@cache_argsless
|
@cache_argsless
|
||||||
def get_unique_identifier():
|
def get_unique_identifier():
|
||||||
uuid = persistent_load().get("client", {}).get("uuid", None)
|
uuid = persistent_load().get("client", {}).get("uuid", None)
|
||||||
@@ -479,11 +373,13 @@ safe_builtins = frozenset((
|
|||||||
|
|
||||||
|
|
||||||
class RestrictedUnpickler(pickle.Unpickler):
|
class RestrictedUnpickler(pickle.Unpickler):
|
||||||
|
generic_properties_module: Optional[object]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||||
self.options_module = importlib.import_module("Options")
|
self.options_module = importlib.import_module("Options")
|
||||||
self.net_utils_module = importlib.import_module("NetUtils")
|
self.net_utils_module = importlib.import_module("NetUtils")
|
||||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
self.generic_properties_module = None
|
||||||
|
|
||||||
def find_class(self, module, name):
|
def find_class(self, module, name):
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
@@ -493,6 +389,8 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
return getattr(self.net_utils_module, name)
|
return getattr(self.net_utils_module, name)
|
||||||
# Options and Plando are unpickled by WebHost -> Generate
|
# Options and Plando are unpickled by WebHost -> Generate
|
||||||
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
||||||
|
if not self.generic_properties_module:
|
||||||
|
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||||
return getattr(self.generic_properties_module, name)
|
return getattr(self.generic_properties_module, name)
|
||||||
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
||||||
if module.lower().endswith("options"):
|
if module.lower().endswith("options"):
|
||||||
@@ -677,7 +575,7 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
|
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \
|
||||||
-> typing.Optional[str]:
|
-> typing.Optional[str]:
|
||||||
def run(*args: str):
|
def run(*args: str):
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||||
@@ -688,11 +586,12 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
|||||||
kdialog = which("kdialog")
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||||
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
|
return run(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
|
||||||
zenity = which("zenity")
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
|
selection = (f"--filename={suggest}",) if suggest else ()
|
||||||
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
try:
|
try:
|
||||||
@@ -703,9 +602,47 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
|||||||
f'This attempt was made because open_filename was used for "{title}".')
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
|
except tkinter.TclError:
|
||||||
|
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes))
|
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||||
|
initialfile=suggest or None)
|
||||||
|
|
||||||
|
|
||||||
|
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||||
|
def run(*args: str):
|
||||||
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||||
|
|
||||||
|
if is_linux:
|
||||||
|
# prefer native dialog
|
||||||
|
from shutil import which
|
||||||
|
kdialog = which("kdialog")
|
||||||
|
if kdialog:
|
||||||
|
return run(kdialog, f"--title={title}", "--getexistingdirectory",
|
||||||
|
os.path.abspath(suggest) if suggest else ".")
|
||||||
|
zenity = which("zenity")
|
||||||
|
if zenity:
|
||||||
|
z_filters = ("--directory",)
|
||||||
|
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
||||||
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
|
# fall back to tk
|
||||||
|
try:
|
||||||
|
import tkinter
|
||||||
|
import tkinter.filedialog
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Could not load tkinter, which is likely not installed. '
|
||||||
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
root = tkinter.Tk()
|
||||||
|
except tkinter.TclError:
|
||||||
|
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||||
|
root.withdraw()
|
||||||
|
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
|
||||||
|
|
||||||
|
|
||||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||||
@@ -843,3 +780,113 @@ def freeze_support() -> None:
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
_extend_freeze_support()
|
_extend_freeze_support()
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_regions(root_region: Region, file_name: str, *,
|
||||||
|
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
||||||
|
linetype_ortho: bool = True) -> None:
|
||||||
|
"""Visualize the layout of a world as a PlantUML diagram.
|
||||||
|
|
||||||
|
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||||
|
:param file_name: The name of the destination .puml file.
|
||||||
|
:param show_entrance_names: (default False) If enabled, the name of the entrance will be shown near each connection.
|
||||||
|
:param show_locations: (default True) If enabled, the locations will be listed inside each region.
|
||||||
|
Priority locations will be shown in bold.
|
||||||
|
Excluded locations will be stricken out.
|
||||||
|
Locations without ID will be shown in italics.
|
||||||
|
Locked locations will be shown with a padlock icon.
|
||||||
|
For filled locations, the item name will be shown after the location name.
|
||||||
|
Progression items will be shown in bold.
|
||||||
|
Items without ID will be shown in italics.
|
||||||
|
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||||
|
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||||
|
|
||||||
|
Example usage in World code:
|
||||||
|
from Utils import visualize_regions
|
||||||
|
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
|
||||||
|
|
||||||
|
Example usage in Main code:
|
||||||
|
from Utils import visualize_regions
|
||||||
|
for player in world.player_ids:
|
||||||
|
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
|
||||||
|
"""
|
||||||
|
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
|
||||||
|
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
|
from collections import deque
|
||||||
|
import re
|
||||||
|
|
||||||
|
uml: typing.List[str] = list()
|
||||||
|
seen: typing.Set[Region] = set()
|
||||||
|
regions: typing.Deque[Region] = deque((root_region,))
|
||||||
|
multiworld: MultiWorld = root_region.multiworld
|
||||||
|
|
||||||
|
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
|
||||||
|
name = obj.name
|
||||||
|
if isinstance(obj, Item):
|
||||||
|
name = multiworld.get_name_string_for_object(obj)
|
||||||
|
if obj.advancement:
|
||||||
|
name = f"**{name}**"
|
||||||
|
if obj.code is None:
|
||||||
|
name = f"//{name}//"
|
||||||
|
if isinstance(obj, Location):
|
||||||
|
if obj.progress_type == LocationProgressType.PRIORITY:
|
||||||
|
name = f"**{name}**"
|
||||||
|
elif obj.progress_type == LocationProgressType.EXCLUDED:
|
||||||
|
name = f"--{name}--"
|
||||||
|
if obj.address is None:
|
||||||
|
name = f"//{name}//"
|
||||||
|
return re.sub("[\".:]", "", name)
|
||||||
|
|
||||||
|
def visualize_exits(region: Region) -> None:
|
||||||
|
for exit_ in region.exits:
|
||||||
|
if exit_.connected_region:
|
||||||
|
if show_entrance_names:
|
||||||
|
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
|
||||||
|
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
|
||||||
|
except ValueError:
|
||||||
|
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
|
||||||
|
else:
|
||||||
|
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||||
|
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||||
|
|
||||||
|
def visualize_locations(region: Region) -> None:
|
||||||
|
any_lock = any(location.locked for location in region.locations)
|
||||||
|
for location in region.locations:
|
||||||
|
lock = "<&lock-locked> " if location.locked else "<&lock-unlocked,color=transparent> " if any_lock else ""
|
||||||
|
if location.item:
|
||||||
|
uml.append(f"\"{fmt(region)}\" : {{method}} {lock}{fmt(location)}: {fmt(location.item)}")
|
||||||
|
else:
|
||||||
|
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
|
||||||
|
|
||||||
|
def visualize_region(region: Region) -> None:
|
||||||
|
uml.append(f"class \"{fmt(region)}\"")
|
||||||
|
if show_locations:
|
||||||
|
visualize_locations(region)
|
||||||
|
visualize_exits(region)
|
||||||
|
|
||||||
|
def visualize_other_regions() -> None:
|
||||||
|
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
|
||||||
|
uml.append("package \"other regions\" <<Cloud>> {")
|
||||||
|
for region in other_regions:
|
||||||
|
uml.append(f"class \"{fmt(region)}\"")
|
||||||
|
uml.append("}")
|
||||||
|
|
||||||
|
uml.append("@startuml")
|
||||||
|
uml.append("hide circle")
|
||||||
|
uml.append("hide empty members")
|
||||||
|
if linetype_ortho:
|
||||||
|
uml.append("skinparam linetype ortho")
|
||||||
|
while regions:
|
||||||
|
if (current_region := regions.popleft()) not in seen:
|
||||||
|
seen.add(current_region)
|
||||||
|
visualize_region(current_region)
|
||||||
|
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
|
||||||
|
if show_other_regions:
|
||||||
|
visualize_other_regions()
|
||||||
|
uml.append("@enduml")
|
||||||
|
|
||||||
|
with open(file_name, "wt", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(uml))
|
||||||
|
|||||||
22
WebHost.py
@@ -10,23 +10,19 @@ ModuleUpdate.update()
|
|||||||
|
|
||||||
# in case app gets imported by something like gunicorn
|
# in case app gets imported by something like gunicorn
|
||||||
import Utils
|
import Utils
|
||||||
|
import settings
|
||||||
|
|
||||||
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||||
|
settings.no_gui = True
|
||||||
from WebHostLib import register, app as raw_app
|
|
||||||
from waitress import serve
|
|
||||||
|
|
||||||
from WebHostLib.models import db
|
|
||||||
from WebHostLib.autolauncher import autohost, autogen
|
|
||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
|
||||||
from WebHostLib.options import create as create_options_files
|
|
||||||
|
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||||
|
|
||||||
|
|
||||||
def get_app():
|
def get_app():
|
||||||
|
from WebHostLib import register, cache, app as raw_app
|
||||||
|
from WebHostLib.models import db
|
||||||
|
|
||||||
register()
|
register()
|
||||||
app = raw_app
|
app = raw_app
|
||||||
if os.path.exists(configpath) and not app.config["TESTING"]:
|
if os.path.exists(configpath) and not app.config["TESTING"]:
|
||||||
@@ -38,6 +34,7 @@ def get_app():
|
|||||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||||
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
||||||
|
|
||||||
|
cache.init_app(app)
|
||||||
db.bind(**app.config["PONY"])
|
db.bind(**app.config["PONY"])
|
||||||
db.generate_mapping(create_tables=True)
|
db.generate_mapping(create_tables=True)
|
||||||
return app
|
return app
|
||||||
@@ -72,6 +69,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
|||||||
with zipfile.ZipFile(zipfile_path) as zf:
|
with zipfile.ZipFile(zipfile_path) as zf:
|
||||||
for zfile in zf.infolist():
|
for zfile in zf.infolist():
|
||||||
if not zfile.is_dir() and "/docs/" in zfile.filename:
|
if not zfile.is_dir() and "/docs/" in zfile.filename:
|
||||||
|
zfile.filename = os.path.basename(zfile.filename)
|
||||||
zf.extract(zfile, target_path)
|
zf.extract(zfile, target_path)
|
||||||
else:
|
else:
|
||||||
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
|
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
|
||||||
@@ -117,6 +115,11 @@ if __name__ == "__main__":
|
|||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
multiprocessing.set_start_method('spawn')
|
multiprocessing.set_start_method('spawn')
|
||||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||||
|
|
||||||
|
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||||
|
from WebHostLib.autolauncher import autohost, autogen
|
||||||
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
try:
|
try:
|
||||||
update_sprites_lttp()
|
update_sprites_lttp()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -133,4 +136,5 @@ if __name__ == "__main__":
|
|||||||
if app.config["DEBUG"]:
|
if app.config["DEBUG"]:
|
||||||
app.run(debug=True, port=app.config["PORT"])
|
app.run(debug=True, port=app.config["PORT"])
|
||||||
else:
|
else:
|
||||||
|
from waitress import serve
|
||||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ app.config["PONY"] = {
|
|||||||
'create_db': True
|
'create_db': True
|
||||||
}
|
}
|
||||||
app.config["MAX_ROLL"] = 20
|
app.config["MAX_ROLL"] = 20
|
||||||
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
|
app.config["CACHE_TYPE"] = "SimpleCache"
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
app.config["HOST_ADDRESS"] = ""
|
app.config["HOST_ADDRESS"] = ""
|
||||||
|
|
||||||
cache = Cache(app)
|
cache = Cache()
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
@@ -13,55 +11,7 @@ from datetime import timedelta, datetime
|
|||||||
from pony.orm import db_session, select, commit
|
from pony.orm import db_session, select, commit
|
||||||
|
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
|
from .locker import Locker, AlreadyRunningException
|
||||||
|
|
||||||
class CommonLocker():
|
|
||||||
"""Uses a file lock to signal that something is already running"""
|
|
||||||
lock_folder = "file_locks"
|
|
||||||
|
|
||||||
def __init__(self, lockname: str, folder=None):
|
|
||||||
if folder:
|
|
||||||
self.lock_folder = folder
|
|
||||||
os.makedirs(self.lock_folder, exist_ok=True)
|
|
||||||
self.lockname = lockname
|
|
||||||
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRunningException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
class Locker(CommonLocker):
|
|
||||||
def __enter__(self):
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.lockfile):
|
|
||||||
os.unlink(self.lockfile)
|
|
||||||
self.fp = os.open(
|
|
||||||
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
|
||||||
except OSError as e:
|
|
||||||
raise AlreadyRunningException() from e
|
|
||||||
|
|
||||||
def __exit__(self, _type, value, tb):
|
|
||||||
fp = getattr(self, "fp", None)
|
|
||||||
if fp:
|
|
||||||
os.close(self.fp)
|
|
||||||
os.unlink(self.lockfile)
|
|
||||||
else: # unix
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
|
||||||
def __enter__(self):
|
|
||||||
try:
|
|
||||||
self.fp = open(self.lockfile, "wb")
|
|
||||||
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
except OSError as e:
|
|
||||||
raise AlreadyRunningException() from e
|
|
||||||
|
|
||||||
def __exit__(self, _type, value, tb):
|
|
||||||
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
|
||||||
self.fp.close()
|
|
||||||
|
|
||||||
|
|
||||||
def launch_room(room: Room, config: dict):
|
def launch_room(room: Room, config: dict):
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ def check():
|
|||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
flash('No file part')
|
flash('No file part')
|
||||||
else:
|
else:
|
||||||
file = request.files['file']
|
files = request.files.getlist('file')
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(files)
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
@@ -39,12 +39,15 @@ def mysterycheck():
|
|||||||
return redirect(url_for("check"), 301)
|
return redirect(url_for("check"), 301)
|
||||||
|
|
||||||
|
|
||||||
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||||
options = {}
|
options = {}
|
||||||
|
for file in files:
|
||||||
# if user does not select file, browser also
|
# if user does not select file, browser also
|
||||||
# submit an empty part without filename
|
# submit an empty part without filename
|
||||||
if file.filename == '':
|
if file.filename == '':
|
||||||
return 'No selected file'
|
return 'No selected file'
|
||||||
|
elif file.filename in options:
|
||||||
|
return f'Conflicting files named {file.filename} submitted'
|
||||||
elif file and allowed_file(file.filename):
|
elif file and allowed_file(file.filename):
|
||||||
if file.filename.endswith(".zip"):
|
if file.filename.endswith(".zip"):
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
|||||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||||
options[file.filename] = zfile.open(file, "r").read()
|
options[file.filename] = zfile.open(file, "r").read()
|
||||||
else:
|
else:
|
||||||
options = {file.filename: file.read()}
|
options[file.filename] = file.read()
|
||||||
if not options:
|
if not options:
|
||||||
return "Did not find a .yaml file to process."
|
return "Did not find a .yaml file to process."
|
||||||
return options
|
return options
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import socket
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
|
import sys
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
from pony.orm import commit, db_session, select
|
from pony.orm import commit, db_session, select
|
||||||
@@ -19,6 +20,7 @@ import Utils
|
|||||||
|
|
||||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||||
from Utils import restricted_loads, cache_argsless
|
from Utils import restricted_loads, cache_argsless
|
||||||
|
from .locker import Locker
|
||||||
from .models import Command, GameDataPackage, Room, db
|
from .models import Command, GameDataPackage, Room, db
|
||||||
|
|
||||||
|
|
||||||
@@ -163,16 +165,21 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
if "worlds" in sys.modules:
|
||||||
|
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||||
|
|
||||||
|
import gc
|
||||||
Utils.init_logging(str(room_id), write_mode="a")
|
Utils.init_logging(str(room_id), write_mode="a")
|
||||||
ctx = WebHostContext(static_server_data)
|
ctx = WebHostContext(static_server_data)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||||
|
gc.collect() # free intermediate objects used during setup
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
except OSError: # likely port in use
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
@@ -198,16 +205,15 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
await ctx.shutdown_task
|
await ctx.shutdown_task
|
||||||
logging.info("Shutting down")
|
logging.info("Shutting down")
|
||||||
|
|
||||||
from .autolauncher import Locker
|
|
||||||
with Locker(room_id):
|
with Locker(room_id):
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except (KeyboardInterrupt, SystemExit):
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||||
except:
|
except Exception:
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
room.last_port = -1
|
room.last_port = -1
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ def generate(race=False):
|
|||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
flash('No file part')
|
flash('No file part')
|
||||||
else:
|
else:
|
||||||
file = request.files['file']
|
files = request.files.getlist('file')
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(files)
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
@@ -130,6 +130,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
erargs.teams = 1
|
erargs.teams = 1
|
||||||
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
||||||
{"bosses", "items", "connections", "texts"}))
|
{"bosses", "items", "connections", "texts"}))
|
||||||
|
erargs.skip_prog_balancing = False
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||||
|
|||||||
51
WebHostLib/locker.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class CommonLocker:
|
||||||
|
"""Uses a file lock to signal that something is already running"""
|
||||||
|
lock_folder = "file_locks"
|
||||||
|
|
||||||
|
def __init__(self, lockname: str, folder=None):
|
||||||
|
if folder:
|
||||||
|
self.lock_folder = folder
|
||||||
|
os.makedirs(self.lock_folder, exist_ok=True)
|
||||||
|
self.lockname = lockname
|
||||||
|
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyRunningException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
class Locker(CommonLocker):
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.lockfile):
|
||||||
|
os.unlink(self.lockfile)
|
||||||
|
self.fp = os.open(
|
||||||
|
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||||
|
except OSError as e:
|
||||||
|
raise AlreadyRunningException() from e
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, tb):
|
||||||
|
fp = getattr(self, "fp", None)
|
||||||
|
if fp:
|
||||||
|
os.close(self.fp)
|
||||||
|
os.unlink(self.lockfile)
|
||||||
|
else: # unix
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
|
||||||
|
class Locker(CommonLocker):
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
self.fp = open(self.lockfile, "wb")
|
||||||
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except OSError as e:
|
||||||
|
raise AlreadyRunningException() from e
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, tb):
|
||||||
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
||||||
|
self.fp.close()
|
||||||
@@ -32,29 +32,34 @@ def page_not_found(err):
|
|||||||
|
|
||||||
# Start Playing Page
|
# Start Playing Page
|
||||||
@app.route('/start-playing')
|
@app.route('/start-playing')
|
||||||
|
@cache.cached()
|
||||||
def start_playing():
|
def start_playing():
|
||||||
return render_template(f"startPlaying.html")
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/weighted-settings')
|
@app.route('/weighted-settings')
|
||||||
|
@cache.cached()
|
||||||
def weighted_settings():
|
def weighted_settings():
|
||||||
return render_template(f"weighted-settings.html")
|
return render_template(f"weighted-settings.html")
|
||||||
|
|
||||||
|
|
||||||
# Player settings pages
|
# Player settings pages
|
||||||
@app.route('/games/<string:game>/player-settings')
|
@app.route('/games/<string:game>/player-settings')
|
||||||
|
@cache.cached()
|
||||||
def player_settings(game):
|
def player_settings(game):
|
||||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
# Game Info Pages
|
# Game Info Pages
|
||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
|
@cache.cached()
|
||||||
def game_info(game, lang):
|
def game_info(game, lang):
|
||||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
# List of supported games
|
# List of supported games
|
||||||
@app.route('/games')
|
@app.route('/games')
|
||||||
|
@cache.cached()
|
||||||
def games():
|
def games():
|
||||||
worlds = {}
|
worlds = {}
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
@@ -64,21 +69,25 @@ def games():
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||||
|
@cache.cached()
|
||||||
def tutorial(game, file, lang):
|
def tutorial(game, file, lang):
|
||||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/')
|
@app.route('/tutorial/')
|
||||||
|
@cache.cached()
|
||||||
def tutorial_landing():
|
def tutorial_landing():
|
||||||
return render_template("tutorialLanding.html")
|
return render_template("tutorialLanding.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/faq/<string:lang>/')
|
@app.route('/faq/<string:lang>/')
|
||||||
|
@cache.cached()
|
||||||
def faq(lang):
|
def faq(lang):
|
||||||
return render_template("faq.html", lang=lang)
|
return render_template("faq.html", lang=lang)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/glossary/<string:lang>/')
|
@app.route('/glossary/<string:lang>/')
|
||||||
|
@cache.cached()
|
||||||
def terms(lang):
|
def terms(lang):
|
||||||
return render_template("glossary.html", lang=lang)
|
return render_template("glossary.html", lang=lang)
|
||||||
|
|
||||||
@@ -147,7 +156,7 @@ def host_room(room: UUID):
|
|||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
def favicon():
|
def favicon():
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
return send_from_directory(os.path.join(app.root_path, "static", "static"),
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
|
||||||
@@ -167,6 +176,7 @@ def get_datapackage():
|
|||||||
|
|
||||||
@app.route('/index')
|
@app.route('/index')
|
||||||
@app.route('/sitemap')
|
@app.route('/sitemap')
|
||||||
|
@cache.cached()
|
||||||
def get_sitemap():
|
def get_sitemap():
|
||||||
available_games: List[Dict[str, Union[str, bool]]] = []
|
available_games: List[Dict[str, Union[str, bool]]] = []
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
flask>=2.2.3
|
flask>=2.2.3
|
||||||
pony>=0.7.16; python_version <= '3.10'
|
pony>=0.7.17
|
||||||
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
|
|
||||||
waitress>=2.1.2
|
waitress>=2.1.2
|
||||||
Flask-Caching>=2.0.2
|
Flask-Caching>=2.0.2
|
||||||
Flask-Compress>=1.13
|
Flask-Compress>=1.14
|
||||||
Flask-Limiter>=3.3.0
|
Flask-Limiter>=3.5.0
|
||||||
bokeh>=3.1.1
|
bokeh>=3.1.1; python_version <= '3.8'
|
||||||
|
bokeh>=3.2.2; python_version >= '3.9'
|
||||||
markupsafe>=2.1.3
|
markupsafe>=2.1.3
|
||||||
|
|||||||
84
WebHostLib/static/assets/supportedGames.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
document.getElementById('js-enabled').style.display = 'block';
|
||||||
|
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
if (gameInfo.classList.contains('collapsed')) {
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle game filter input
|
||||||
|
const gameSearch = document.getElementById('game-search');
|
||||||
|
gameSearch.value = '';
|
||||||
|
|
||||||
|
gameSearch.addEventListener('input', (evt) => {
|
||||||
|
if (!evt.target.value.trim()) {
|
||||||
|
// If input is empty, display all collapsed games
|
||||||
|
return Array.from(gameHeaders).forEach((header) => {
|
||||||
|
header.style.display = null;
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
document.getElementById(`${gameName}-arrow`).innerText = '▶';
|
||||||
|
document.getElementById(gameName).classList.add('collapsed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all the games
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
|
||||||
|
// If the game name includes the search string, display the game. If not, hide it
|
||||||
|
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||||
|
header.style.display = null;
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
console.log(header);
|
||||||
|
header.style.display = 'none';
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('expand-all').addEventListener('click', expandAll);
|
||||||
|
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandAll = () => {
|
||||||
|
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||||
|
// Loop over all the games
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
|
||||||
|
if (header.style.display === 'none') { return; }
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseAll = () => {
|
||||||
|
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||||
|
// Loop over all the games
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
|
||||||
|
if (header.style.display === 'none') { return; }
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -14,6 +14,17 @@ const adjustTableHeight = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an integer number of seconds into a human readable HH:MM format
|
||||||
|
* @param {Number} seconds
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const secondsToHours = (seconds) => {
|
||||||
|
let hours = Math.floor(seconds / 3600);
|
||||||
|
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const tables = $(".table").DataTable({
|
const tables = $(".table").DataTable({
|
||||||
paging: false,
|
paging: false,
|
||||||
@@ -27,7 +38,18 @@ window.addEventListener('load', () => {
|
|||||||
stateLoadCallback: function(settings) {
|
stateLoadCallback: function(settings) {
|
||||||
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
|
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
|
||||||
},
|
},
|
||||||
|
footerCallback: function(tfoot, data, start, end, display) {
|
||||||
|
if (tfoot) {
|
||||||
|
const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x));
|
||||||
|
Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText =
|
||||||
|
(activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
|
||||||
|
}
|
||||||
|
},
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: 'last-activity',
|
||||||
|
name: 'lastActivity'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
targets: 'hours',
|
targets: 'hours',
|
||||||
render: function (data, type, row) {
|
render: function (data, type, row) {
|
||||||
@@ -40,11 +62,7 @@ window.addEventListener('load', () => {
|
|||||||
if (data === "None")
|
if (data === "None")
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
let hours = Math.floor(data / 3600);
|
return secondsToHours(data);
|
||||||
let minutes = Math.floor((data - (hours * 3600)) / 60);
|
|
||||||
|
|
||||||
if (minutes < 10) {minutes = "0"+minutes;}
|
|
||||||
return hours+':'+minutes;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -114,11 +132,16 @@ window.addEventListener('load', () => {
|
|||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
target.find(".table").each(function (i, new_table) {
|
target.find(".table").each(function (i, new_table) {
|
||||||
const new_trs = $(new_table).find("tbody>tr");
|
const new_trs = $(new_table).find("tbody>tr");
|
||||||
|
const footer_tr = $(new_table).find("tfoot>tr");
|
||||||
const old_table = tables.eq(i);
|
const old_table = tables.eq(i);
|
||||||
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
||||||
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
||||||
old_table.clear();
|
old_table.clear();
|
||||||
old_table.rows.add(new_trs).draw();
|
if (footer_tr.length) {
|
||||||
|
$(old_table.table).find("tfoot").html(footer_tr);
|
||||||
|
}
|
||||||
|
old_table.rows.add(new_trs);
|
||||||
|
old_table.draw();
|
||||||
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
||||||
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ const buildUI = (settingData) => {
|
|||||||
weightedSettingsDiv.classList.add('invisible');
|
weightedSettingsDiv.classList.add('invisible');
|
||||||
itemPoolDiv.classList.add('invisible');
|
itemPoolDiv.classList.add('invisible');
|
||||||
hintsDiv.classList.add('invisible');
|
hintsDiv.classList.add('invisible');
|
||||||
|
locationsDiv.classList.add('invisible');
|
||||||
expandButton.classList.remove('invisible');
|
expandButton.classList.remove('invisible');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ const buildUI = (settingData) => {
|
|||||||
weightedSettingsDiv.classList.remove('invisible');
|
weightedSettingsDiv.classList.remove('invisible');
|
||||||
itemPoolDiv.classList.remove('invisible');
|
itemPoolDiv.classList.remove('invisible');
|
||||||
hintsDiv.classList.remove('invisible');
|
hintsDiv.classList.remove('invisible');
|
||||||
|
locationsDiv.classList.remove('invisible');
|
||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1134,8 +1136,8 @@ const validateSettings = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any disabled options
|
|
||||||
Object.keys(settings[game]).forEach((setting) => {
|
Object.keys(settings[game]).forEach((setting) => {
|
||||||
|
// Remove any disabled options
|
||||||
Object.keys(settings[game][setting]).forEach((option) => {
|
Object.keys(settings[game][setting]).forEach((option) => {
|
||||||
if (settings[game][setting][option] === 0) {
|
if (settings[game][setting][option] === 0) {
|
||||||
delete settings[game][setting][option];
|
delete settings[game][setting][option];
|
||||||
@@ -1149,6 +1151,32 @@ const validateSettings = () => {
|
|||||||
) {
|
) {
|
||||||
errorMessage = `${game} // ${setting} has no values above zero!`;
|
errorMessage = `${game} // ${setting} has no values above zero!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove weights from options with only one possibility
|
||||||
|
if (
|
||||||
|
Object.keys(settings[game][setting]).length === 1 &&
|
||||||
|
!Array.isArray(settings[game][setting]) &&
|
||||||
|
setting !== 'start_inventory'
|
||||||
|
) {
|
||||||
|
settings[game][setting] = Object.keys(settings[game][setting])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty arrays
|
||||||
|
else if (
|
||||||
|
['exclude_locations', 'priority_locations', 'local_items',
|
||||||
|
'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) &&
|
||||||
|
settings[game][setting].length === 0
|
||||||
|
) {
|
||||||
|
delete settings[game][setting];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty start inventory
|
||||||
|
else if (
|
||||||
|
setting === 'start_inventory' &&
|
||||||
|
Object.keys(settings[game]['start_inventory']).length === 0
|
||||||
|
) {
|
||||||
|
delete settings[game]['start_inventory'];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1156,6 +1184,11 @@ const validateSettings = () => {
|
|||||||
errorMessage = 'You have not chosen a game to play!';
|
errorMessage = 'You have not chosen a game to play!';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove weights if there is only one game
|
||||||
|
else if (Object.keys(settings.game).length === 1) {
|
||||||
|
settings.game = Object.keys(settings.game)[0];
|
||||||
|
}
|
||||||
|
|
||||||
// If an error occurred, alert the user and do not export the file
|
// If an error occurred, alert the user and do not export the file
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
userMessage.innerText = errorMessage;
|
userMessage.innerText = errorMessage;
|
||||||
|
|||||||
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/advanceballistics.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/autoturretblackops.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/burstcapacitors.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclone.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/drillingclaws.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/emergencythrusters.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/hellionbattlemode.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperflightrotors.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperfluxor.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/impalerrounds.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedburstlaser.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedsiegemode.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/interferencematrix.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
WebHostLib/static/static/icons/sc2/jotunboosters.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/jumpjets.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/liberator.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/lockdown.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/magrailmunitions.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/opticalflare.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/optimizedlogistics.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/restoration.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/ripwavemissiles.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/shreddermissile.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetankrange.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/specialordance.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/spidermine.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/staticempblast.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/superstimpack.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
WebHostLib/static/static/icons/sc2/targetingoptics.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-cloak-color.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-emp-color.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/thorsiegemode.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/transformationservos.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/valkyrie.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/warpjump.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowminehidden.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -235,9 +235,6 @@ html{
|
|||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing .variable{
|
|
||||||
color: #ffff00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-deco{
|
.landing-deco{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ html{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#player-settings{
|
#player-settings{
|
||||||
max-width: 1000px;
|
box-sizing: border-box;
|
||||||
|
max-width: 1024px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
@@ -163,6 +164,11 @@ html{
|
|||||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#player-settings table .randomize-button[data-tooltip]::after {
|
||||||
|
left: unset;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#player-settings table label{
|
#player-settings table label{
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
@@ -177,18 +183,31 @@ html{
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
@media all and (max-width: 1024px) {
|
||||||
|
#player-settings {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#player-settings #game-options{
|
#player-settings #game-options{
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-settings .left, #player-settings .right{
|
#player-settings .left,
|
||||||
flex-grow: unset;
|
#player-settings .right {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-options table {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#game-options table label{
|
#game-options table label{
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#game-options table tr td {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
padding: 3px 3px 10px;
|
padding: 3px 3px 10px;
|
||||||
width: 500px;
|
width: 710px;
|
||||||
background-color: #525494;
|
background-color: #525494;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,10 +34,12 @@
|
|||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
border: 1px solid #000000;
|
border: 1px solid #000000;
|
||||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||||
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table img.acquired{
|
#inventory-table img.acquired{
|
||||||
filter: none;
|
filter: none;
|
||||||
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table div.counted-item {
|
#inventory-table div.counted-item {
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#location-table{
|
#location-table{
|
||||||
width: 500px;
|
width: 710px;
|
||||||
border-left: 2px solid #000000;
|
border-left: 2px solid #000000;
|
||||||
border-right: 2px solid #000000;
|
border-right: 2px solid #000000;
|
||||||
border-bottom: 2px solid #000000;
|
border-bottom: 2px solid #000000;
|
||||||
|
|||||||
@@ -18,6 +18,20 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#games h2 .collapse-arrow{
|
||||||
|
font-size: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games h2 .game-name{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games p.collapsed{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#games a{
|
#games a{
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -31,3 +45,17 @@
|
|||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#games #page-controls{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games #page-controls button{
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games #js-enabled{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,16 +55,16 @@ table.dataTable thead{
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody{
|
table.dataTable tbody, table.dataTable tfoot{
|
||||||
background-color: #dce2bd;
|
background-color: #dce2bd;
|
||||||
font-family: LexendDeca-Light, sans-serif;
|
font-family: LexendDeca-Light, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody tr:hover{
|
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
|
||||||
background-color: #e2eabb;
|
background-color: #e2eabb;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody td{
|
table.dataTable tbody td, table.dataTable tfoot td{
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{
|
|||||||
top: 46px;
|
top: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody td{
|
table.dataTable tbody td, table.dataTable tfoot td{
|
||||||
border: 1px solid #bba967;
|
border: 1px solid #bba967;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.dataTable tfoot td{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
div.dataTables_scrollBody{
|
div.dataTables_scrollBody{
|
||||||
background-color: inherit !important;
|
background-color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<div id="check-form-wrapper">
|
<div id="check-form-wrapper">
|
||||||
<form id="check-form" method="post" enctype="multipart/form-data">
|
<form id="check-form" method="post" enctype="multipart/form-data">
|
||||||
<input id="file-input" type="file" name="file">
|
<input id="file-input" type="file" name="file" multiple>
|
||||||
</form>
|
</form>
|
||||||
<button id="check-button">Upload</button>
|
<button id="check-button">Upload File(s)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -203,10 +203,10 @@ Warning: playthrough can take a significant amount of time for larger multiworld
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="generate-form-button-row">
|
<div id="generate-form-button-row">
|
||||||
<input id="file-input" type="file" name="file">
|
<input id="file-input" type="file" name="file" multiple>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<button id="generate-game-button">Upload File</button>
|
<button id="generate-game-button">Upload File(s)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
WebHostLib/templates/hintTable.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% for team, hints in hints.items() %}
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Finder</th>
|
||||||
|
<th>Receiver</th>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Entrance</th>
|
||||||
|
<th>Found</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- for hint in hints -%}
|
||||||
|
<tr>
|
||||||
|
<td>{{ long_player_names[team, hint.finding_player] }}</td>
|
||||||
|
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
|
||||||
|
<td>{{ hint.item|item_name }}</td>
|
||||||
|
<td>{{ hint.location|location_name }}</td>
|
||||||
|
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||||
|
<td>{% if hint.found %}✔{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% block footer %}
|
{% block footer %}
|
||||||
<footer id="island-footer">
|
<footer id="island-footer">
|
||||||
<div id="copyright-notice">Copyright 2022 Archipelago</div>
|
<div id="copyright-notice">Copyright 2023 Archipelago</div>
|
||||||
<div id="links">
|
<div id="links">
|
||||||
<a href="/sitemap">Site Map</a>
|
<a href="/sitemap">Site Map</a>
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
our crazy idea into a reality.
|
our crazy idea into a reality.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="variable">{{ seeds }}</span>
|
<a href="{{ url_for("stats") }}">{{ seeds }}</a>
|
||||||
games were generated and
|
games were generated and
|
||||||
<span class="variable">{{ rooms }}</span>
|
<a href="{{ url_for("stats") }}">{{ rooms }}</a>
|
||||||
were hosted in the last 7 days.
|
were hosted in the last 7 days.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||